35

我今天在做一个项目,发现自己在几个地方使用 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#,但我想不同语言的答案可能会有所不同,尽管我不确定哪些具有相似的概念。

4

8 回答 8

33

我马上注意到的主要区别之一是为了可读性,据我所知,为了实现/性能,它们几乎是等价的。

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

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

当有疑问时 - 追求可读性

于 2011-03-29T21:07:38.793 回答
27

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

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

  • 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 秒
于 2012-06-02T17:04:47.660 回答
16

如果陈述被认为是有益的

概括

形式的陈述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:1.2 秒最佳情况/1.2 秒最坏情况/1.2 秒平均情况
  • if (a > max) max = a:0.2 秒最佳情况 / 0.9 秒最坏情况 / 0.3 秒平均情况

因此,尽管 CPU 流水线很长,并且对分支造成了惩罚,但好的旧if语句显然是所有模拟数据集的赢家;在最好的情况下,它比 快 10 倍Math.Max,在最坏的情况下仍然快 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 > 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:0.011 秒最佳情况 / 0.005 秒最坏情况 / 0.018 秒平均情况
  • if (a > 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 优化。最后,我将 for 更改intia 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();
    }
  }
}
于 2014-05-06T10:12:59.360 回答
6

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

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

于 2011-03-29T21:09:12.520 回答
6

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

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

于 2011-03-29T23:09:54.707 回答
3

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

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

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

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

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

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

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

于 2014-03-19T09:55:15.387 回答
3

数学.Max(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;

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

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
于 2018-05-19T12:57:23.500 回答
0

进行手术;N 必须 >= 0

一般解决方案:

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

按速度排序:

慢:Math.Max (A) < (B) if-then 语句:快(比解决方案“A”快 3%)

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

N *= Math.Sign(1 + Math.Sign(N));
于 2019-01-15T11:23:16.563 回答