Skip to content

Allocation

"prefix_" + str(i) one million times.

Results

Language Mean vs C++
C++ 11.8 ms
Rust 23.7 ms
Python 79 ms 6.7×

Raw: results_bench_alloc.md.

Wall clock

Why C++ wins here (SSO)

graph TD
    subgraph cpp["C++ std::string (8-13 chars)"]
        inline["bytes live inside string object on stack"]
    end

    subgraph py["Python str"]
        hdr["PyObject header on heap"]
        dat["payload on heap"]
        hdr --> dat
    end

    style inline fill:#1565c0,color:#fff
    style hdr fill:#b71c1c,color:#fff

Rust format! always hits the heap. Slower than C++ SSO, still faster than building a full PyUnicodeObject.

Gap vs reference passing

xychart-beta
    title "Python slowdown vs fastest native"
    x-axis ["ref-pass", "alloc"]
    y-axis "times slower" 0 --> 70
    bar [65, 6.7]

Everyone pays for allocation. Python's pymalloc holds up reasonably well. The PyObject header (~80+ bytes) still hurts next to a 13-char SSO buffer.

Cachegrind (alloc)

Same cachegrind pipeline as reference passing. Gap vs ref-pass shrinks because everyone spends cycles in the allocator.

C++ Python ratio
Instructions 304M 2,223M 7.3×
L1 misses 15K 827K 55.9×

Instructions (millions)

L1 data cache misses

Last-level cache misses

Python divided by C++

Overhead split (estimated)

Source

Python

python/bench_alloc.py
import time

def make_obj(i):
    # Create a new string dynamically to force allocation
    # str(i) creates a new PyObject
    return "prefix_" + str(i)

def main():
    start = time.perf_counter()
    for i in range(1_000_000): # Reduced to 1M to save time
        t = make_obj(i)
    end = time.perf_counter()
    print(f"Time: {end - start:.6f} s")

if __name__ == "__main__":
    main()

C++

cpp/bench_alloc.cpp
#include <string>
#include <iostream>
#include <chrono>

// Return by value triggers allocation (unless SSO works, so we make it large enough to bypass SSO if we want true malloc, but let's test small string creation first as user asked "making objects")
// Actually, Python allocates for EVERYTHING. C++ SSO optimization is a valid advantage.
// Let's make it comparable to Python: "prefix_" + number.
// This is likely small enough for SSO in some implementations (16 chars), but let's see.

std::string make_obj(int i) {
    return "prefix_" + std::to_string(i);
}

int main() {
    auto start = std::chrono::high_resolution_clock::now();

    for (int i = 0; i < 1000000; ++i) {
        std::string t = make_obj(i);
        asm volatile("" :: "r"(&t) : "memory");
    }

    auto end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> diff = end - start;
    std::cout << "Time: " << diff.count() << " s" << std::endl;
    return 0;
}

Rust

rust/bench_alloc.rs
use std::time::Instant;
use std::hint::black_box;

fn make_obj(i: i32) -> String {
    format!("prefix_{}", i)
}

fn main() {
    let start = Instant::now();
    for i in 0..1_000_000 {
        let t = make_obj(i);
        black_box(t);
    }
    let duration = start.elapsed();
    println!("Time: {:.6} s", duration.as_secs_f64());
}

Run

make bench_languages
# or:
python3 python/bench_alloc.py && ./cpp/bench_alloc && ./rust/bench_alloc