Skip to content

btcec: reduce allocations#2559

Open
allocz wants to merge 7 commits into
btcsuite:masterfrom
allocz:btcec_reduce_allocations
Open

btcec: reduce allocations#2559
allocz wants to merge 7 commits into
btcsuite:masterfrom
allocz:btcec_reduce_allocations

Conversation

@allocz

@allocz allocz commented Jun 30, 2026

Copy link
Copy Markdown

Change Description

I've optimized public key parsing, signature parsing and signature verification
for both ECDSA and Schnorr, the following are the improvements in the benchmarks
before and after.

Obs: using the following patches for both benchmarks:

golang/go#79708: big.Int.ModInverse allocation zeroing.
decred/dcrd#3714: Public Key parsing and ModNScalar
modulo inverse allocation zeroing.

To avoid forgetting to update the dependencies in the future, even after approval, I think we should wait for upstream releases of both Go and dcrec/secp256k1 containing the patches mentioned above, so that when this code is merged, the benchmark report the exact same results in respect of allocation count.

goos: linux
goarch: amd64
pkg: github.com/btcsuite/btcd/btcec/v2
cpu: Intel(R) Core(TM) i5-10310U CPU @ 1.70GHz
                          │ /tmp/BeforeOpts │          /tmp/WithAllOpts          │
                          │     sec/op      │   sec/op     vs base               │
ParseCompressedPubKey-8         17.40µ ± 0%   17.33µ ± 0%  -0.35% (p=0.000 n=60)
ParseUncompressedPubKey-8       362.4n ± 1%   327.4n ± 0%  -9.66% (n=60)
geomean                         2.511µ        2.382µ       -5.12%

                          │ /tmp/BeforeOpts │        /tmp/WithAllOpts        │
                          │      B/op       │   B/op     vs base             │
ParseCompressedPubKey-8          80.00 ± 0%   0.00 ± 0%  -100.00% (n=60)
ParseUncompressedPubKey-8        80.00 ± 0%   0.00 ± 0%  -100.00% (n=60)
geomean                          80.00                   ?               ¹ ²
¹ summaries must be >0 to compute geomean
² ratios must be >0 to compute geomean

                          │ /tmp/BeforeOpts │        /tmp/WithAllOpts         │
                          │    allocs/op    │ allocs/op   vs base             │
ParseCompressedPubKey-8          1.000 ± 0%   0.000 ± 0%  -100.00% (n=60)
ParseUncompressedPubKey-8        1.000 ± 0%   0.000 ± 0%  -100.00% (n=60)
geomean                          1.000                    ?               ¹ ²
¹ summaries must be >0 to compute geomean
² ratios must be >0 to compute geomean

pkg: github.com/btcsuite/btcd/btcec/v2/ecdsa
                    │ /tmp/BeforeOpts │          /tmp/WithAllOpts           │
                    │     sec/op      │   sec/op     vs base                │
SigVerify-8               215.9µ ± 0%   215.9µ ± 0%        ~ (p=0.281 n=60)
ParseDERSignature-8      109.15n ± 1%   76.74n ± 0%  -29.69% (n=60)
ParseBERSignature-8      107.35n ± 1%   75.61n ± 0%  -29.57% (n=60)
geomean                   1.363µ        1.078µ       -20.88%

                    │ /tmp/BeforeOpts │            /tmp/WithAllOpts             │
                    │      B/op       │    B/op     vs base                     │
SigVerify-8              0.000 ± 0%     0.000 ± 0%         ~ (p=1.000 n=60) ¹
ParseDERSignature-8      64.00 ± 0%      0.00 ± 0%  -100.00% (n=60)
ParseBERSignature-8      64.00 ± 0%      0.00 ± 0%  -100.00% (n=60)
geomean                             ²               ?                       ² ³
¹ all samples are equal
² summaries must be >0 to compute geomean
³ ratios must be >0 to compute geomean

                    │ /tmp/BeforeOpts │            /tmp/WithAllOpts             │
                    │    allocs/op    │ allocs/op   vs base                     │
SigVerify-8              0.000 ± 0%     0.000 ± 0%         ~ (p=1.000 n=60) ¹
ParseDERSignature-8      1.000 ± 0%     0.000 ± 0%  -100.00% (n=60)
ParseBERSignature-8      1.000 ± 0%     0.000 ± 0%  -100.00% (n=60)
geomean                             ²               ?                       ² ³
¹ all samples are equal
² summaries must be >0 to compute geomean
³ ratios must be >0 to compute geomean

pkg: github.com/btcsuite/btcd/btcec/v2/schnorr
                 │ /tmp/BeforeOpts │          /tmp/WithAllOpts           │
                 │     sec/op      │   sec/op     vs base                │
