#27507

Performance: array.slice() marks the source buffer (mark_buffer_has_slices) on every slice — ~13% of a hot loop for transient read-only views

Openenghitaloopened 4mo ago

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)

  1. A zero-cost read-only view primitive, e.g. a.window(start, len) or an explicit unsafe/non-marking slice form, returning a []T that shares the backing but does not mark the parent. (Today this requires hand-fabricating the array struct in unsafe.)
  2. Skip mark_buffer_has_slices() when the source is not autofree-managed (e.g. -gc none/-manualfree), where has_slices is dead.
  3. Make the marking cheaper — avoid the data_header() pointer round-trip per slice (e.g. fold has_slices into the array header that's already in registers, or set it lazily only when a delete/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

0

No activity yet.