I sometimes find I need to pass around a data in a URL. Doing this succinctly and/or securely may mean I need to hash or encrypt the data to pass around a much shorter value. Hashing and encryption in turn each operate on binary data, which means I first have to convert my data into bytes. Binary data can’t be included in a URL resulting in another conversion, typically to encode the bytes back into a string-based format such as Base64.
All of these operations have an overhead. .NET provides each of the different building blocks for these operations but there’s no one-shot method to do it all. Further, while each of the operations the .NET base class library (BCL) are highly optimized, there is inevitably some performance lost if I were to use some code like this:
static string HashToBase64String(Encoding encoding, string input){ byte[] bytes = encoding.GetBytes(input); byte[] hash = SHA256.HashData(bytes); return Base64Url.EncodeToString(hash);}
The biggest performance culprit is the call to GetBytes(). It must allocate an entire new array just to be able to immediately then transform that data again and discard the original bytes. This can add up, especially if data is a rather large string. It would be nice if we could pool those arrays, or better yet to avoid it altogether for smaller strings.
As a result, I have a helper snippet I find I often resort to and modify as cases come up which outperforms the above implementation in both speed and memory consumption when run through benchmarks of varying sized strings.
static string HashToBase64String(Encoding encoding, string input){ const int StackAllocThreshold = 256; int maxByteCount = encoding.GetMaxByteCount(input.Length); byte[]? pooledBuffer = null; Span<byte> buffer = maxByteCount <= StackAllocThreshold ? stackalloc byte[StackAllocThreshold] : (pooledBuffer = ArrayPool<byte>.Shared.Rent(maxByteCount)); try { int byteCount = encoding.GetBytes(input, buffer); Span<byte> hash = stackalloc byte[SHA256.HashSizeInBytes]; SHA256.HashData(buffer[..byteCount], hash); return Base64Url.EncodeToString(hash); } finally { if (pooledBuffer is not null) { ArrayPool<byte>.Shared.Return(pooledBuffer); } }}
This takes advantage of a few things:
- Prefer
Span<byte>overbyte[]where possible - Use
stackallocwhere possible - Pool buffers where we can’t use
stackalloc
It could be taken further through other features such as SkipLocalsInit or fixed, both of which would require the /unsafe flag to compile. The benchmarks without this already give a nice boost, in particular by reducing memory overhead on larger payloads.
| Method | Length | Utf8 | Mean | Ratio | Gen0 | Allocated | Alloc Ratio ||---------- |------- |------ |---------:|------:|-------:|----------:|------------:|| Simple | 100 | False | 197.2 ns | 1.00 | 0.0470 | 296 B | 1.00 || Optimized | 100 | False | 164.9 ns | 0.84 | 0.0176 | 112 B | 0.38 || | | | | | | | || Simple | 100 | True | 191.8 ns | 1.00 | 0.0470 | 296 B | 1.00 || Optimized | 100 | True | 177.5 ns | 0.93 | 0.0176 | 112 B | 0.38 || | | | | | | | || Simple | 1000 | False | 615.2 ns | 1.00 | 0.1898 | 1192 B | 1.00 || Optimized | 1000 | False | 582.6 ns | 0.95 | 0.0172 | 112 B | 0.09 || | | | | | | | || Simple | 1000 | True | 635.0 ns | 1.00 | 0.1898 | 1192 B | 1.00 || Optimized | 1000 | True | 584.4 ns | 0.92 | 0.0172 | 112 B | 0.09 |

Leave a Reply