Context
For thread-per-core workloads (e.g. HTTP servers where each worker is pinned to a core and allocates independently), allocation throughput should scale roughly with core count. In practice V's allocation does not scale — and with the default GC it gets worse with more cores.
This is a discussion/RFC, not a crash bug. The data below is reproducible on master.
Benchmark
Each of N threads performs 500k []u8{len: 64} allocations in a tight loop (memory bounded by count — N × 500k × 64 B). We measure aggregate allocations/sec. If allocation scaled, aggregate would grow ~linearly with N and per-thread would stay roughly flat.
import time
import os
const alloc_size = 64
fn worker(count int, sink &u64) {
mut acc := u64(0)
for _ in 0 .. count {
b := []u8{len: alloc_size}
unsafe {
b[0] = u8(acc) // touch so the alloc isn't optimised away
acc += u64(b.data)
}
}
unsafe { *sink = acc }
}
fn main() {
nthreads := os.getenv_opt('NT') or { '1' }.int()
count := os.getenv_opt('COUNT') or { '500000' }.int()
mut sinks := []u64{len: nthreads}
sw := time.new_stopwatch()
mut threads := []thread{}
for i in 0 .. nthreads {
threads << spawn worker(count, unsafe { &sinks[i] })
}
threads.wait()
secs := f64(sw.elapsed().nanoseconds()) / 1e9
total := i64(nthreads) * i64(count)
println('NT=${nthreads} aggregate=${f64(total) / secs:.0f}/s per_thread=${f64(total) / secs / f64(nthreads):.0f}/s time=${secs:.3f}s')
}Run (pin to N cores):
v -prod -gc none -o as_none bench.v # libc malloc
v -prod -o as_boehm bench.v # default GC (Boehm)
for nt in 1 2 4 8 16; do NT=$nt taskset -c 0-$((nt-1)) ./as_none; done
for nt in 1 2 4 8 16; do NT=$nt taskset -c 0-$((nt-1)) ./as_boehm; doneResults (AMD Ryzen 7 5800H, 8 cores / 16 threads; 3× re-runs, stable)
| threads | -gc none aggregate/s | per-thread | default GC aggregate/s | per-thread |
|---|---|---|---|---|
| 1 | 13.0 M | 13.0 M | 18.5 M | 18.5 M |
| 2 | 20.4 M | 10.2 M | 21.7 M | 10.8 M |
| 4 | 33.3 M | 8.3 M | 18.1 M | 4.5 M |
| 8 | 33.1 M | 4.1 M | 16.6 M | 2.1 M |
| 16 | 32.0 M | 2.0 M | 18.0 M | 1.15 M |
Observations
- Default GC (Boehm): aggregate throughput is essentially flat at ~18 M/s regardless of core count — 16 cores deliver the same total allocation throughput as a single core, and per-thread throughput collapses ~16×. Allocation is effectively serialized.
-gc none(libc malloc): scales ~2.5× from 1→4 threads, then plateaus past 4 cores (well below the 8 physical cores) — glibc per-arena lock / arena contention.
Discussion / questions
- Is this scaling profile expected for the default GC?
- Would thread-local allocation caches (or a per-thread arena in front of the GC) be in scope?
- Is an optional scalable allocator backend (e.g. mimalloc / jemalloc) selectable via
-gcsomething the project would consider? - Failing the above, would docs guidance for thread-per-core designs ("minimise per-request allocation; the allocator does not scale across cores") be welcome?
For the server this came from, the practical answer was to drive the hot path to zero allocations — but the underlying allocator scalability seemed worth surfacing.
V version
V 0.5.1 98bbdd7 (master)
OS
Linux x86_64 (AMD Ryzen 7 5800H, 8C/16T)
NOTE
You can use the 👍 reaction to increase the issue's priority for developers.
Please note that only the 👍 reaction to the issue itself counts as a vote. Other reactions and those to comments will not be taken into account.
Activity
0No activity yet.