基于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>();
关键修改点说明
- 用
[GlobalSetup]初始化Signer实例:确保每个Signer只创建一次,测试的是重复调用Sign方法的性能,这才是RequestSigner2设计要优化的真实场景; - 复用测试参数:提前生成
timestamp,避免每次调用都生成新字符串带来的额外开销,保证测试聚焦在Sign方法本身; - 返回签名结果:BenchmarkDotNet会忽略无返回值方法的一些优化,返回结果可以避免JIT优化掉无意义的计算;
- 实现
IDisposable清理资源:确保RequestSigner2的HMAC资源被正确释放。
预期结果
修改后,RequestSigner2的性能会显著优于RequestSigner,因为它避免了每次签名都创建和销毁HMACSHA256实例的开销——HMAC的初始化涉及密钥的哈希运算,这是比较耗时的操作,复用实例能大幅减少这部分开销。
内容的提问来源于stack exchange,提问作者nop
相关产品推荐
相关产品推荐

