Allocation¶
"prefix_" + str(i) one million times.
Results¶
| Language | Mean | vs C++ |
|---|---|---|
| C++ | 11.8 ms | 1× |
| Rust | 23.7 ms | 2× |
| 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());
}