.net BenchmarkDotNet给出了意想不到的结果

5lhxktic  于 2023-05-02  发布在  .NET
关注(0)|答案(2)|浏览(167)

我在做一个调查,在计算性能的int,浮点数,双精度,十进制。我对结果很好奇。首先,我期望当我们做加号运算时,赢家将是int,但真实的是在截图上。

下面是我正在检查的代码。

  1. public class PerformanceTest
  2. {
  3. [Benchmark]
  4. public void CalcDouble()
  5. {
  6. double firstDigit = 135.543d;
  7. double secondDigit = 145.1234;
  8. double result = firstDigit + secondDigit;
  9. }
  10. [Benchmark]
  11. public void CalcDecimal()
  12. {
  13. decimal firstDigit = 135.543m;
  14. decimal secondDigit = 145.1234m;
  15. decimal result = firstDigit + secondDigit;
  16. }
  17. [Benchmark]
  18. public void Calcfloat()
  19. {
  20. float firstDigit = 135.543f;
  21. float secondDigit = 145.1234f;
  22. float result = firstDigit + secondDigit;
  23. }
  24. [Benchmark]
  25. public void Calcint()
  26. {
  27. int firstDigit = 135;
  28. int secondDigit = 145;
  29. int result = firstDigit + secondDigit;
  30. }
  31. }

谁能告诉我这是怎么回事?谢谢你
我希望Int是赢家,但赢家是float。

ix0qys7i

ix0qys7i1#

这显示了基准测试的问题,事情是 * 真的真的快 *;你可以通过 * 做更多的事情 * 来减慢速度-可选地使用OperationsPerInvoke来相应地缩放结果:
更好的结果:

  1. | Method | Mean | Error | StdDev |
  2. |------------ |----------:|----------:|----------:|
  3. | CalcDouble | 0.8501 ns | 0.0007 ns | 0.0006 ns |
  4. | CalcDecimal | 3.6070 ns | 0.0371 ns | 0.0329 ns |
  5. | CalcSingle | 0.8512 ns | 0.0027 ns | 0.0024 ns |
  6. | CalcInt32 | 0.2301 ns | 0.0019 ns | 0.0017 ns |

特别地,误差现在是平均值的舍入误差。
验证码:

  1. using BenchmarkDotNet.Attributes;
  2. using BenchmarkDotNet.Running;
  3. BenchmarkRunner.Run<PerformanceTest>();
  4. public class PerformanceTest
  5. {
  6. const int OperationsPerInvoke = 4096;
  7. [Benchmark(OperationsPerInvoke = OperationsPerInvoke)]
  8. public double CalcDouble()
  9. {
  10. double firstDigit = 135.543d;
  11. double secondDigit = 145.1234;
  12. for (int i = 0; i < OperationsPerInvoke; i++)
  13. {
  14. firstDigit += secondDigit;
  15. }
  16. return firstDigit;
  17. }
  18. [Benchmark(OperationsPerInvoke = OperationsPerInvoke)]
  19. public decimal CalcDecimal()
  20. {
  21. decimal firstDigit = 135.543m;
  22. decimal secondDigit = 145.1234m;
  23. for (int i = 0; i < OperationsPerInvoke; i++)
  24. {
  25. firstDigit += secondDigit;
  26. }
  27. return firstDigit;
  28. }
  29. [Benchmark(OperationsPerInvoke = OperationsPerInvoke)]
  30. public float CalcSingle()
  31. {
  32. float firstDigit = 135.543f;
  33. float secondDigit = 145.1234f;
  34. for (int i = 0; i < OperationsPerInvoke; i++)
  35. {
  36. firstDigit += secondDigit;
  37. }
  38. return firstDigit;
  39. }
  40. [Benchmark(OperationsPerInvoke = OperationsPerInvoke)]
  41. public int CalcInt32()
  42. {
  43. int firstDigit = 135;
  44. int secondDigit = 145;
  45. for (int i = 0; i < OperationsPerInvoke; i++)
  46. {
  47. firstDigit += secondDigit;
  48. }
  49. return firstDigit;
  50. }
  51. }
展开查看全部
h9a6wy2h

h9a6wy2h2#

第一部分。基准测试的问题在于

C#编译器和即时(JIT)编译器都可以对您的代码执行各种优化。具体的优化集取决于这些编译器的特定版本,但默认情况下,应该有一些基本的代码转换。
示例中的一个优化称为constant folding;它能够冷凝

  1. double firstDigit = 135.543d;
  2. double secondDigit = 145.1234;
  3. double result = firstDigit + secondDigit;

  1. double result = 280.6664d;

