Avoid bit-loop lowering of 256-bit division and remainder#576
Conversation
div/mod/mulmod| %prodl = trunc i512 %prode to i256 | ||
| %prodeh = lshr i512 %prode, 256 | ||
| %prodh = trunc i512 %prodeh to i256 | ||
| %res = call i256 @__ulongrem(i256 %prodl, i256 %prodh, i256 %modulo) |
There was a problem hiding this comment.
do we still have uses of _ulongrem ?
There was a problem hiding this comment.
Nope, __mulmod was its only caller, which is now using __urem512by256 instead.
There was a problem hiding this comment.
You mean _ulongrem? That is already removed.
There was a problem hiding this comment.
Were these functions generated using some high level PL ? Perhaps, instead of keeping this ll file, we could describe algos in Rust and then convert to ll file as a part of the cargo build
Also can you please add comment with pseudo-code for each new function ?
There was a problem hiding this comment.
Were these functions generated using some high level PL?
The preexisting code here was adopted from https://github.com/matter-labs/era-compiler-llvm/blob/v1.4.0/llvm/lib/Target/EraVM/eravm-stdlib.ll. So the ll changes here are handwritten (implemented and scrutinized by Claude Fable 5). It was reported along with this issue that revm uses a handwritten mulmod using Knuth Algorithm as well. What Fable has referenced precisely is "Knuth TAOCP §4.3.1 Algorithm D with 128-bit digits. The 128/64 step is Hacker's Delight §9-4 divlu". (Fable also validated the functions using millions of fuzz vectors against big-integer oracles plus our integration tests.)
Perhaps, instead of keeping this ll file, we could describe algos in Rust and then convert to ll file as a part of the cargo build
I'm not entirely sure of the feasibility of that. Perhaps having Rust emit the IR via inkwell would also be an alternative (still need to build the IR lines one-by-one though). May be worth exploring the benefits.
Also can you please add comment with pseudo-code for each new function ?
Sure!
There was a problem hiding this comment.
Update: The function comments have been updated.
There was a problem hiding this comment.
The preexisting code here was adopted from https://github.com/matter-labs/era-compiler-llvm/blob/v1.4.0/llvm/lib/Target/EraVM/eravm-stdlib.ll. So the ll changes here are handwritten (implemented and scrutinized by Claude Fable 5). It was reported along with this issue that revm uses a handwritten mulmod using Knuth Algorithm as well. What Fable has referenced precisely is "Knuth TAOCP §4.3.1 Algorithm D with 128-bit digits. The 128/64 step is Hacker's Delight §9-4 divlu". (Fable also validated the functions using millions of fuzz vectors against big-integer oracles plus our integration tests.)
Got it, thanks. Assuming it's taking from Matter labs and also validated, I don't need to read it
I'm not entirely sure of the feasibility of that. Perhaps having Rust emit the IR via inkwell would also be an alternative (still need to build the IR lines one-by-one though). May be worth exploring the benefits.
I meant to do something like that, i.e. describe the algorithm in Rust and compile it to LLVM IR. In this case it will be way easier to understand the code.
There was a problem hiding this comment.
Just to clarify, the preexisting ll was from Matter Labs. The new/added implementation is Fable 5 generated and reviewed as described.
The Rust algorithm approach seems interesting. Yeah I agree it's not the most ideal review-wise with LLVM IR diffs. The algorithms rely on i256/i384/i512 SSA values though.
Divisions and modular multiplications whose divisor or modulus is a compile-time constant no longer perform any runtime division: they multiply by a precomputed reciprocal with a bounded number of conditional corrections (Barrett reduction). * `lower_wide_division` gains a second rung: an i256 `udiv`/`urem` by a non-power-of-two constant is rewritten into a generated per-constant helper (`constant_division.rs`) with floor(2^256/C) baked in and exactly one correction, ~1.5x faster per call than the routed long-division routine. * Constant-modulus mulmod is rewritten to the new stdlib routine `__mulmod_barrett` (HAC 14.42 at t = 256, two corrections, no operand pre-reduction needed) for any 256-bit non-power-of-two modulus, which covers field primes like P-256 and secp256k1; power-of-two moduli become an inline mul+mask. The rewrite runs BEFORE the main pipeline (`specialize_constant_modulus_mulmod`, behind a mem2reg/sccp/instcombine pre-pass that folds solc's NOT-form constants), because the pipeline inlines `__mulmod` into callers and no rewritable call site survives it; `lower_wide_division` repeats the sweep afterwards for moduli that only become constant during optimization. The hook is skipped at -O0 and for modules without `__mulmod` calls, leaving other contracts untouched.
Adds `make coverage` (cargo-llvm-cov over the full workspace including revive-llvm-builder), a new "Code coverage" book chapter that briefly explains how to run it, and a PR workflow that runs coverage and comments on the PR (it updates the same comment if it already exists) when the `measure code coverage` label is applied (see e.g. [this comment](#529 (comment))). Building instrumented LLVM and generating an LLVM C++ coverage report is also possible, see accompanying [PR description #575](#575) for more details. --------- Signed-off-by: Cyrill Leutwiler <[email protected]> Signed-off-by: xermicus <[email protected]> Co-authored-by: Cyrill Leutwiler <[email protected]> Co-authored-by: xermicus <[email protected]> Co-authored-by: elle-j <[email protected]> Co-authored-by: LJ <[email protected]>
The branch-free body is one ~700-instruction polkavm basic block. The O1+ inliner concatenated one copy per call site, exceeding pallet-revive's instruction limit. `noinline` caps every block at the single-body size.
Description
Any 256-bit
div/sdiv/mod/smod/mulmod/addmodwhose operands are not provably narrow compiles to a bit-at-a-time shift/subtract loop (one quotient bit per iteration) with no hardware divide. LLVM'sExpandLargeDivRemexpands any integer div/rem wider than 128 bits into that loop before instruction selection.An existing pass already spares the operands it can prove fit a smaller type, so the gap is everything else (e.g. runtime values,
mulmodinternals).__mulmodalone is 8,480 bytes / 2,923 instructions with zerodivu.The issue was detected with a contract using
mulmodin resolc-compiler-tests PR17 (which further explains runtime implications).Note
This PR explores the feasibility/benefits/effects of algorithms that avoid the expensive bit-loop lowering. The
word-width-microbenchworkload added in the resolc-compiler-tests PR ☝️ chainsmulmodwith the constant P-256 field prime, which additionally motivates the constant-modulus specialization explored here.Changes
The target already has an efficient wide-divide primitive: an i128 divide lowers to the linked compiler-rt libcall
__udivti3, which uses hardware 64-bitdivu. The fix in this PR expresses 256-bit division with 128-bit "digits" so its core divide stays at i128 and never triggers the bit loop. On top of that, divisions and modular multiplications by compile-time constants skip runtime division entirely via Barrett reduction (multiply by a precomputed reciprocal plus a bounded number of conditional corrections).stdlib.ll:__udivrem256(Knuth Algorithm D, 128-bit digits)__udiv256/__urem256wrappers__sdiv256/__srem256signed wrappers (sign-magnitude over the unsigned routine)__urem512by256formulmod's 512-by-256 reduction__mulmod_barrett(constant-modulus Barrett reduction, HAC 14.42 at t = 256; needs no operand pre-reduction since its bound holds for any 512-bit product)__mulmodto use them__ulongrem/__clz__clzremoved in favor ofllvm.ctlzllvm-context:lower_wide_division: any i256udiv/urem/sdiv/sremit cannot prove narrowable is rewritten into a call to the matching__udiv256/__urem256/__sdiv256/__srem256.udiv/uremby a non-power-of-two constant is instead rewritten into a generated per-constant Barrett helper (constant_division.rs), measurably faster per call than the routed routine.ExpandLargeDivRem) so it coversdiv/mod/sdiv/smodreductions.specialize_constant_modulus_mulmod__mulmodaway, a small constant-folding pre-pass exposes composed moduli and eligible calls are rewritten (256-bit constants to__mulmod_barrett, powers of two to inline mul+mask);lower_wide_divisionrepeats the sweep post-pipeline. Skipped at-O0and for modules without__mulmodcalls.Results
mulmodexampleThe table below is based only on the following contract example (Yul pipeline with
-Oz, the default):.L__mulmodsymbol__udivti3Instruction counts are the
Instructionstotals reported in the pvmasm dump (final code after--gc-sections).Dynamic cost
The table shows dynamic cost, measured (by Claude Fable 5 via
revive-runner) as pallet-revive ref_time (of an entire contract call) per call on the same runner (full-width operands, runtime divisors unless marked constant).Fixed per-call overhead is included, it is not the ref_time of the division instruction sequence alone. Thus the table's ratios will understate the speedup of the division itself. (Barrett reduction added in commit 6b87577.)