Math.Max vs inline if – 有什么区别?

我今天正在研究一个项目,并发现自己在几个地方使用Math.Max并在其他地方使用内联if语句。 所以,我想知道是否有人知道哪个更“好”……或者更确切地说,真正的差异是什么。

例如,在下面, c1 = c2

 Random rand = new Random(); int a = rand.next(0,10000); int b = rand.next(0,10000); int c1 = Math.Max(a, b); int c2 = a>b ? a : b; 

我是专门询问C#,但我想在不同的语言中答案可能会有所不同,但我不确定哪些有类似的概念。

我立即注意到的一个主要差异是为了可读性,据我所知,在实现/性能方面,它们几乎是等价的。

无论以前的编码知识如何, Math.Max(a,b)都很容易理解。

a>b ? a : b a>b ? a : b至少要求用户对三元运算符有一定的了解。

如有疑问 – 请寻求可读性

我认为在这个讨论中加入一些数字会很有趣,所以我写了一些代码来描述它。 正如所料,它们在所有实际用途中几乎相同。

该代码完成了十亿次循环(是10亿)。 减去循环的开销,你会得到:

  • Math.Max()耗时.0044秒,运行10亿次
  • 内联如果花费.0055秒运行10亿次

我通过运行10亿次空循环减去了我计算的开销,开销是1.2秒。

我在笔记本电脑上运行了这个,64位Windows 7,3.3 Ghz Intel Core i5(U470)。 代码是在发布模式下编译的,并且在没有附加调试器的情况下运行。

