Skip to content

perf: make lending buffers per event cheap again - #106

Merged
Tuntii merged 1 commit into
mainfrom
perf/pool-stack
Oct 29, 2026
Merged

Tuntii merged 1 commit into
mainfrom
perf/pool-stack

Conversation

@Tuntii

@Tuntii Tuntii commented Oct 28, 2026

Copy link
Copy Markdown
Owner

The HttpArena run of v1.13.1 (MDA2AV/HttpArena#1660) came back lower than the v1.10.0 run on some profiles. I measured where that comes from.

What the drop is

hostlab's wall-clock numbers are too noisy to see a few percent, so I counted user-space instructions with callgrind instead. The count is deterministic. The server is pinned to 4 cores and the load comes from the other 4; startup is excluded.

build baseline, Ir/request json-comp, Ir/request WS echo, Ir/message
v1.10.0 9,902 375,259
v1.12.0 9,928
v1.13.0 9,922 2,286
v1.13.1 (#101) 10,741 375,413 3,096
main (v1.13.3) 10,763 3,110
this PR 10,270 2,616
  • perf: lend connection buffers per event from worker pools #101 is the only step that costs anything. Lending buffers per event adds about 820 instructions to every event: +8% on a keep-alive request and +35% on a WebSocket echo, which does little else per message.
  • json-comp is unchanged (375K per request, almost all gzip). Its -8.5% on HttpArena does not come from viltrum's code. The run it was compared against (959K rps) was itself 6% above the two runs before it (899K and 901K).
  • baseline's -6% on HttpArena came with the same CPU per request (-0.6%); the server just got less CPU in that run. Other entries benchmarked on the same days moved by similar amounts in both directions (rage -10.9%, fulmine +3.3%).
  • echo-ws's -3% fits the extra instructions. It had been within 0.6% over the previous three runs.

The change

V 0.5.2's pop(), <<, array element stores and []u8{} each go through a runtime call and a memcpy of the array header. Lend and settle did several of them per event, plus three calls of their own.

  • BufPool is a stack whose slots are written through a pointer, so a move is a plain struct copy. The slot array only grows, on the cold path.
  • A connection without a buffer gets the pool's empty header instead of a new []u8{}.
  • lend, settle, give_back and Loop.settle are @[inline].

This removes about 500 of the 820 instructions. Memory is unchanged: buffers still go back to the pool after every event.

Two things I tried and dropped: storing slots with bufs[n] = b gained nothing, because array_set has the same memcpy. Avoiding the pointer locals that V pins with GC_reachable_here gained 3 instructions.

Checks

All on hostlab:

  • With V 0.5.2: every step of the CI test job (unit tests, debug, gcc TLS/deflate, hotpath_lint, gcpin_check, the six examples) passes with 0 warnings. v fmt -verify is clean.
  • With V master (ee6d62a): the same steps pass with 0 warnings. Its C for BufPool has no array__clone or memdup.
  • I did not run the 500k-iteration fuzz steps.

Lending buffers per event (#101) added about 820 user-space instructions to every
event on V 0.5.2: array pop and push, empty-array literals, and the calls to lend,
settle and give_back. That is +35% on a WebSocket echo and +8% on a keep-alive
request (callgrind on hostlab).

The pools are now a stack written through a pointer, so a move is a plain struct copy,
and the per-event functions are inlined. Instructions per WebSocket echo go from 3110
to 2616 (-16%) and per baseline request from 10763 to 10270 (-4.6%). Memory is
unchanged: the buffers still go back after each event.
@Tuntii
Tuntii merged commit 071aa35 into main Oct 29, 2026
3 checks passed
@Tuntii
Tuntii deleted the perf/pool-stack branch October 30, 2026 11:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant