Skip to content

keccakf_amd64: drop NOSPLIT so the runtime can preempt the block loops - #32

Open
AskAlexSharov wants to merge 1 commit into
masterfrom
fix/amd64-preemption
Open

keccakf_amd64: drop NOSPLIT so the runtime can preempt the block loops#32
AskAlexSharov wants to merge 1 commit into
masterfrom
fix/amd64-preemption

Conversation

@AskAlexSharov

Copy link
Copy Markdown
Contributor

keccakF1600BMI2 is declared NOSPLIT, which drops the stack check whatever the frame size. That check is the only cooperative preemption point in the absorb and squeeze loops that call it — the loops call nothing else, and assembly bodies are not async-preemptible — so one large Sum256 or Write holds every P in stop-the-world until it returns.

Max GC stopping pause while hashing, EPYC 4344P, GOMAXPROCS=2:

32 MiB 128 MiB
master 50.3 ms 67.1 ms
this PR 0.098 ms 0.016 ms

Throughput is unchanged — -count=10, same idle box:

Size master PR
32 B 203.8 ns 203.5 ns ~
128 B 209.7 ns 208.3 ns -0.67%
256 B 492.5 ns 484.6 ns -1.61%
1 KB 1.773 us 1.794 us +1.18%
4 KB 6.187 us 6.286 us +1.60%
500 KB 724.0 us 732.6 us +1.19%
geomean +0.25%

arm64 never had NOSPLIT and does not show the stall, which is what the diagnosis rests on. The frame stays 200 bytes: a leaf with a zero frame gets no stack check either, so shrinking it reintroduces the stall — that is also why #12, which makes the arm64 kernel NOSPLIT, $0-16, would extend this behaviour to arm64. Measured there: 0.164 ms to 100.7 ms at 128 MiB.

I first fixed this by chunking the Go-side block loops so they return to a function carrying a stack check every 16 KiB. That also works (0.049 ms) but costs 5.5-8.9% geomean and about 16% on 32 B inputs, and the cost stayed even when the small-input path executed code identical to master, so it is package layout rather than the added logic. Two lines beat it.

The generator carries the same change, so go generate reproduces the committed .s, and a comment there records why NOSPLIT must not come back.

keccakF1600BMI2 was NOSPLIT, which removes the stack check regardless of
frame size. That check is the only cooperative preemption point in the
absorb and squeeze loops that call it: the loops themselves call nothing
else, and assembly bodies are not async-preemptible, so the runtime has
nowhere to land. One large Sum256 or Write then holds every P in
stop-the-world for its whole duration.

Max GC stopping pause while hashing on an EPYC 4344P, GOMAXPROCS=2:

           32 MiB     128 MiB
  before    50.3 ms     67.1 ms
  after      0.098 ms    0.016 ms

Throughput is unchanged: +0.25% geomean over -count=10, with 256 B and
128 B slightly faster and the large sizes about 1% slower.

The arm64 kernel never had NOSPLIT and does not show the problem, which is
what the comparison above rests on. The frame stays at 200 bytes: a leaf
with a zero frame gets no stack check either, so shrinking it would bring
the stall back.
@AskAlexSharov

Copy link
Copy Markdown
Contributor Author

Second machine (n0)

Repeated on a second idle box, dev-bm-e3-ethmainnet-n0. Same CPU model as the numbers above (EPYC 4344P) and same Go 1.26.6, so this is a repeatability check rather than a second microarchitecture.

Max GC stopping pause while hashing, GOMAXPROCS=2:

32 MiB 128 MiB
master 50.332 ms 83.886 ms
this PR 0.029 ms 0.014 ms

The stall reproduces to the same bucket at 32 MiB on both boxes, and the fix lands in the tens-of-microseconds range on both.

Throughput, -count=10:

Size master PR n5 said
32 B 212.2 ns 211.6 ns -0.28% ~
128 B 218.1 ns 216.2 ns -0.87% -0.67%
256 B 511.2 ns 503.4 ns -1.53% -1.61%
1 KB 1.845 us 1.892 us +2.55% +1.18%
4 KB 6.433 us 6.550 us +1.82% +1.60%
500 KB 756.9 us 760.6 us +0.49% +1.19%
geomean +0.35% +0.25%

Same shape on both machines: the small sizes come out slightly faster, 1 KB and 4 KB slightly slower, and the total is a fraction of a percent. The one number that moved between boxes is 1 KB, +1.18% on n5 against +2.55% here; every size is ±0% within its own run, so that is a between-machine difference rather than measurement noise.

n0 runs about 4% slower than n5 across the board on identical silicon, so treat the absolute figures as per-box and the deltas as the comparable part.

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