Skip to content

⚡️ Cheaper expWad and lnWad via symmetric rationals - #1546

Open
ddallaire wants to merge 1 commit into
Vectorized:mainfrom
ddallaire:perf/expwad-lnwad-symmetry
Open

⚡️ Cheaper expWad and lnWad via symmetric rationals#1546
ddallaire wants to merge 1 commit into
Vectorized:mainfrom
ddallaire:perf/expwad-lnwad-symmetry

Conversation

@ddallaire

@ddallaire ddallaire commented Aug 11, 2026

Copy link
Copy Markdown

Description

Adds expWadFast and lnWadFast: cheaper symmetric-rational implementations with a
certified rounding direction.

function gas/call vs original wrapper bytes
expWad 415.6 579
expWadFast 339.3 −76.2 (−18.3%) 498 (−81)
lnWad 528.3 731
lnWadFast 489.7 −38.7 (−7.3%) 632 (−99)

Guarantees (proved, not sampled):

  • lnWadFast(x) always returns ⌊L⌋ or ⌊L⌋ − 1, where L = ln(x/1e18) * 1e18 is the
    exact result. It never overestimates. lnWadFast(1e18) = 0 exactly. Monotonically
    increasing.
  • expWadFast(x) never overestimates the exact E = exp(x/1e18) * 1e18, and returns more
    than E·(1 − 2.58e-22) − 1. For x ≤ 8265113944572514620 (results up to ~3885e18) that
    is ⌊E⌋ or ⌊E⌋ − 1; an absolute 1-wei bound for the full int256 codomain is not
    attainable in 256-bit arithmetic, so beyond that the bound is relative. expWadFast(0) = 1e18 exactly. Monotonically increasing, including across all 255 range-reduction seams.

A full writeup and validation/proof toolkit is available here: https://github.com/ddallaire/wad-exponentials

Checklist

Ensure you completed all of the steps below before submitting your pull request:

  • Ran forge fmt?
  • Ran forge test?

@ddallaire
ddallaire force-pushed the perf/expwad-lnwad-symmetry branch from bb4aec7 to 46fcb70 Compare August 12, 2026 18:23
@atarpara

atarpara commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

@ddallaire Thanks for this PR! @duncancmt and I worked on the same thing (lnWad) using clz a few months ago: #1537. I’ll review your approach as well.

I remember that at the time, we had an issue with the 18-decimal precision — we couldn’t get the true floor version of lnWad. @duncancmt then came up with a solution using ray precision.

@ddallaire

Copy link
Copy Markdown
Author

@atarpara I'll be waiting for comments 👍 Note that one CI test run is failing as the test outgrew the configured gas limit of 100M, please tell me if you want to bump the gas limit or no, I can always split the test contract on its own for labertW0Wad.

@duncancmt

Copy link
Copy Markdown
Contributor