另一种优化称为dead code elimination。由于在基准测试中不使用计算结果,因此C#/JIT编译器能够完全消除此代码。因此,实际上,您可以像这样对空方法进行基准测试:

  1. [Benchmark]
  2. public void CalcDouble()
  3. {
  4. }

唯一的例外是CalcDecimal:由于Decimal是C#中的结构体(不是原始类型),C#/Roslyn编译器不够智能,无法完全消除计算(目前;这在将来可以改进)。
的上下文中详细讨论了这两种优化。NET基准测试(第65页:Dead Code Elimination,第69页:常数折叠)。这两个主题都属于“Common Benchmarking Pitfalls”一章,其中包含了更多可能扭曲基准测试结果的陷阱。

第二部分。BenchmarkDotNet结果

粘贴汇总表时,会剪切表下方的警告部分。我重新运行您的基准测试,以下是结果的扩展版本(默认情况下在BenchmarkDotNet结果中显示):

  1. | Method | Mean | Error | StdDev | Median |
  2. |------------ |----------:|----------:|----------:|----------:|
  3. | CalcDouble | 0.0006 ns | 0.0023 ns | 0.0022 ns | 0.0000 ns |
  4. | CalcDecimal | 3.0367 ns | 0.0527 ns | 0.0493 ns | 3.0135 ns |
  5. | Calcfloat | 0.0026 ns | 0.0023 ns | 0.0021 ns | 0.0000 ns |
  6. | Calcint | 0.0004 ns | 0.0010 ns | 0.0009 ns | 0.0000 ns |
  7. // * Warnings *
  8. ZeroMeasurement
  9. PerformanceTest.CalcDouble: Default -> The method duration is indistinguishable from the empty method duration
  10. PerformanceTest.Calcfloat: Default -> The method duration is indistinguishable from the empty method duration
  11. PerformanceTest.Calcint: Default -> The method duration is indistinguishable from the empty method duration

这些警告提供了对结果的宝贵见解。实际上,CalcDoubleCalcfloatCalcint与空方法(如

  1. public void Empty() { }

您在Mean列中看到的数字只是随机的CPU噪声,其持续时间低于一个CPU周期。假设你的CPU的频率是5GHz。这意味着单个CPU周期的持续时间约为0。2ns。没有什么可以比一个CPU周期更快地执行(如果我们谈论BenchmarkDotNet中默认测量的操作延迟;如果我们切换到吞吐量测量,我们可以获得“更快”的计算,这会产生各种效果,如instruction level parallelism,参见"Pro .NET Benchmarking",第440页)。CalcDoubleCalcfloatCalcint的“平均”值明显小于单个CPU周期的持续时间,因此实际比较它们没有意义。
BenchmarkDotNet发现Mean列有问题。因此,除了汇总表下面的警告之外,它还添加了一个奖励列Median(默认情况下是隐藏的),以突出显示所讨论的基准测试的零持续时间或空值。

第三部分。可能的基准设计改进

设计这样一个基准测试的最好方法是使它与所考虑的实际工作负载相似。算术运算的实际性能是一个极其棘手的东西来衡量;它取决于许多外部因素(如我前面提到的指令级并行性)。详情请参见"Pro .NET Benchmarking"的第7章“CPU绑定的基准测试”;它有24个案例研究,提供了各种例子。评估算术运算的“纯”持续时间是一个有趣的技术挑战,但它不能适用于现实生活中的代码。
这里也是一些推荐的BenchmarkDotNet技巧来设计更好的基准测试:
1.将所有“常量”变量移动到公共字段/属性。在这种情况下,C#/JIT编译器将无法应用常量折叠(因为它事先不知道没有人会实际更改这些公共字段/属性的值)。
1.从[Benchmark]方法返回计算结果。这是要求BenchmarkDotNet防止死代码消除的方法。
suggested approach by Marc Gravell将在一定程度上起作用。但是,它也可能存在一些其他问题:
1.不建议在for循环中使用固定的迭代次数,因为不同的JIT编译器可能会以不同的方式应用循环展开(另一个基准测试陷阱,参见"Pro .NET Benchmarking",第61页),这可能会扭曲某些环境中的结果。
1.请注意,添加人工循环会给基准测试增加一些性能成本。因此,可以使用这样一组基准测试来获得相对结果,但绝对值还包括循环开销("Pro .NET Benchmarking",第54页)。
1.据我所知,现代的C#/JIT编译器还没有聪明到完全消除这样的代码。但是,我们不能保证它不会被消除,因为它实际上总是返回相同的常数。未来版本的编译器可以足够智能地执行这样的优化(我相信一些Java运行时能够消除类似的基准测试)。因此,最好将所有常量移动到公共非常量字段/属性中,以防止这种情况。

展开查看全部

相关问题