Skip to content

compiler: compare large plain structs with memequal - #6095

Draft
0pcom wants to merge 1 commit into
tinygo-org:devfrom
0magnet:compiler-struct-memequal-pr
Draft

0pcom wants to merge 1 commit into
tinygo-org:devfrom
0magnet:compiler-struct-memequal-pr

Conversation

@0pcom

@0pcom 0pcom commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

Comparing a large struct lowered to one comparison per field. crypto/mldsa compares its 91 KB PrivateKey with the zero value, which made a single huge function that V8 rejects after Asyncify and that wasm-opt struggles with. This extends the existing memequal path for large arrays to structs whose memory fully determines equality, which means no blank fields, no padding and only binary comparable fields. gc makes the same check (cmd/compile/internal/types/size.go). A constant operand is read from a global so the zero value is not copied to the stack.

This is the trimmed version discussed in #5862. An ML-DSA sign and verify program built for js/wasm did not finish within 400 s on dev. With this change it builds in 22 s and verifies under node. The new case in compiler/testdata/basic.go fails without the change. TestBuild binop.go passes, and binop.go matches on js under node, on 386, and on arm64 and arm under qemu. Only the four TestCompiler goldens that also fail on dev with LLVM 23 fail here (slice, float, zeromap and large).

Fixes #5862

Struct equality was lowered to one comparison per field, recursing into
arrays. crypto/mldsa compares its 91 KB PrivateKey with the zero value,
which became a single function of about 98k instructions. Asyncify then
gave it 125k locals, past what V8 accepts, and wasm-opt ran out of
memory on it.

Extend the memequal path for large arrays to structs whose memory fully
determines equality: no blank fields, no padding, and only binary
comparable fields. gc makes the same check before comparing a struct as
plain memory (cmd/compile/internal/types/size.go). Read a constant
operand from a global so that the zero value is not copied to a
temporary.

Fixes tinygo-org#5862
@dgryski

dgryski commented Oct 29, 2026

Copy link
Copy Markdown
Member

/cc @jakebailey

Comment thread compiler/compiler.go
func canUseDereferencePointer(unop *ssa.UnOp) bool {
// isPlainComparable reports whether two values of typ are equal exactly when
// their memory is, meaning no blank fields, no padding and only binary comparable fields.
func (c *compilerContext) isPlainComparable(typ types.Type) bool {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit shocked that this does not already exist? I know Dave touched very similar code, the code which generates the runtime funcs etc etc?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The closest existing helper is isBinaryComparable in compiler/map.go, used for map keys, arrays and interfaces. It rejects structs on purpose because padding and blank fields make memequal wrong. isPlainComparable reuses it for the fields and only adds the struct case, which needs the target layout to rule out padding, so it is a compilerContext method. Dave's recent compare changes (8119216, 04ca26f) avoid byte slice copies for strings, a different path. I could fold the struct case into isBinaryComparable so padding-free struct map keys use memequal too, but that seemed better as a separate change.

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.

compiler: comparing a large struct makes a function V8 rejects on wasm

3 participants