My PR (and un-PR'd complementary work on exp in the same repo as referenced by the ln PR) are somewhat orthogonal to @ddallaire 's work here. @ddallaire 's work targets lambertW0 while mine doesn't bother. My work extends ln and exp to work in Ray precision (rather than Wad) because these values are almost always rates or rate-like; by Maker convention, these should be Ray, not Wad. As a consequence of the larger fixnum format, my implementation is less gas efficient.

Independent of the fixnum format, my implementation carefully quantifies the error in the approximation and compensates for/bounds it. Fundamentally, because ln and exp are both[0] transcendental, due to the table-maker's dilemma, without significant knowledge of or constraint on the domain, it's not possible to produce a exactly-computed correctly-rounded result in bounded computation (gas). Therefore, my implementation does the best that is possible in bounded computation: at most 1 wei of error and only error in one direction.

@ddallaire , does your implementation have this rounding behavior? If not, that would be a nice improvement. I'm happy to work with you on that and try to formally verify that property using the same stack that I did with my implementation.


[0] Although sqrt and cbrt are transcendental, because their inverses are not, it is possible to produce an exactly-computed correctly-rounded result in bounded computation by running the inverse (squaring or cubing) and comparing the result.

@ddallaire

Copy link
Copy Markdown
Author

@duncancmt Thanks for the context on previous work. The goal of this implementation was to optimize the gas while being equally or more accurate than the previous version. If the current efforts are towards accuracy, you can disregard this PR. Error stats for these, feel free to validate:

  • lnWad: error in [−1.04, +0.04] wei. ~99% of inputs land below the true value (the final wad conversion is a floor), ~1% dip just past −1 wei, and a small tail lands up to +0.04 wei above.
  • expWad x > 0: relative error in [−2.4e−18, +1.7e−18], both directions, so the wei error scales with the result, as it must for a relative fit.
  • expWad x ≤ 0: absolute error in [−1.9, +1.1] wei.

The 6,6 version from my repo which also cuts down on gas reduces the error by a lot.

@duncancmt

Copy link
Copy Markdown
Contributor

Well, whether or not the stated error is adequate is a bit putting the cart before the horse. I think the major question is whether to keep the existing expWad/lnWad functions or whether to go to Ray-basis (at the expense of gas) while keeping Wad-basis wrappers around for backwards-compatibility. Answering that question rests entirely with @atarpara .

Independent of the answer to that question, I think it's very likely that your implementation could be coaxed into having floor-or-1-wei-less behavior with a bit of tweaking to the constants and a subtraction. My professional opinion is that having one-sided rounding error makes the design of systems with safe/conservative/optionality-adverse rounding behavior considerably easier, but this too could be a point for discussion.

Certainly either implementation would be an improvement on the current one! 👍

@ddallaire

Copy link
Copy Markdown
Author

I'm not going to chime in on the wad vs ray discussion as I have no horse in this.

If the decision ends up going towards supporting both, I'd be happy to tweak both exp and ln function to make the error one sided as my PR doesn't supported that as is, my (6,6) exp implementation can be one-sided by removing a small bias, at the cost of 8 gas over the (5,5), unsure about the ln function as it's centered on lnWad(1e18) == 0

Ref:
https://github.com/ddallaire/wad-exponentials/blob/d1056df258aaea6ac4b0ed7dfdf740b9b311d691/src/ExpWad.sol#L93

@atarpara

Copy link
Copy Markdown
Collaborator

@ddallaire I agree with the design direction suggested by @duncancmt , but I don't think Solady needs to move to Ray precision here.

I'd prefer to keep the existing Wad/18-decimal basis and optimize around that. For Solady, the important property isn't higher internal precision, but rather that the approximation error is bounded and, more importantly, that the rounding direction is guaranteed.

So I'd be happy with a solution that stays at Wad precision as long as we can guarantee that the result is always rounded in one direction (e.g. true floor for lnWad), with the error bounded to at most 1 wei.

I think that property is more important for Solady than squeezing out additional precision, especially since it keeps the behavior predictable while keeping the implementation simple and efficient.

And yes, I'd definitely like to formally verify that rounding-direction guarantee if possible. Maybe @duncancmt can help us with that.

@duncancmt

Copy link
Copy Markdown
Contributor

You can check my full implementation (again, at Ray-scale, but the general techniques may be applicable) in the branch in this PR 0xProject/0x-settler#599 . This is part of a larger PR stack, so I recommend pulling just the branch involved and examining the code/proof.

I'm surprised that the 6-coeff version of exp is required to get the desired one-sided rounding error. Perhaps the technique from my branch that involves doing a raw search over the low bits of the coeffs can be applied here? You may also find it possible to obtain smaller code size by adopting the mixed fixed-point Horner technique and sharing the constant coefficient between both polynomials (applicable to both ln and exp).

@Vectorized

Vectorized commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Let’s support both.

Just postfix the faster functions with “Fast”, while keeping the original functions in case some applications require the original precision.

FixedPointMathLib is like a museum on the different numerical techniques.

lambertW0Wad is really niche, so we can leave it using the original lnWad and expWad.

In the comments, also give a link to the 0x-settler implementations in case viewers want to use the ray versions.

@ddallaire
ddallaire force-pushed the perf/expwad-lnwad-symmetry branch from 46fcb70 to 3cba99a Compare August 18, 2026 14:04
@ddallaire

Copy link
Copy Markdown
Author

Let’s support both.

Just postfix the faster functions with “Fast”, while keeping the original functions in case some applications require the original precision.

FixedPointMathLib is like a museum on the different numerical techniques.

lambertW0Wad is really niche, so we can leave it using the original lnWad and expWad.

In the comments, also give a link to the 0x-settler implementations in case viewers want to use the ray versions.

Reverted every change from current functions, added the "fast" versions. Ensured that both functions are monotonically increasing, and that they both floor the result with the error bounded to at most 1 wei, while also preserving:

expWadFast(0) = 1e18
lnWadFast(1e18) = 0

Created a proof/test suite using z3 and everything I could think of, feel free to validate using Lean as I'm not familiar with that.

I'll let @duncancmt reference his ray implementation in comments as I was unsure what the best links were for that.

@github-actions

Copy link
Copy Markdown

Gas Snapshot Comparison Report

Generated at commit : 3cba99a, Compared to commit : cedd793

Contract Name Test Name Main Gas PR Gas Diff
FixedPointMathLibTest testAbs() 698 676 -22
testAbsEdgeCases() 490 513 23
testAvg() 462 440 -22
testAvgEdgeCase() 445 468 23
testAvgSigned() 916 894 -22
testCbrt() 10279 10324 45
testCbrtWad() 12049 12027 -22
testCbrtWadConverged() 2699 2786 87
testCbrtWadDebug() 7369 7325 -44
testCbrtWadMonotonicallyIncreasing() 4209 4121 -88
testDist() 675 697 22
testDivWad() 731 687 -44
testDivWadEdgeCases() 399 444 45
testDivWadUp() 2949 2972 23
testDivWadUpEdgeCases() 446 469 23
testDivWadZeroDenominatorReverts() 3948 3970 22
testExpWad() 8033 7989 -44
testFactorial() 97524 97569 45
testFullMulDiv() 1099 1143 44
testFullMulDivN() 890 846 -44
testFullMulDivUnchecked() 1622 1600 -22
testFullMulDivUpRevertsIfRoundedUpResultOverflowsCase1() 4450 4495 45
testFullMulDivUpRevertsIfRoundedUpResultOverflowsCase2() 4494 4472 -22
testGcd() 4223 4244 21
testInvMod() 12885 12886 1
testLambertW0WadMonoDebug() 769090 769068 -22
testLambertW0WadMonotonicallyIncreasing() 18356886 18356887 1
testLambertW0WadMonotonicallyIncreasing2() 4030663 4030686 23
testLambertW0WadRevertsForOutOfDomain() 57923 58153 230
testLambertW0WadWithinBounds() 122404 122360 -44
testLerpInt() 6410 6455 45
testLerpUint() 6413 6369 -44
testLnWad() 2071 2049 -22
testLnWadBig() 2084 2062 -22
testLnWadNegativeReverts() 6231 6165 -66
testLnWadOverflowReverts() 4037 4014 -23
testLnWadSmall() 2703 2681 -22
testLog2() 265326 265304 -22
testLog256() 22804 22848 44
testLog256Up() 1286 1287 1
testLog2Up() 318958 318981 23
testMulDivUp() 2125 2146 21
testMulDivUpZeroDenominator() 4041 4040 -1
testMulDivZeroDenominatorReverts() 4018 4082 64
testMulSqrt() 285751 288722 2971
testMulWad() 704 682 -22
testMulWadEdgeCases() 695 717 22
testMulWadUp() 778 801 23
testMulWadUpEdgeCases() 768 791 23
testPackUnpackSci() 129024 129069 45
testRPowOverflowReverts() 8229 8119 -110
testSDivWad() 858 880 22
testSDivWadEdgeCases() 401 446 45
testSMulWad() 1028 1006 -22
testSMulWadEdgeCases() 1275 1298 23
testSaturatingAdd() 1491 1514 23
testSaturatingMul() 1599 1600 1
testSci() 1828180 1828136 -44
testSqrt() 45390 45435 45
testSqrtHashedSingle() 54630 54608 -22
testSqrtWad() 7914 7958 44
test__codesize() 60859 72338 11479
testExpWadFast() - 26475 -
testExpWadFastGas() - 7618 -
testExpWadFastOverflowReverts() - 8386 -
testExpWadFastSeamsMonotonic() - 2619037 -
testExpWadGas() - 8881 -
testLnWadFast() - 28118 -
testLnWadFastGas() - 5963 -
testLnWadFastNegativeReverts() - 8559 -
testLnWadFastOctaveSeamsMonotonic() - 286311 -
testLnWadGas() - 6343 -
FixedPointMathLibWithCLZTest testAbs() 698 676 -22
testAbsEdgeCases() 490 513 23
testAvg() 462 440 -22
testAvgEdgeCase() 445 468 23
testAvgSigned() 916 894 -22
testCbrt() 6222 6267 45
testCbrtWad() 8231 8209 -22
testCbrtWadConverged() 2449 2536 87
testCbrtWadDebug() 7119 7075 -44
testCbrtWadMonotonicallyIncreasing() 3486 3398 -88
testDist() 675 697 22
testDivWad() 731 687 -44
testDivWadEdgeCases() 399 444 45
testDivWadUp() 2949 2972 23
testDivWadUpEdgeCases() 446 469 23
testDivWadZeroDenominatorReverts() 3948 3970 22
testExpWad() 8033 7989 -44
testFactorial() 97524 97569 45
testFullMulDiv() 1099 1143 44
testFullMulDivN() 890 846 -44
testFullMulDivUnchecked() 1622 1600 -22
testFullMulDivUpRevertsIfRoundedUpResultOverflowsCase1() 4450 4495 45
testFullMulDivUpRevertsIfRoundedUpResultOverflowsCase2() 4494 4472 -22
testGcd() 4223 4244 21
testInvMod() 12885 12886 1
testLambertW0WadMonoDebug() 700861 700839 -22
testLambertW0WadMonotonicallyIncreasing() 17296838 17296839 1
testLambertW0WadMonotonicallyIncreasing2() 3731565 3731588 23
testLambertW0WadRevertsForOutOfDomain() 57923 58153 230
testLambertW0WadWithinBounds() 111382 111338 -44
testLerpInt() 6410 6455 45
testLerpUint() 6413 6369 -44
testLnWad() 1648 1626 -22
testLnWadBig() 1661 1639 -22
testLnWadNegativeReverts() 5989 5923 -66
testLnWadOverflowReverts() 3916 3893 -23
testLnWadSmall() 2139 2117 -22
testLog2() 136107 136085 -22
testLog256() 12553 12597 44
testLog256Up() 1858 1859 1
testLog2Up() 159794 159817 23
testMulDivUp() 2125 2146 21
testMulDivUpZeroDenominator() 4041 4040 -1
testMulDivZeroDenominatorReverts() 4018 4082 64
testMulSqrt() 252951 255922 2971
testMulWad() 704 682 -22
testMulWadEdgeCases() 695 717 22
testMulWadUp() 778 801 23
testMulWadUpEdgeCases() 768 791 23
testPackUnpackSci() 129024 129069 45
testRPowOverflowReverts() 8229 8119 -110
testSDivWad() 858 880 22
testSDivWadEdgeCases() 401 446 45
testSMulWad() 1028 1006 -22
testSMulWadEdgeCases() 1275 1298 23
testSaturatingAdd() 1491 1514 23
testSaturatingMul() 1599 1600 1
testSci() 1824283 1824239 -44
testSqrt() 31560 31605 45
testSqrtHashedSingle() 38120 38098 -22
testSqrtWad() 5813 5857 44
test__codesize() 62814 74030 11216
testExpWadFast() - 26475 -
testExpWadFastGas() - 7618 -
testExpWadFastOverflowReverts() - 8386 -
testExpWadFastSeamsMonotonic() - 2619037 -
testExpWadGas() - 8881 -
testLnWadFast() - 23708 -
testLnWadFastGas() - 4493 -
testLnWadFastNegativeReverts() - 8196 -
testLnWadFastOctaveSeamsMonotonic() - 211635 -
testLnWadGas() - 4933 -

@duncancmt

Copy link
Copy Markdown
Contributor

Please reference:

https://github.com/0xProject/0x-settler/blob/ea7f9721d3bc2cd7be849ec0ba46b0de3a403e1a/src/vendor/Exp.sol
https://github.com/0xProject/0x-settler/blob/ea7f9721d3bc2cd7be849ec0ba46b0de3a403e1a/src/vendor/Ln.sol

I owe you a code review on this PR, but I am a bit busy with the bills-paying work for the moment.


My Lean proofs use largely the same techniques as your Z3 proofs. I think the TCB on my proofs is a bit smaller, but nothing immediately jumps out at my as "this proof is wrong". I'll review the proofs after I review the implementation after I get done with paying my bills 😅

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.

4 participants