You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于BenchmarkDotNet的两个RequestSigner类性能测试疑问

问题

我想对比以下两个C#类的性能差异,用BenchmarkDotNet写了测试用例,但结果显示预期性能更优的RequestSigner2(对应测试方法First())反而比RequestSigner(对应测试方法Second())慢,不确定当前测试是否正确。如果测试有问题,合适的测试方案应该是什么样的?

现有测试代码

public class RequestSignerBenchmark
{
    [Benchmark]
    public void First()
    {
        var signer = RequestSigner2.FromSecret("hidden");

        var timestamp = TimestampProvider.Default.CurrentTimestamp();
        signer.Sign(timestamp, "GET", "products", "");
    }

    [Benchmark]
    public void Second()
    {
        var signer = RequestSigner.FromSecret("hidden");

        var timestamp = TimestampProvider.Default.CurrentTimestamp();
        signer.Sign(timestamp, "GET", "products", "");
    }
}

// Program.cs
BenchmarkRunner.Run<RequestSignerBenchmark>();

现有测试结果

| Method |     Mean |     Error |    StdDev |
|------- |---------:|----------:|----------:|
|  First | 3.234 us | 0.0646 us | 0.1769 us |
| Second | 2.659 us | 0.0516 us | 0.0614 us |

待评估类

public sealed class RequestSigner
{
    private readonly byte[] _base64Secret;

    public RequestSigner(byte[] base64Secret)
    {
        _base64Secret = base64Secret;
    }

    public static RequestSigner FromSecret(string secret)
    {
        return new RequestSigner(Convert.FromBase64String(secret));
    }

    public string Sign(string timestamp, string method, string requestPath, string body)
    {
        var orig = Encoding.UTF8.GetBytes(timestamp + method.ToUpperInvariant() + requestPath + body);
        using var hmac = new HMACSHA256(_base64Secret);
        return Convert.ToBase64String(hmac.ComputeHash(orig));
    }
}

public class RequestSigner2 : IDisposable
{
    private readonly HMACSHA256 _hmac;

    public RequestSigner2(byte[] base64Secret)
    {
        _hmac = new HMACSHA256(base64Secret);
    }

    public static RequestSigner2 FromSecret(string secret)
    {
        return new RequestSigner2(Convert.FromBase64String(secret));
    }

    public string Sign(string timestamp, string method, string requestPath, string body)
    {
        DoDisposeChecks();

        if (string.IsNullOrWhiteSpace(method) || string.IsNullOrWhiteSpace(requestPath))
        {
            return string.Empty;
        }

        var orig = Encoding.UTF8.GetBytes(timestamp + method.ToUpperInvariant() + requestPath + body);
        return Convert.ToBase64String(_hmac.ComputeHash(orig));
    }

    #region Disposable

    private bool _isDisposed;

    /// <summary>
    ///     Checks if this object has been disposed.
    /// </summary>
    /// <exception cref="ObjectDisposedException">Thrown if the object has been disposed.</exception>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    protected void DoDisposeChecks()
    {
        if (_isDisposed)
        {
            throw new ObjectDisposedException(nameof(RequestSigner2));
        }
    }

    /// <inheritdoc />
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    /// <summary>
    ///     Disposes of managed and unmanaged resources.
    /// </summary>
    /// <param name="disposing">A value indicating whether or not to dispose of managed resources.</param>
    protected virtual void Dispose(bool disposing)
    {
        if (_isDisposed)
        {
            return;
        }

        if (disposing)
        {
            _hmac.Dispose();
        }

        _isDisposed = true;
    }

    #endregion
}

问题分析

当前测试完全偏离了RequestSigner2的设计意图:

  • RequestSigner的设计是每次签名都创建新的HMACSHA256实例,用完即释放;
  • RequestSigner2的设计是复用同一个HMACSHA256实例,通过避免重复初始化HMAC来提升多次签名的性能。

但你现在的测试逻辑是每次Benchmark调用都创建新的Signer实例,这相当于把RequestSigner2的HMAC初始化开销、Disposable检查逻辑都算进了单次签名的耗时里,反而掩盖了它复用HMAC的优势。

正确的测试方案

要测试RequestSigner2的性能优势,必须模拟多次复用同一个Signer实例进行签名的场景,具体修改如下:

修改后的测试代码

public class RequestSignerBenchmark : IDisposable
{
    private RequestSigner _signer1;
    private RequestSigner2 _signer2;
    private string _timestamp;

    // 全局初始化:只执行一次,创建Signer实例和复用的参数
    [GlobalSetup]
    public void Setup()
    {
        _signer1 = RequestSigner.FromSecret("hidden");
        _signer2 = RequestSigner2.FromSecret("hidden");
        _timestamp = TimestampProvider.Default.CurrentTimestamp();
    }

    [Benchmark]
    public string First()
    {
        return _signer2.Sign(_timestamp, "GET", "products", "");
    }

    [Benchmark]
    public string Second()
    {
        return _signer1.Sign(_timestamp, "GET", "products", "");
    }

    // 全局清理:释放RequestSigner2
    [GlobalCleanup]
    public void Cleanup()
    {
        _signer2.Dispose();
    }
}

// Program.cs
BenchmarkRunner.Run<RequestSignerBenchmark>();

关键修改点说明

  1. 用[GlobalSetup]初始化Signer实例:确保每个Signer只创建一次,测试的是重复调用Sign方法的性能,这才是RequestSigner2设计要优化的真实场景;
  2. 复用测试参数:提前生成timestamp,避免每次调用都生成新字符串带来的额外开销,保证测试聚焦在Sign方法本身;
  3. 返回签名结果:BenchmarkDotNet会忽略无返回值方法的一些优化,返回结果可以避免JIT优化掉无意义的计算;
  4. 实现IDisposable清理资源:确保RequestSigner2的HMAC资源被正确释放。

预期结果

修改后,RequestSigner2的性能会显著优于RequestSigner,因为它避免了每次签名都创建和销毁HMACSHA256实例的开销——HMAC的初始化涉及密钥的哈希运算,这是比较耗时的操作,复用实例能大幅减少这部分开销。


内容的提问来源于stack exchange,提问作者nop

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 03:24:20