【问题标题】:Math.Max vs inline if - what are the differences?Math.Max vs inline if - 有什么区别?
【发布时间】:2011-07-25 15:11:21
【问题描述】:

我今天在做一个项目,发现自己在多个地方使用 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)Math.Max(b, a)a > b ? a : ba < b ? b : ab > a ? b : ab < a ? a : a
  • 代码可读性远比两者之间任何微小的性能差异更有价值。
  • 我不认为它对性能有任何影响,但Math.Max() 更具可读性,所以我更喜欢这样。
  • 我会说 Math.Max 更清楚地显示了代码的意图,这很重要。
  • @BoltClock,我认为没有必要唯一考虑所有可交换等效语句。不管你怎么切,Math.Max(a,b)Math.Max(b,a)一样,就像a+bb+a一样

标签: c# inline-if


【解决方案1】:

进行一次手术; N 必须 >= 0

一般解决方案:

A) N = Math.Max(0, N)
B) if(N < 0){N = 0}

按速度排序:

慢:Math.Max (A)

但我的解决方案比解决方案“B”快 4%:

N *= Math.Sign(1 + Math.Sign(N));

【讨论】:

  • 在您的示例中,您将 N 与 1 进行比较是什么?
【解决方案2】:

Math.Max(a,b)

在所有情况下不等同于a &gt; 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 双重重载的情况下,未定义将映射到double.NaN

a > b ?一:乙

如果 a 大于 b,则计算为 a,这并不一定意味着 b 小于 a。

一个简单的例子证明它们是不等价的:

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

【讨论】:

    【解决方案3】:

    if 语句被认为是有益的

    总结

    if (a &gt; 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 &gt; a ? max : a:最佳情况 1.2 秒 / 最坏情况 1.2 秒 / 平均情况 1.2 秒
    • if (a &gt; max) max = a:0.2 秒最佳情况/0.9 秒最坏情况/0.3 秒平均情况

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

    另一个惊喜是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 &gt; a ? max : a:最佳情况 0.022 秒 / 最坏情况 0.02 秒 / 平均情况 0.01 秒
    • if (a &gt; 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 &gt; a ? max : a:0.011 秒最佳情况/0.005 秒最坏情况/0.018 秒平均情况
    • if (a &gt; max) max = a:0.000 秒最佳情况/0.005 秒最坏情况/0.039 秒平均情况

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

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

    电脑

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

    代码修改

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

    此外,我将所有测试运行了两次,以考虑任何 CPU 优化。最后,我将 iint 更改为 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();
        }
      }
    }
    

    【讨论】:

    • 这是一个真正的科学答案。但是您忘记了最终结论,这是一篇真正的科学文章的典型特征。 :)
    【解决方案4】:

    我认为在这个讨论中加入一些数字会很有趣,所以我编写了一些代码来分析它。正如预期的那样,它们在所有实际用途上几乎相同。

    代码执行了十亿次循环(是的,十亿次)。减去你得到的循环开销:

    • Math.Max() 运行 10 亿次耗时 0.0044 秒
    • 内联 if 需要 0.0055 秒才能运行 10 亿次

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

    我在 64 位 Windows 7、1.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 上 在没有附加调试器的情况下作为控制台应用程序在发布模式下运行。

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

    【讨论】:

    • 所以 Math.Max 比 inline if 快??
    • 看起来是这样。至少在这个简单的例子中。就像其他人提到的那样,JITer 可能内联代码,可能更有效。即使 Math.Max() 稍微慢一点,这也是一个微不足道的差异。并且使用 Max() 使代码更清晰,更不容易出错。
    • 如果将 num1、num2 和 maxValue 转换为双精度数,结果会大不相同。作为 int,我的结果几乎与 @luisperezphd 相同,但是一旦转换为双打,Math.Max 就上升了 6 秒。这是一个巨大的差异,我运行了几次以确保。
    • @Firestrand 类似的事情发生在我身上,但我正在编译 32 位。编译为 64 位后结果相同。
    【解决方案5】:

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

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

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

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

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

    如果是更复杂的 Max - 比如处理向量,double []v; v.Max() 编译器可以利用高度优化的库代码,这比常规编译代码要快得多。

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

    【讨论】:

      【解决方案6】:

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

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

      【讨论】:

      • 您是否有任何参考资料支持函数不能跨程序集边界内联的想法?我有理由相信他们可以。
      • 可能是 NGEN 限制造成的污染,这些污染在 .NET 1.0 天被烧到了我的眼睑上。看起来有些函数现在可以跨程序集边界内联,特别是如果它们是属性访问器或使用值类型。参考文献:stackoverflow.com/questions/4660004/…blogs.msdn.com/b/vancem/archive/2008/08/19/…pcreview.co.uk/forums/…
      • 我使用这两种方法使用巨大的 for 循环进行了快速模拟,两者之间存在性能差异。另外,看看 IL,似乎 Math.Max() 没有内联。有没有办法强制或建议编译器内联某些函数?
      • 您对 IL 的看法如何?我很确定函数在调试时没有内联,在 ILDASM 反汇编时也没有内联。不,我不知道有任何内联提示可以诱使 jit'er 内联函数。我听过/读过关于内联的 .NET 核心人员非常坚决反对内联提示。内联是由 JITer 自行决定执行的内部优化。
      【解决方案7】:

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

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

      【讨论】:

        【解决方案8】:

        我会立即注意到的主要区别之一是为了可读性,据我所知,出于实现/性能的考虑,它们几乎等效。

        Math.Max(a,b)非常简单易懂,不管以前的编码知识。

        a&gt;b ? a : b 至少需要用户对三元运算符有所了解。

        如有疑问 - 提高可读性

        【讨论】:

        • 是的,几乎等价,这就是重点。 Math.Max 不需要任何知识?
        • 显然它需要一些知识。但我相信它比这里的替代方案要简单得多。
        • 你是对的,无论如何,我想说的是,有人宁愿写简短易读的代码,而有人更愿意编写更长的优化代码。
        • [TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")] 应该是内联的,所以一定要使用 Min/Max。
        • 需要一些知识?为什么有人不知道三元运算符是什么 - 甚至会尝试阅读您的代码?
        猜你喜欢
        • 2012-02-16
        • 1970-01-01
        • 2011-05-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-08-02
        • 2014-07-01
        相关资源
        最近更新 更多