solidity: write struct fields directly into result memory on deserialize#97
Draft
deuszx wants to merge 1 commit into
Draft
solidity: write struct fields directly into result memory on deserialize#97deuszx wants to merge 1 commit into
deuszx wants to merge 1 commit into
Conversation
`bcs_deserialize_offset_<Struct>` previously held every decoded field
as a stack local until the final `Struct(field0, field1, ...)`
constructor. At ~2 stack slots per field (the value plus the second
slot of the `(new_pos, value)` tuple return) Yul ran out of the
16-slot budget when these helpers were inlined into wider call
graphs. The current `linera-bridge` workaround is a `build.rs` shim
that rewrites every `assembly { ... }` block in the generated file
to `assembly ("memory-safe") { ... }` so Yul's optimizer is allowed
to spill across them.
Switch the generated code to declare the return value with a named
binding (`returns (uint256, <Struct> memory result)`) and write each
field through `result.<field>` as it is decoded. The only persistent
locals are now `pos`, `new_pos`, and the `result` pointer — stack
footprint is O(1) regardless of field count, and the final
`Struct(...)` constructor call (which previously memcpy'd N locals
into a freshly allocated struct) disappears.
Validation against linera-protocol#6294:
* Before this change, with the in-tree codegen output and the
`memory-safe` annotations stripped, `forge build` on
`linera-bridge/src/solidity` fails with
"Cannot swap Variable var_input_2223_mpos … too deep in the stack
by 1 slots" inside the inlined `verifyCertificate` deserializer
chain.
* After regenerating `BridgeTypes.sol` from the same registry
snapshot using this commit's codegen — still without any
`memory-safe` annotations — `forge build` succeeds. The
`mark_assembly_memory_safe` shim in `linera-bridge/build.rs`
becomes obsolete.
Measured on linera-bridge after regen (via_ir, optimizer_runs=1,
solc 0.8.30, evm_version=cancun):
Metric | Old + shim | New (no shim) | Delta
------------------------------------|-------------|---------------|--------
LightClient runtime bytecode | 23 296 B | 22 725 B | -571 B
EIP-170 headroom for LightClient | 1 280 B | 1 851 B | +571 B
LightClient deployment size | 24 785 B | 24 214 B | -571 B
FungibleBridge runtime bytecode | 10 987 B | 10 958 B | -29 B
DeployLightClient script gas | 10 838 349 | 10 600 945 | -237 K
Synthetic gas/bytecode measurement on a 16x Inner struct
(Inner = { String, u64, u128, Vec<u32> }, via_ir = true,
optimizer_runs = 200, solc 0.8.33):
* deserialize gas: 217 224 -> 210 076 (-7 148, -3.3 %)
* harness bytecode: 3 088 B -> 2 759 B (-329 B, -11 %)
* deployment gas: 715 222 -> 644 074 (-71 K, -10 %)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
bcs_deserialize_offset_<Struct>previously held every decoded field as a stack local until the finalStruct(field0, field1, ...)constructor. At ~2 stack slots per field (the value plus the second slot of the(new_pos, value)tuple return) Yul ran out of the 16-slot budget when these helpers were inlined into wider call graphs. The currentlinera-bridgeworkaround is abuild.rsshim that rewrites everyassembly { ... }block in the generated file toassembly ("memory-safe") { ... }so Yul's optimizer is allowed to spill across them.Switch the generated code to declare the return value with a named binding (
returns (uint256, <Struct> memory result)) and write each field throughresult.<field>as it is decoded. The only persistent locals are nowpos,new_pos, and theresultpointer — stack footprint is O(1) regardless of field count, and the finalStruct(...)constructor call (which previously memcpy'd N locals into a freshly allocated struct) disappears.Validation against linera-protocol#6294:
memory-safeannotations stripped,forge buildonlinera-bridge/src/solidityfails with "Cannot swap Variable var_input_2223_mpos … too deep in the stack by 1 slots" inside the inlinedverifyCertificatedeserializer chain.BridgeTypes.solfrom the same registry snapshot using this commit's codegen — still without anymemory-safeannotations —forge buildsucceeds. Themark_assembly_memory_safeshim inlinera-bridge/build.rsbecomes obsolete.Measured on linera-bridge after regen (via_ir, optimizer_runs=1, solc 0.8.30, evm_version=cancun):
Synthetic gas/bytecode measurement on a 16x Inner struct (Inner = { String, u64, u128, Vec }, via_ir = true, optimizer_runs = 200, solc 0.8.33):
Test Plan
New test
test_wide_struct_deserialize_round_tripwas added.