Skip to content

crypto/blake2b: bound input of assembly call - #35550

Open
AskAlexSharov wants to merge 2 commits into
ethereum:masterfrom
AskAlexSharov:blake2b-bound-rounds
Open

crypto/blake2b: bound input of assembly call#35550
AskAlexSharov wants to merge 2 commits into
ethereum:masterfrom
AskAlexSharov:blake2b-bound-rounds

Conversation

@AskAlexSharov

Copy link
Copy Markdown

Problem

GC stop-the-world is slow if a large rounds is passed to the BLAKE2b F precompile.

Root cause: rounds comes straight from calldata as a uint32 and is priced at one gas per round, so a single transaction can ask for tens of millions of rounds. fAVX2, fAVX and fSSE4 run that whole loop inside NOSPLIT assembly:

TEXT ·fAVX2(SB), 4, $64-48        // 4 = NOSPLIT
	MOVQ rounds+40(FP), BX
loop:   ...
	JMP loop

Max GC stop-the-world stopping pause (/sched/pauses/stopping/gc:seconds), EPYC 4344P, GOMAXPROCS=2:

rounds main this PR
1,048,576 8.389 ms 0.057 ms
45,000,000 (a full block of gas) 402.653 ms 0.057 ms

Throughput is unchanged — ns/round, best of 5 runs, no collector running:

rounds main this PR
12 8.250 8.250
100 7.680 7.660
1000 7.616 7.606
4090 7.608 7.608
65536 7.587 7.581
1048576 7.587 7.599

Fix

F hands anything over 4090 rounds to fLong, which splits it into chunks.

The existing assembly cannot be split: it derives the working vector v from h, runs every round, and folds v back into h, all inside one call. So a new entry point fAVX2Rounds loads v from memory and stores it back, which is what lets a long F resume between chunks. The chunk size is a multiple of 10 because the round function permutes the message with period 10 and the assembly unrolls exactly those ten permutations — a chunk ending mid-cycle would restart at the wrong one.

fRounds is //go:noinline on purpose: its prologue carries the stack-growth check that is the chunk loop's only preemption point. Inlined, the loop would call NOSPLIT assembly directly and be unpreemptible again.

TestFChunkedMatchesGeneric checks the chunked assembly against the pure-Go reference across round counts, including the chunk boundary.

References:

The rounds argument of the BLAKE2b F precompile comes straight from calldata
as a uint32 and is priced at one gas per round, so a single transaction can
ask for tens of millions of rounds. fAVX2, fAVX and fSSE4 run that whole loop
inside NOSPLIT assembly, which the runtime cannot preempt, so one call holds
every P in stop-the-world for its duration: 402 ms for a full block of gas.

F now hands anything over 4090 rounds to fLong, which splits it into chunks.
The existing assembly cannot be split -- it derives the working vector v from
h, runs every round and folds v back into h inside one call -- so a new entry
point fAVX2Rounds takes v through memory instead, letting a long F resume
between chunks. The chunk size is a multiple of 10 because the round function
permutes the message with period 10 and the assembly unrolls exactly those ten
permutations.

Worst stop-the-world stopping pause drops from 402.653 ms to 0.057 ms at
45,000,000 rounds, and from 8.389 ms to 0.057 ms at 1,048,576. Throughput is
unchanged at 7.59 ns/round.
The 10 was repeated as a literal in the table size and the round index, with
the chunk-alignment rule stated only in prose. sigmaRounds carries it, the
table is declared with it, and a compile-time check pins maxAsmRounds to a
whole cycle.
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