Describe the bug
array.slice() (i.e. every a[start..end]) unconditionally calls mark_buffer_has_slices() on the source array — which computes the array's malloc data-header pointer (data_header()) and writes header.has_slices = true — plus a uses_noscan_data() flag check and the result-struct build, on every slice.
For the extremely common pattern of a transient, read-only sub-view (parsers, byte scanners, splitting a buffer), this bookkeeping is pure overhead: nothing ever consults has_slices, because the parent is not delete()d/shrunk while the view is alive. In a hot loop it dominates.
Reproduction
module main
@[direct_array_access]
fn sum_via_slice(buf []u8, iters int) u64 {
mut acc := u64(0)
for _ in 0 .. iters {
s := buf[8..buf.len] // read-only window, dropped immediately
for b in s {
acc += b
}
}
return acc
}
fn main() {
mut buf := []u8{len: 0, cap: 1024}
for i in 0 .. 256 {
buf << u8(i)
}
println('acc=${sum_via_slice(buf, 2_000_000)}')
}v -prod -g -gc none -cc gcc -o slice_repro slice_repro.v
valgrind --tool=callgrind ./slice_repro && callgrind_annotate callgrind.out.*Result (V 0.5.1 c0624b2, Linux x86-64, gcc):
830,199,252 (100.0%) PROGRAM TOTALS
108,000,000 (13.01%) vlib/builtin/array.v:builtin__array_slice (2,000,000x → 54 instr/slice)array_slice alone is 13% of all instructions — for a slice that is only read and then discarded. In a real HTTP/1.1 server hot path (re-slicing the read buffer twice per request) it measured 11% of -prod instructions; replacing those two slices with a hand-rolled non-marking view cut total instructions/request by ~10%.
What the codegen shows
-prod emits (abridged):
VV_LOC array builtin__array_slice(array a, int start, int _end) {
... bounds checks (still present in -prod) ...
builtin__array_mark_buffer_has_slices(&a); // <-- unconditional, every call
... uses_noscan_data check ...
array res = (array){ .data=..., .len=l, .cap=l, .flags=ArrayFlags__is_slice, ... };
return res;
}
inline VV_LOC void builtin__array_mark_buffer_has_slices(array* a) {
if (!ArrayFlags_has(&a->flags, managed) || a->data == 0) return;
ArrayDataHeader* header = builtin__array_data_header(*a); // header pointer round-trip
if (header) header->has_slices = true;
}Why this is avoidable
has_slices exists so delete()/shrink can preserve correctness when a live slice still references the buffer. But the overwhelmingly common case — a slice that is read and dropped before any mutation of the parent — never benefits from the flag. Marking the parent on every transient view is wasted work, and it is especially pointless under -gc none / -manualfree, where the buffer is managed by hand and never delete()d with a live slice.
Proposals (any one would help)
- A zero-cost read-only view primitive, e.g.
a.window(start, len)or an explicitunsafe/non-marking slice form, returning a[]Tthat shares the backing but does not mark the parent. (Today this requires hand-fabricating thearraystruct inunsafe.) - Skip
mark_buffer_has_slices()when the source is not autofree-managed (e.g.-gc none/-manualfree), wherehas_slicesis dead. - Make the marking cheaper — avoid the
data_header()pointer round-trip per slice (e.g. foldhas_slicesinto the array header that's already in registers, or set it lazily only when adelete/shrink actually runs and a slice exists).
Environment
- V 0.5.1 c0624b2, Linux x86-64, gcc,
-prod -gc none.
Happy to provide the full HTTP-server profile or test any patch.
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.