bcrypt.net icon indicating copy to clipboard operation
bcrypt.net copied to clipboard

Provide `[ReadOnly]Span<char>` overloads for the methods

Open Rekkonnect opened this issue 8 months ago • 4 comments

Feature Request

Details

Problem Description

The overloads currently exposed in the public API are all using string parameters. Since .NET Core, spans have been widely preferred for a variety of reasons involving performance, and are adopted in many cases.

Proposed Solution

Either change all string parameters to ReadOnlySpan<char> parameters, or provide overloads taking ROS<char> parameters, optionally using the OverloadResolutionPriority attribute to enhance their visibility.

For HashPassword, we can also provide overloads accepting the input as ROS<char>, and outputting the value into a user-provided Span<char> buffer:

public void HashPassword(ReadOnlySpan<char> inputKey, Span<char> output);
public void HashPassword(ReadOnlySpan<char> inputKey, ReadOnlySpan<char> salt, Span<char> output);
...

Alternatives considered

The current workaround is to manually convert the ROS<char> into a newly allocated string in order to pass it into this library's functions. This is suboptimal for critical performance scenaria.

Additional context

With the 5.0.0 release nearing, this would also be a good breaking change to include. Another release with even more breaking changes would probably not be ideal.

Rekkonnect avatar Mar 16 '25 12:03 Rekkonnect

@Rekkonnect its a work in progress as it kicked off splitting the framework from the standard a tad more brutally than the inline changes made the previous time in order to chase the chain through the various methods and to rebuild various benchmarks around the changes.

https://github.com/BcryptNet/bcrypt.net/tree/ReadonlySpanSupport


BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.3476)
AMD Ryzen 9 9900X, 1 CPU, 24 logical and 12 physical cores
.NET SDK 9.0.202
  [Host]     : .NET 9.0.3 (9.0.325.11113), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
  Job-JCLYIQ : .NET 9.0.3 (9.0.325.11113), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI

Runtime=.NET 9.0  

Method Mean Error StdDev Ratio RatioSD Rank Gen0 Allocated Alloc Ratio
OldMethod 1.326 μs 0.0190 μs 0.0158 μs 1.00 0.02 2 0.0172 1008 B 1.00
NewMethod 1.023 μs 0.0168 μs 0.0149 μs 0.77 0.01 1 - 72 B 0.07
Method Mean Error StdDev Ratio RatioSD Rank Gen0 Gen1 Gen2 Allocated Alloc Ratio
EncodeBase64Unsized 36.29 ns 0.729 ns 0.682 ns 1.00 0.03 3 0.0049 - - 280 B 1.00
EncodeBase64Sized 36.49 ns 0.742 ns 1.065 ns 1.01 0.03 3 0.0049 - - 280 B 1.00
EncodeBase64AsBytes 15.95 ns 0.332 ns 0.465 ns 0.44 0.02 1 0.0013 - - 72 B 0.26
EncodeBase64StackAlloc 22.56 ns 0.229 ns 0.214 ns 0.62 0.01 2 0.0016 - - 72 B 0.26
EncodeBase64HeapSpanAlloc 16.07 ns 0.230 ns 0.215 ns 0.44 0.01 1 0.0017 0.0000 0.0000 - 0.00
Method Mean Error StdDev Median Ratio RatioSD Rank Gen0 Allocated Alloc Ratio
OldMethod 1.363 μs 0.0236 μs 0.0197 μs 1.369 μs 1.00 0.02 2 0.0172 1008 B 1.00
NewMethod 1.111 μs 0.0219 μs 0.0395 μs 1.128 μs 0.82 0.03 1 - - 0.00
Method key Mean Error StdDev Ratio RatioSD Rank Allocated Alloc Ratio
TestHashValidateEnhanced ~!@#$(...)NBFRD [34] 167.3 ms 0.75 ms 0.66 ms 1.00 0.01 1 262.51 KB 1.00
TestHashValidateEnhancedv305Perf1 ~!@#$(...)NBFRD [34] 167.7 ms 0.21 ms 0.17 ms 1.00 0.00 1 261.83 KB 1.00
TestHashValidateEnhancedv4Perf ~!@#$(...)NBFRD [34] 166.5 ms 3.29 ms 3.23 ms 0.99 0.02 1 261.85 KB 1.00
TestHashValidateEnhancedCurrent ~!@#$(...)NBFRD [34] 166.9 ms 3.06 ms 2.56 ms 1.00 0.02 1 5.51 KB 0.02
TestHashValidateEnhancedNet8Plus ~!@#$(...)NBFRD [34] 167.6 ms 0.22 ms 0.18 ms 1.00 0.00 1 4.81 KB 0.02
Method key salt Mean Error StdDev Ratio RatioSD Rank Allocated Alloc Ratio
TestHashValidateEnhanced ~!@#$(...)NBFRD [34] $2a$1(...)rOvHe [29] 41.95 ms 0.121 ms 0.113 ms 1.00 0.00 1 70.29 KB 1.00
TestHashValidateEnhancedv3Perf ~!@#$(...)NBFRD [34] $2a$1(...)rOvHe [29] 41.60 ms 0.797 ms 0.783 ms 0.99 0.02 1 69.62 KB 0.99
TestHashValidateEnhancedv4Perf ~!@#$(...)NBFRD [34] $2a$1(...)rOvHe [29] 41.71 ms 0.794 ms 0.663 ms 0.99 0.02 1 69.62 KB 0.99
TestHashValidateEnhancedCurrent ~!@#$(...)NBFRD [34] $2a$1(...)rOvHe [29] 41.73 ms 0.690 ms 0.611 ms 0.99 0.01 1 5.29 KB 0.08
TestHashValidateEnhancedNet8Plus ~!@#$(...)NBFRD [34] $2a$1(...)rOvHe [29] 41.98 ms 0.062 ms 0.058 ms 1.00 0.00 1 4.63 KB 0.07

