Skip to content

Optimize HexEncoder.DecodeData methods for .NET 8+#1317

Merged
NicolasDorier merged 6 commits into
MetacoSA:masterfrom
Carti-it:optimize-hex-computations
Jun 16, 2026
Merged

Optimize HexEncoder.DecodeData methods for .NET 8+#1317
NicolasDorier merged 6 commits into
MetacoSA:masterfrom
Carti-it:optimize-hex-computations

Conversation

@Carti-it

@Carti-it Carti-it commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Benchmark

dotnet run -c Release -- --runtimes net8.0 --filter *HexBench*

master:

Method Mean Error StdDev Gen0 Allocated
DecodeData 43.36 ns 0.863 ns 0.886 ns 0.0089 56 B
DecodeDataSpan 40.19 ns 0.818 ns 1.064 ns - -
EncodeData 46.64 ns 0.955 ns 1.370 ns 0.0485 304 B

PR (with git 2 commits):

Method Mean Error StdDev Gen0 Allocated
DecodeData 20.57 ns 0.440 ns 0.631 ns 0.0089 56 B
DecodeDataSpan 39.59 ns 0.808 ns 1.078 ns - -
EncodeData 45.65 ns 0.934 ns 1.215 ns 0.0485 304 B

PR (with git 3 commits):

Method Mean Error StdDev Gen0 Allocated
DecodeData 21.03 ns 0.443 ns 0.843 ns 0.0089 56 B
DecodeDataSpan 22.95 ns 0.487 ns 0.814 ns 0.0089 56 B
EncodeData 46.27 ns 0.944 ns 1.469 ns 0.0485 304 B

Decoding is about twice as fast with the PR compared to master.

using System.Threading.Tasks;

namespace NBitcoin.Bench
namespace NBitcoin.Bench;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread NBitcoin.Bench/HexBench.cs Outdated
[Benchmark]
public void DecodeDataSpan()
{
Span<byte> tmp = stackalloc byte[32];

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simulates

Span<byte> tmp = stackalloc byte[32];
Encoder.DecodeData(str, tmp);
from the uint256 constructor.

throw new ArgumentException("output should be bigger", nameof(output));

#if NET8_0_OR_GREATER
var decoded = Convert.FromHexString(encoded);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though this allocates a byte array, it's still faster to use Convert.FromHexString because it uses SIMD instructions.

@Carti-it Carti-it Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An alternative would be to target NET 10 TFM (along with NET 8.0) by NBitcoin and just use Convert.FromHexString(string source, Span<byte> destination). I'm not sure if it's worth it and how much work it would require (mostly updating a CI script and a csproj file I guess).

@Carti-it Carti-it changed the title Optimize HexEncoder.DecodeData methods Optimize HexEncoder.DecodeData methods for .NET 8+ Jun 14, 2026
@NicolasDorier

Copy link
Copy Markdown
Collaborator

@Carti-it hey, the bench is on 32 bytes. Can you try with bigger side? I fear that it will have a big impact on GC.

@Carti-it Carti-it force-pushed the optimize-hex-computations branch from fac028a to 49d403e Compare June 15, 2026 06:21
@Carti-it

Copy link
Copy Markdown
Contributor Author

@Carti-it hey, the bench is on 32 bytes. Can you try with bigger side? I fear that it will have a big impact on GC.

I added 49d403e and the results are:

Method hexString Mean Error StdDev Gen0 Allocated
DecodeData ? 20.36 ns 0.430 ns 0.718 ns 0.0089 56 B
EncodeData ? 48.92 ns 0.896 ns 0.699 ns 0.0485 304 B
DecodeDataSpan 00000(...)fffff [64] 23.62 ns 0.492 ns 0.923 ns 0.0089 56 B
DecodeDataSpan 0000(...)ffff [128] 23.39 ns 0.494 ns 1.183 ns 0.0089 56 B
DecodeDataSpan 0000(...)ffff [192] 23.68 ns 0.498 ns 0.682 ns 0.0089 56 B

So it does not appear to be worse with longer strings. stackalloc is for short pieces of data anyway.

Moreover, DecodeData(string encoded, Span<byte> output) is used only in uint256 in NBitcoin where 32 bytes is the standard.

It is true that HexEncoder.DecodeData(string encoded, Span<byte> output) is public but people on .NET10+ can use even faster API.

@NicolasDorier

NicolasDorier commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

@Carti-it 96 bytes is still small, I am thinking about 10 KB for example (a typical large transaction)

How come DecodeData on a 96 bytes only allocate 56 bytes? There might be some dotnet dedups internally. Maybe a better idea would be to generate a random string, then compare the DecodeData before and after your change.

@Carti-it

Copy link
Copy Markdown
Contributor Author

master

Method hexString Mean Error StdDev Gen0 Allocated
DecodeData ? 51.62 ns 1.047 ns 1.778 ns 0.0089 56 B
DecodeDataLongSpan ? 13,465.71 ns 237.901 ns 210.893 ns - -
EncodeData ? 55.50 ns 1.124 ns 1.104 ns 0.0485 304 B
DecodeDataSpan 00000(...)fffff [64] 46.00 ns 0.734 ns 0.651 ns - -
DecodeDataSpan 0000(...)ffff [128] 87.42 ns 1.330 ns 1.111 ns - -
DecodeDataSpan 0000(...)ffff [192] 133.03 ns 1.941 ns 1.721 ns - -

PR

Method hexString Mean Error StdDev Gen0 Allocated
DecodeData ? 25.25 ns 0.534 ns 0.832 ns 0.0089 56 B
DecodeDataLongSpan ? 3,644.64 ns 69.845 ns 93.241 ns 1.6289 10264 B
EncodeData ? 56.18 ns 0.989 ns 1.418 ns 0.0485 304 B
DecodeDataSpan 00000(...)fffff [64] 29.30 ns 0.343 ns 0.304 ns 0.0089 56 B
DecodeDataSpan 0000(...)ffff [128] 39.83 ns 0.631 ns 0.560 ns 0.0140 88 B
DecodeDataSpan 0000(...)ffff [192] 53.62 ns 0.856 ns 0.759 ns 0.0191 120 B

How come DecodeData on a 96 bytes only allocate 56 bytes?

There was a bug in the bench code: 0293327

@NicolasDorier NicolasDorier merged commit a858a36 into MetacoSA:master Jun 16, 2026
6 checks passed
@NicolasDorier

Copy link
Copy Markdown
Collaborator

Released NBitcoin.Secp256k1/v4.0.1 and NBitcoin.10.0.7 with your latest changes.

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.

2 participants