SigVerify-8            252.1µ ± 0%   251.7µ ± 0%   -0.17% (p=0.000 n=60)
ParsePubKey-8          17.43µ ± 0%   17.37µ ± 0%   -0.34% (p=0.000 n=60)
ParseSignature-8      129.35n ± 1%   86.78n ± 0%  -32.91% (n=60)
geomean                8.284µ        7.240µ       -12.60%

                 │ /tmp/BeforeOpts │        /tmp/WithAllOpts        │
                 │      B/op       │   B/op     vs base             │
SigVerify-8             240.0 ± 0%    0.0 ± 0%  -100.00% (n=60)
ParsePubKey-8           80.00 ± 0%   0.00 ± 0%  -100.00% (n=60)
ParseSignature-8        80.00 ± 0%   0.00 ± 0%  -100.00% (n=60)
geomean                 115.4                   ?               ¹ ²
¹ summaries must be >0 to compute geomean
² ratios must be >0 to compute geomean

                 │ /tmp/BeforeOpts │        /tmp/WithAllOpts         │
                 │    allocs/op    │ allocs/op   vs base             │
SigVerify-8             5.000 ± 0%   0.000 ± 0%  -100.00% (n=60)
ParsePubKey-8           1.000 ± 0%   0.000 ± 0%  -100.00% (n=60)
ParseSignature-8        1.000 ± 0%   0.000 ± 0%  -100.00% (n=60)
geomean                 1.710                    ?               ¹ ²
¹ summaries must be >0 to compute geomean
² ratios must be >0 to compute geomean

In summary:

Schnorr signature parsing time: ~-33%
ECDSA signature parsing time: ~-30%
Uncompressed public key parsing time: ~-9.5%

But all the procedures in the benchmarks above are not doing allocations
anymore, reducing the frequency between garbage collection cycles that must scan
all the reachable heap (that is considerably large in production).

Now we have more CPU available to perform block verification.

Steps to Test

Steps for reviewers to follow to test the change.

Pull Request Checklist

Testing

  • Your PR passes all CI checks.
  • Tests covering the positive and negative (error paths) are included.
  • Bug fixes contain tests triggering the bug to prevent regressions.

Code Style and Documentation

📝 Please see our Contribution Guidelines for further guidance.

@allocz

allocz commented Jun 30, 2026

Copy link
Copy Markdown
Author

CC @kcalvinalvin

@Roasbeef

Copy link
Copy Markdown
Member

Approved CI run.

Comment thread btcec/pubkey.go
Comment thread btcec/schnorr/pubkey.go
Comment thread btcec/ecdsa/signature.go Outdated
@allocz allocz force-pushed the btcec_reduce_allocations branch from 2beacf8 to 1f3d515 Compare July 4, 2026 12:43
allocz added 7 commits July 4, 2026 12:43
To optimize critical procedures used in validation loop, we first need
all of them covered by benchmarks, so that we can have a baseline for
future optimizations.

btcec added benchmarks:
* BenchmarkParseUncompressedPubkey

btcec/ecdsa added benchmarks:
* BenchmarkParseDERSignature
* BenchmarkParseBERSignature

btcec/schnorr added benchmarks:
* BenchmarkParsePubKey
* BenchmarkParseSignature

btcec/ecdsa benchmark improvements:
* Avoid unneccessary allocations on BenchmarkSigVerify.
* Avoid unneccessary allocations on BenchmarkParseCompressedPubKey.
Previously, changing DER boolean argument of parseSig was breaking
VerifyLowS behavior without breaking the tests. New sentinel errors and
entries to the VerifyLowS test table were added, effectively catching
regressions in the implementation.
Added unexported function that does not return a pointer to the public
key to execute the key parsing, letting the exported ParsePubKey short
and simple, making it eligible to inlining and therefore zeroing out
allocations.
Added unexported function that does not return a pointer to the public
key to execute the key parsing, letting the exported ParsePubKey short
and simple, making it eligible to inlining and therefore zeroing out
allocations.
btcec/schnorr: replace usage of SerializePubKey for serializePubKey32,
which writes to a byte slice instead of allocating and returning a new
one.

chainhash: keep TaggedHash short and simple to make it eligible to
inlining and avoid allocations.
Make ParseSignature and ParseDERSignature short and simple, making the
procedures eligible to inlining and therefore avoiding allocations.
Make ParseSignature short and simple, becoming eligible to inlining
which avoids allocations.
@allocz allocz force-pushed the btcec_reduce_allocations branch from 1f3d515 to ac92fde Compare July 4, 2026 12:44
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.

3 participants