这是代码:

 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Diagnostics; namespace TestMathMax { class Program { static int Main(string[] args) { var num1 = 10; var num2 = 100; var maxValue = 0; var LoopCount = 1000000000; double controlTotalSeconds; { var stopwatch = new Stopwatch(); stopwatch.Start(); for (var i = 0; i < LoopCount; i++) { // do nothing } stopwatch.Stop(); controlTotalSeconds = stopwatch.Elapsed.TotalSeconds; Console.WriteLine("Control - Empty Loop - " + controlTotalSeconds + " seconds"); } Console.WriteLine(); { var stopwatch = new Stopwatch(); stopwatch.Start(); for (int i = 0; i < LoopCount; i++) { maxValue = Math.Max(num1, num2); } stopwatch.Stop(); Console.WriteLine("Math.Max() - " + stopwatch.Elapsed.TotalSeconds + " seconds"); Console.WriteLine("Relative: " + (stopwatch.Elapsed.TotalSeconds - controlTotalSeconds) + " seconds"); } Console.WriteLine(); { var stopwatch = new Stopwatch(); stopwatch.Start(); for (int i = 0; i < LoopCount; i++) { maxValue = num1 > num2 ? num1 : num2; } stopwatch.Stop(); Console.WriteLine("Inline Max: " + stopwatch.Elapsed.TotalSeconds + " seconds"); Console.WriteLine("Relative: " + (stopwatch.Elapsed.TotalSeconds - controlTotalSeconds) + " seconds"); } Console.ReadLine(); return maxValue; } } } 

更新结果2015年2月7日

在Windows 8.1,Surface 3 Pro,i7 4650U 2.3Ghz Ran上作为控制台应用程序在发布模式下没有附加调试器。

  • Math.Max() – 0.3194749秒
  • 内联最大值:0.3465041秒

如果声明被认为有益

摘要

forms声明if (a > max) max = a是确定一组数字最大值的最快方法。 然而,循环基础结构本身占用了大部分CPU时间,因此这种优化最终是值得怀疑的。

细节

luisperezphd的答案很有意思,因为它提供了数字,但我相信这个方法存在缺陷:编译器很可能将比较移出循环,所以答案并不能衡量它想要衡量的内容。 这解释了控制回路和测量回路之间可忽略不计的时序差异。

为了避免这种循环优化,我添加了一个依赖于循环变量的操作,空控制循环以及所有测量循环。 我模拟了在数字列表中找到最大值的常见用例,并使用了三个数据集:

  • 最佳情况:第一个数字是最大值,后面的所有数字都是较小的
  • 最坏的情况:每个数字都比前一个更大,所以最大值会改变每次迭代
  • 平均情况:一组随机数

请参阅下面的代码。

结果对我来说相当令人惊讶。 在我的Core i5 2520M笔记本电脑上,我获得了以下10亿次迭代(空控制在所有情况下大约需要2.6秒):

  • max = Math.Max(max, a) :2.0秒最佳情况/ 1.3秒最差情况/ 2.0秒平均情况
  • max = Math.Max(a, max) :1.6秒最佳情况/ 2.0秒最差情况/ 1.5秒平均情况
  • max = max > a ? max : a max = max > a ? max : a :1.2秒最佳情况/ 1.2秒最差情况/ 1.2秒平均情况
  • if (a > max) max = a :0.2秒最佳情况/ 0.9秒最差情况/ 0.3秒平均情况

因此,尽管CPU流水线很长,并且由此产生了对分支的惩罚,但对于所有模拟数据集而言,良好的旧if语句是明显的赢家; 在最好的情况下,它比Math.Max快10倍,在最坏的情况下仍然快30%以上。

另一个惊喜是Math.Max的参数顺序Math.Max重要。 据推测,这是因为CPU分支预测逻辑对两种情况的工作方式不同,并且根据参数的顺序或多或少地错误预测分支。

但是,大部分CPU时间都花在了循环基础结构上,因此最终这种优化充其量是有问题的。 它提供了可测量但略微减少的总体执行时间。

由luisperezphd更新

我不能把它作为一个评论,在这里写它更有意义,而不是作为我的答案的一部分,所以它是在上下文中。

你的理论是有道理的,但我无法重现结果。 首先出于某种原因使用你的代码我的控制循环比包含工作的循环花费更长的时间。

出于这个原因,我把这里的数字相对于最低时间而不是控制循环。 结果中的秒数是最长时间所花费的时间。 例如,在紧接着最快时间的结果中,对于Math.Max(a,max)最佳情况,所以每个其他结果表示他们花费的时间比这长。

以下是我得到的结果:

  • max = Math.Max(max, a) :0.012秒最佳情况/ 0.007秒最差情况/ 0.028秒平均情况
  • max = Math.Max(a, max) :0.000最佳情况/ 0.021最差情况/ 0.019秒平均情况
  • max = max > a ? max : a max = max > a ? max : a :0.022秒最佳情况/ 0.02秒最差情况/ 0.01秒平均情况
  • if (a > max) max = a :0.015秒最佳情况/ 0.024秒最差情况/ 0.019秒平均情况

我第二次跑它时得到了:

  • max = Math.Max(max, a ):0.024秒最佳情况/ 0.010秒最差情况/ 0.009秒平均情况
  • max = Math.Max(a, max) :0.001秒最佳情况/ 0.000秒最差情况/ 0.018秒平均情况
  • max = max > a ? max : a max = max > a ? max : a :0.011秒最佳情况/ 0.005秒最差情况/ 0.018秒平均情况
  • if (a > max) max = a :0.000秒最佳情况/ 0.005秒最差情况/ 0.039秒平均情况

在这些测试中有足够的音量可以消除任何exception。 尽管如此,结果却截然不同。 也许数组的大内存分配与它有关。 或者可能差异太小,以至于当时计算机上发生的任何其他事情都是变异的真正原因。

注意最快的时间,在上面的结果中表示为0.000约为8秒。 因此,如果你认为最长的运行时间是8.039,那么时间的变化大约是0.5%(也就是说太小了)。

电脑

代码在Windows 8.1,i7 4810MQ 2.8Ghz上运行,并在.NET 4.0中编译。

代码修改

我稍微修改了你的代码,以上面显示的格式输出结果。 在开始考虑运行程序集时.NET可能需要的任何额外加载时间后,我还添加了额外的代码等待1秒。

此外,我还运行了所有测试两次,以解决任何CPU优化问题。 最后我将int更改为一个unit这样我就可以运行循环40亿次而不是10亿次来获得更长的时间跨度。

这可能都是矫枉过正,但要尽可能确保测试不受任何这些因素的影响。

您可以在http://pastebin.com/84qi2cbD找到该代码

 using System; using System.Diagnostics; namespace ProfileMathMax { class Program { static double controlTotalSeconds; const int InnerLoopCount = 100000; const int OuterLoopCount = 1000000000 / InnerLoopCount; static int[] values = new int[InnerLoopCount]; static int total = 0; static void ProfileBase() { Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); int maxValue; for (int j = 0; j < OuterLoopCount; j++) { maxValue = 0; for (int i = 0; i < InnerLoopCount; i++) { // baseline total += values[i]; } } stopwatch.Stop(); controlTotalSeconds = stopwatch.Elapsed.TotalSeconds; Console.WriteLine("Control - Empty Loop - " + controlTotalSeconds + " seconds"); } static void ProfileMathMax() { int maxValue; Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); for (int j = 0; j < OuterLoopCount; j++) { maxValue = 0; for (int i = 0; i < InnerLoopCount; i++) { maxValue = Math.Max(values[i], maxValue); total += values[i]; } } stopwatch.Stop(); Console.WriteLine("Math.Max(a, max) - " + stopwatch.Elapsed.TotalSeconds + " seconds"); Console.WriteLine("Relative: " + (stopwatch.Elapsed.TotalSeconds - controlTotalSeconds) + " seconds"); } static void ProfileMathMaxReverse() { int maxValue; Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); for (int j = 0; j < OuterLoopCount; j++) { maxValue = 0; for (int i = 0; i < InnerLoopCount; i++) { maxValue = Math.Max(maxValue, values[i]); total += values[i]; } } stopwatch.Stop(); Console.WriteLine("Math.Max(max, a) - " + stopwatch.Elapsed.TotalSeconds + " seconds"); Console.WriteLine("Relative: " + (stopwatch.Elapsed.TotalSeconds - controlTotalSeconds) + " seconds"); } static void ProfileInline() { int maxValue = 0; Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); for (int j = 0; j < OuterLoopCount; j++) { maxValue = 0; for (int i = 0; i < InnerLoopCount; i++) { maxValue = maxValue > values[i] ? values[i] : maxValue; total += values[i]; } } stopwatch.Stop(); Console.WriteLine("max = max > a ? a : max: " + stopwatch.Elapsed.TotalSeconds + " seconds"); Console.WriteLine("Relative: " + (stopwatch.Elapsed.TotalSeconds - controlTotalSeconds) + " seconds"); } static void ProfileIf() { int maxValue = 0; Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); for (int j = 0; j < OuterLoopCount; j++) { maxValue = 0; for (int i = 0; i < InnerLoopCount; i++) { if (values[i] > maxValue) maxValue = values[i]; total += values[i]; } } stopwatch.Stop(); Console.WriteLine("if (a > max) max = a: " + stopwatch.Elapsed.TotalSeconds + " seconds"); Console.WriteLine("Relative: " + (stopwatch.Elapsed.TotalSeconds - controlTotalSeconds) + " seconds"); } static void Main(string[] args) { Random rnd = new Random(); for (int i = 0; i < InnerLoopCount; i++) { //values[i] = i; // worst case: every new number biggest than the previous //values[i] = i == 0 ? 1 : 0; // best case: first number is the maximum values[i] = rnd.Next(int.MaxValue); // average case: random numbers } ProfileBase(); Console.WriteLine(); ProfileMathMax(); Console.WriteLine(); ProfileMathMaxReverse(); Console.WriteLine(); ProfileInline(); Console.WriteLine(); ProfileIf(); Console.ReadLine(); } } } 

我会说更快地理解Math.Max正在做什么,而这应该是这里唯一的决定因素。

但作为一种放纵,有趣的是, Math.Max(a,b)一次评估参数,而a > b ? a : b a > b ? a : b两次评估其中一个。 局部变量不是问题,但对于有副作用的属性,副作用可能会发生两次。

如果JITer选择内联Math.Max函数,则可执行代码将与if语句相同。 如果没有内联Math.Max,它将作为函数调用执行,并且if语句中不存在调用和返回开销。 因此,if语句在内联情况下将给出与Math.Max()相同的性能,或者if语句在非内联情况下可能会快几个时钟周期,但除非你运行数十,否则差异不会很明显数以百万计的比较。

由于两者之间的性能差异很小,在大多数情况下可以忽略不计,我更喜欢Math.Max(a,b),因为它更容易阅读。

关于性能,现代CPU具有内部命令管道,使得每个汇编命令在几个内部步骤中执行。 (例如取,解释,计算,存储)

在大多数情况下,CPU足够智能,可以并行执行这些步骤以实现顺序命令,因此整体吞吐量非常高。

这是好的,直到有一个分支( if?:等)。 分支可能会破坏序列并强制CPU废弃管道。 这会花费很多时钟周期。

理论上,如果编译器足够智能,可以使用内置的CPU命令实现Math.Max ,并且可以避免分支。

在这种情况下, Math.Max实际上会比if更快 – 但它取决于编译器..

如果更复杂的Max – 就像在矢量上工作一样, double []v; v.Max() double []v; v.Max()编译器可以使用高度优化的库代码,这比常规编译代码快得多。

因此最好使用Math.Max,但是如果它足够重要,还建议检查您的特定目标系统和编译器。

Math.Max(A,B)

不是等于 a > b ? a : b a > b ? a : b在所有情况下。

Math.Max返回两个参数的更大值,即:

 if (a == b) return a; // or b, doesn't matter since they're identical else if (a > b && b < a) return a; else if (b > a && a < b) return b; else return undefined; 

例如,在Math.Max的双重过载的情况下,undefined被映射到double.NaN

a> b? a:b

评估a a是否大于b,这并不一定意味着b小于a。

一个简单的例子,certificate它们不等价:

 var a = 0.0/0.0; // or double.NaN var b = 1.0; a > b ? a : b // evaluates to 1.0 Math.Max(a, b) // returns double.NaN