ChrisMcKee avatar May 07 '25 10:05 ChrisMcKee

That's some very good improvement. I'm only not sure about returning a new byte[], we could have a parameter accepting a writable Span to contain the output bytes in. This would be passed by the user and should always accommodate the expected output byte count. That way we avoid one more allocation and it should also be faster.

The purpose of this is to allow passing a reusable output buffer across multiple calls of the function. An overload that allocates the returned byte array is also good to have because users may not want to reuse the result buffer, avoiding the necessary step of allocating the output buffer in calling the API.

Rekkonnect avatar May 07 '25 11:05 Rekkonnect

@Rekkonnect The base https://github.com/BcryptNet/bcrypt.net/blob/ReadonlySpanSupport/src/BCrypt.Net/BCryptBase.cs#L297 takes an ouput buffer. I've tried to keep the api relatively low effort, hence the overload can just return string as normal.

    internal static string CreatePasswordHash(ReadOnlySpan<char> inputKey, ReadOnlySpan<char> salt, HashType hashType = HashType.None, EnhancedHashDelegate enhancedHashKeyGen = null)
    {
        Span<char> outputBuffer = stackalloc char[60];
        CreatePasswordHash(inputKey, salt, outputBuffer, out var outputBufferWritten, hashType, enhancedHashKeyGen);
        return new string(outputBuffer[..outputBufferWritten]);
    }

and

        public static string HashPassword(ReadOnlySpan<char> inputKey, ReadOnlySpan<char> salt)
        {
            Span<char> outputBuffer = stackalloc char[60];
            HashPassword(inputKey, salt, outputBuffer, out var outputBufferWritten);
            return new string(outputBuffer[..outputBufferWritten]);
        }

There'll always be an allocation at some point with a string as the benchmarks are testing the same default method signature end to end so they're technically going to be worst case.

ChrisMcKee avatar May 07 '25 11:05 ChrisMcKee

Right I missed that. It's all good then

Rekkonnect avatar May 07 '25 12:05 Rekkonnect

There are other reasons to use Span<T>, such as compatibility with CryptographicOperations.ZeroMemory and CryptographicOperations.FixedTimeEquals.

Is there a timeline for this?

danielcrenna avatar Sep 06 '25 00:09 danielcrenna

Aiming for the end of the month.

ChrisMcKee avatar Sep 07 '25 12:09 ChrisMcKee