Bundled multi-point multiplication
Summary
Below the Pippenger threshold, secp256k1_ecmult_multi_var currently uses Strauss' algorithm: a wNAF table of odd multiples for every point. This PR takes the points six at a time instead and gives each such bundle one table of subset sums. The main loop does the same number of additions, but the tables are cheaper to build and smaller, and the scalar recoding is close to free.
- 15% to 18% faster than
strauss_wnafonce there are three bundles, and 12% to 15% with one. A call that leaves five points over loses part of that: 7% to 10% at eleven points, the worst case (see Benchmarks) - about 58% of the scratch space per point: 846 bytes against 1452 on 64-bit
- the crossover with Pippenger's algorithm moves from 88 points to 181, so calls of 88 to 180 points gain up to 12% over what they get today
- zero scalars and points at infinity need no special handling in a bundle
The idea
Split every scalar with lambda, as now, into two halves; call the plain halves and the lambda halves the two sides. Take ECMULT_BUNDLE_SIZE = 6 points as a bundle. Its first point is the anchor, the other five are followers. On each side, recode the bundle's six halves together into ECMULT_BUNDLE_BITS = 130 positions so that at every position
- the anchor's digit is +1 or -1, never zero, and
- each follower's digit is either zero or equal to the anchor's.
At one position on one side the bundle then contributes
sign * (anchor + some subset of the followers)
which is an entry of a 32-entry table of subset sums, or its negation. A bundle index is one byte that names it: a sign bit over the 5-bit number of the table entry. The main loop runs 130 positions, and at each one spends two lookups and two additions per bundle, one per side. The lambda side uses the same table with x multiplied by beta.
Points that do not fill a bundle (at most five per call, see batches below) are handled as in Strauss' algorithm, exactly as today, in the same loop.
Terms
| term | meaning |
|---|---|
| bundle | ECMULT_BUNDLE_SIZE points served by one table |
| anchor, follower | a bundle's first point; each of the others |
| half, side | an output of scalar_split_lambda; the plain or the lambda halves taken together |
| position | one of the ECMULT_BUNDLE_BITS doublings of the main loop |
| bundle index | a byte per bundle per side per position: anchor's sign, then one table-entry bit per follower |
| digit | a scalar's signed digit at a position (+1, -1 or 0), as in the two conditions above |
| table | the 2^(SIZE-1) subset sums of a bundle that contain its anchor |
| chain | the sequence of additions that builds the tables, whose z ratios give one common z |
| points left over | those that do not fill a bundle; they are handled as in Strauss' algorithm |
Why it is faster
Per six points, against six points in Strauss' algorithm at WINDOW_A = 5:
| bundle | Strauss | |
|---|---|---|
| main loop additions | 260 | about 258 |
| table build | 31 additions, 32 normalizations | 42 additions, 6 doublings, 48 normalizations |
| recoding | 6 splits, a few word operations each | 6 splits, 12 wNAF recodings |
| tables and indices (or wNAF digits) | 4356 bytes | 7752 bytes |
Benchmarks
bench_ecmult-style runs of n points plus G, microseconds per point, lower is better. i7-12700H, one P-core, clock fixed at 2.3 GHz (turbo and hyper-threading off), process pinned at high priority. Each figure is the minimum over six alternating rounds of ten runs each; rounds agree to within 0.7%.
Against strauss_wnaf:
| points | gcc 16.1 -O2: Strauss |
bundle | change | clang 22.1 -O2: Strauss |
bundle | change |
|---|---|---|---|---|---|---|
| 6 | 16.68 | 14.59 | -12.5% | 18.32 | 15.65 | -14.6% |
| 11 | 16.13 | 14.96 | -7.2% | 17.81 | 16.12 | -9.5% |
| 12 | 16.07 | 13.80 | -14.1% | 17.76 | 14.83 | -16.5% |
| 17 | 15.81 | 14.24 | -10.0% | 17.52 | 15.37 | -12.3% |
| 18 | 15.80 | 13.50 | -14.6% | 17.48 | 14.50 | -17.0% |
| 24 | 15.67 | 13.31 | -15.1% | 17.31 | 14.36 | -17.1% |
| 48 | 15.47 | 13.08 | -15.4% | 17.07 | 14.12 | -17.3% |
| 96 | 15.36 | 12.96 | -15.6% | 16.89 | 13.97 | -17.3% |
| 144 | 15.33 | 12.88 | -16.0% | 16.81 | 13.91 | -17.2% |
| 180 | 15.34 | 12.88 | -16.0% | 16.80 | 13.88 | -17.4% |
Below six points the code path is unchanged and the two columns agree to 0.1%. The rows for 11 and 17 are the worst cases, one bundle or two with five points left over at Strauss' cost.
Against pippenger_wnaf, which is what secp256k1_ecmult_multi_var uses today from 88 points:
| points | gcc: Pippenger | bundle | change | clang: Pippenger | bundle | change |
|---|---|---|---|---|---|---|
| 96 | 14.62 | 12.96 | -11.4% | 15.91 | 13.97 | -12.2% |
| 120 | 14.01 | 12.92 | -7.8% | 15.22 | 13.93 | -8.5% |
| 144 | 13.62 | 12.88 | -5.4% | 14.84 | 13.91 | -6.2% |
| 168 | 13.12 | 12.89 | -1.8% | 14.30 | 13.89 | -2.9% |
| 180 | 12.89 | 12.88 | -0.0% | 14.06 | 13.88 | -1.3% |
| 192 | 12.71 | 12.87 | +1.3% | 13.85 | 13.86 | +0.1% |
| 204 | 12.56 | 12.85 | +2.3% | 13.69 | 13.84 | +1.1% |
The two are equal at about 180 points with gcc and a little past 190 with clang; sweeps at every third and sixth point, made when the threshold was chosen, put the crossovers at 181 and 194. ECMULT_PIPPENGER_THRESHOLD is set to 181, the lower of the two, which also gives Pippenger's algorithm some credit for the smaller scratch space it needs. This is one machine and two compilers; the value will need tuning on more hardware. For comparison, on this machine Strauss and Pippenger are equal at about 77 points (gcc) and 74 (clang), against the 88 in the source.
What is and is not new
The representation is the sign-aligned column form (GLV-SAC) of Faz-Hernandez, Longa and Sanchez (https://eprint.iacr.org/2013/158, section 3), also used by FourQ (https://eprint.iacr.org/2015/565). There the columns run across the sub-scalars of one multiplication; here they run across the points of a bundle, separately on each side of the lambda split.
Two things differ from those papers:
- The recoding is closed form. With
uthe anchor's half andfa follower's, both as signed integers, the follower's presence mask is~((f + ~u) ^ u): one addition and one XNOR per follower per side, where the papers run a loop over the positions. (Literature search for this form is still owed before claiming it.) - 130 positions rather than 129 are needed, because secp256k1's split halves reach 0.635 * 2^128 and an anchor's and a follower's together must stay below 2^129.
Commits
secp256k1_ge_z: an affine point together with the z of the isomorphic curve it is on, valid at infinity too. It is the accumulator for a chain of additions that must keep its z ratios across points at infinity, which asecp256k1_gejcannot do.- The bundle table, built in Gray code order: each entry is the previous one plus or minus one input point, so the table is one chain with one live accumulator. All bundles extend the same chain and reach a common z in one backward pass, with no inversion.
- The lookups, which define the bundle index.
secp256k1_hscalar: a 160-bit two's complement integer. Split halves are small signed integers stored as residues mod n; this is them as integers, so the recoding is plain word arithmetic.- The recoding.
- The main loop over whole bundles and G. Its comment carries the outline and the cost counts.
- Points left over handled as in Strauss' algorithm, in the same loop.
- The batch function over a scratch space, a
bundlemode forbench_ecmult, tests. secp256k1_ecmult_multi_varuses it where it used Strauss.- Batches are sized for bundles and made of whole bundles, so only a call's last batch has points left over. A scratch space too small for one bundle still gets Strauss' algorithm.
ECMULT_PIPPENGER_THRESHOLDgoes from 88 to 181.
Degenerate inputs
A subset sum can be the point at infinity even when no input is (a follower equal to the anchor's negation, or chosen inputs deeper in), so this is the first table in the library whose entries may be infinite. The chain passes through such entries with a ratio of 1, the entry is stored as an ordinary infinite secp256k1_ge, and the lookups carry the flag. The exhaustive tests run the whole algorithm over the small test groups (orders 7, 13 and 199), where a subset sum vanishes one time in the group order and so the tables do hold infinities.
Notes
Extensive use of Claude Code throughout, in particular producing this final reorganization of the branch.