Skip to content

Conversation

@LeaFrock
Copy link
Contributor

As stated in the title.

Copy link
Member

@martincostello martincostello left a comment

Choose a reason for hiding this comment

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

Just curious, do you have any numbers showing the actual impact of the changes?

@martincostello
Copy link
Member

Just curious, do you have any numbers showing the actual impact of the changes?

@LeaFrock
Copy link
Contributor Author

Just curious, do you have any numbers showing the actual impact of the changes?

It is not easy to write a precise microbenchmark. I tested a simplified and somewhat crude version.

The result is the following:

BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.4061)
AMD Ryzen 7 3700X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 9.0.300
  [Host]     : .NET 9.0.5 (9.0.525.21509), X64 RyuJIT AVX2
  DefaultJob : .NET 9.0.5 (9.0.525.21509), X64 RyuJIT AVX2
Method Mean Error StdDev Median Ratio RatioSD Gen0 Allocated Alloc Ratio
DictLinq 1,209.9 ns 24.13 ns 48.19 ns 1,188.7 ns 1.00 0.06 0.2823 2.31 KB 1.00
SortList 570.7 ns 11.38 ns 21.64 ns 565.2 ns 0.47 0.03 0.1764 1.45 KB 0.62

The src codes:

[MemoryDiagnoser]
public class DictLinqVsSortList
{
    private const string ClientId = "Bilibili_Cheers";
    private readonly string Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();

    [Benchmark(Baseline = true)]
    public string DictLinq()
    {
        Dictionary<string, string> d = new(7)
        {
            { "access-token", "1234567890" },
            { "x-bili-accesskeyid", ClientId },
            { "x-bili-content-md5", "d41d8cd98f00b204e9800998ecf8427e" }, // It's a GET request so there's no content, so we send the MD5 hash of an empty string
            { "x-bili-signature-method", "HMAC-SHA256" },
            { "x-bili-signature-nonce", GenerateNonce() },
            { "x-bili-signature-version", "2.0" },
            { "x-bili-timestamp", Timestamp }
        };
        var headers = d
           .Where(h => h.Key.StartsWith("x-bili-", StringComparison.OrdinalIgnoreCase))
           .OrderBy(h => h.Key)
           .Select(h => $"{h.Key}:{string.Join(",", h.Value)}")
           .ToList();
        return string.Join('\n', headers);
    }

    [Benchmark]
    public string SortList()
    {
        SortedList<string, string> s = new(6, StringComparer.OrdinalIgnoreCase)
        {
            { "x-bili-accesskeyid", ClientId },
            { "x-bili-content-md5", "d41d8cd98f00b204e9800998ecf8427e" }, // It's a GET request so there's no content, so we send the MD5 hash of an empty string
            { "x-bili-signature-method", "HMAC-SHA256" },
            { "x-bili-signature-nonce", GenerateNonce() },
            { "x-bili-signature-version", "2.0" },
            { "x-bili-timestamp", Timestamp }
        };
        return BuildSignatureString(s);

        static string BuildSignatureString(SortedList<string, string> xbiliHeaders)
        {
            var sb = new StringBuilder(256); // 256 is an estimated size for the plain text
            foreach ((var name, var value) in xbiliHeaders)
            {
                sb.Append(name)
                  .Append(':')
                  .Append(value)
                  .Append('\n');
            }

            return sb.ToString(0, sb.Length - 1); // Ignore the last '\n'
        }
    }

    private static string GenerateNonce()
    {
        Span<byte> bytes = stackalloc byte[256 / 8];
        RandomNumberGenerator.Fill(bytes);
        return Base64Url.EncodeToString(bytes);
    }
}

I do allocate an extra SortedList<string, string> but it's worthy to replace the LINQ and temp strings' selecting.

The fastest way should be generating the sorted x-bili- headers codes during compiling time with SG, which avoids the runtime sorting anymore, but that would be a little bit complex to maintain and not quite meaningful.

Also, the GenerateNonce is a better version as it avoids a byte[32] heap allocation each time. Perhaps the JIT of .NET 10 can eliminate such differences since the byte array is small(32 bytes), but it is better to optimize at the code level.

@martincostello
Copy link
Member

Thanks for putting together the benchmark - ~2x throughput and memory improvement is a nice win.

I'll merge this now, but I won't release it today so there's not two releases in one day 😄

@martincostello martincostello added this to the 9.4.1 milestone May 21, 2025
@martincostello martincostello merged commit ae32734 into aspnet-contrib:dev May 21, 2025
8 checks passed
@LeaFrock
Copy link
Contributor Author

Thanks. There's no rush for the next release.

I just think that library-level code should be as performance-friendly as possible, and I'm willing to make a contribution 😄 .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

2 participants