[c#] 三元运算符的速度是if-else块的两倍?


Answers

编辑:所有更改...见下文。

我无法在x64 CLR上重现您的结果,但我可以在x86上重现。 在x64上,我可以看到条件运算符和if / else之间的差异(小于10%),但比您看到的要小得多。

我做了以下潜在的改变:

  • 在控制台应用程序中运行
  • /o+ /debug-构建,并在调试器外部运行
  • 运行这两个代码段以便进行JIT操作,然后多次执行以获得更高的准确性
  • 使用Stopwatch

/platform:x64 (没有“忽略”行)的结果:

if/else with 1 iterations: 17ms
conditional with 1 iterations: 19ms
if/else with 1000 iterations: 17875ms
conditional with 1000 iterations: 19089ms

使用/platform:x86 (不含“忽略”行)的结果:

if/else with 1 iterations: 18ms
conditional with 1 iterations: 49ms
if/else with 1000 iterations: 17901ms
conditional with 1000 iterations: 47710ms

我的系统详情:

  • x64 i7-2720QM CPU @ 2.20GHz
  • 64位Windows 8
  • .NET 4.5

因此,与以前不同,我认为您看到了真正的差异 - 而这一切都与x86 JIT有关。 我不想确切地说明是什么导致了差异 - 如果我可以进入cordbg,我可能会稍后更新该帖子,并提供更多详细信息:)

有趣的是,如果没有先排序数组,我最终的测试需要大约4.5倍的时间,至少在x64上。 我的猜测是,这与分支预测有关。

码:

using System;
using System.Diagnostics;

class Test
{
    static void Main()
    {
        Random r = new Random(0);
        int[] array = new int[20000000];
        for(int i = 0; i < array.Length; i++)
        {
            array[i] = r.Next(int.MinValue, int.MaxValue);
        }
        Array.Sort(array);
        // JIT everything...
        RunIfElse(array, 1);
        RunConditional(array, 1);
        // Now really time it
        RunIfElse(array, 1000);
        RunConditional(array, 1000);
    }

    static void RunIfElse(int[] array, int iterations)
    {        
        long value = 0;
        Stopwatch sw = Stopwatch.StartNew();

        for (int x = 0; x < iterations; x++)
        {
            foreach (int i in array)
            {
                if (i > 0)
                {
                    value += 2;
                }
                else
                {
                    value += 3;
                }
            }
        }
        sw.Stop();
        Console.WriteLine("if/else with {0} iterations: {1}ms",
                          iterations,
                          sw.ElapsedMilliseconds);
        // Just to avoid optimizing everything away
        Console.WriteLine("Value (ignore): {0}", value);
    }

    static void RunConditional(int[] array, int iterations)
    {        
        long value = 0;
        Stopwatch sw = Stopwatch.StartNew();

        for (int x = 0; x < iterations; x++)
        {
            foreach (int i in array)
            {
                value += i > 0 ? 2 : 3;
            }
        }
        sw.Stop();
        Console.WriteLine("conditional with {0} iterations: {1}ms",
                          iterations,
                          sw.ElapsedMilliseconds);
        // Just to avoid optimizing everything away
        Console.WriteLine("Value (ignore): {0}", value);
    }
}
Question

我到处读到,三元运算符应该比其相当于if块的块更快,或者至少与其相当。

但是,我做了以下测试,发现情况并非如此:

Random r = new Random();
int[] array = new int[20000000];
for(int i = 0; i < array.Length; i++)
{
    array[i] = r.Next(int.MinValue, int.MaxValue);
}
Array.Sort(array);

long value = 0;
DateTime begin = DateTime.UtcNow;

foreach (int i in array)
{
    if (i > 0)
    {
        value += 2;
    }
    else
    {
        value += 3;
    }
    // if-else block above takes on average 85 ms

    // OR I can use a ternary operator:
    // value += i > 0 ? 2 : 3; // takes 157 ms
}
DateTime end = DateTime.UtcNow;
MessageBox.Show("Measured time: " + (end-begin).TotalMilliseconds + " ms.\r\nResult = " + value.ToString());

我的电脑花了85毫秒来运行上面的代码。 但如果我注释掉if - else块,并取消三元运算符的注释,则需要大约157 ms。

这是为什么发生?




编辑:

增加了一个可以用if-else语句而不是条件运算符来完成的例子。

在答案之前,请看看[ 哪个更快? ]在Lippert先生的博客上。 我认为埃尔松梅兹先生的答案是最正确的答案

我试图用高级编程语言提及一些我们应该记住的内容。

首先,我从来没有听说过条件运算符应该更快或者与C中的if-else语句具有相同的性能。

原因很简单,如果if-else语句没有任何操作:

if (i > 0)
{
    value += 2;
}
else
{
}

条件运算符的要求是任何一方都必须有一个值 ,并且在C中,它还要求:的两边具有相同的类型。 这只是使它不同于if-else语句。 因此,您的问题成为一个问题,询问机器代码的指令是如何生成的,以便改善性能。

对于条件运算符,语义上是:

无论表达如何评估,都有价值。

但是用if-else语句:

如果表达式被评估为true,则执行一些操作; 如果没有,做另一件事。

值不一定涉及if-else语句。 你的假设只有在优化时才有可能。

证明它们之间差异的另一个例子如下所示:

var array1=new[] { 1, 2, 3 };
var array2=new[] { 5, 6, 7 };

if(i>0)
    array1[0]=4;
else
    array2[0]=4;

上面的代码编译,但是,替换if-else语句与条件运算符只是不会编译:

var array1=new[] { 1, 2, 3 };
var array2=new[] { 5, 6, 7 };
(i>0?array1[0]:array2[0])=4; // incorrect usage 

当你做同样的事情时,条件操作符和if-else语句的概念是相同的, 在C中 ,条件操作符可能会更快,因为C更接近平台组装。

对于您提供的原始代码,条件运算符在foreach循环中使用,这会弄乱一切以查看它们之间的差异。 所以我提出了以下代码:

public static class TestClass {
    public static void TestConditionalOperator(int i) {
        long value=0;
        value+=i>0?2:3;
    }

    public static void TestIfElse(int i) {
        long value=0;

        if(i>0) {
            value+=2;
        }
        else {
            value+=3;
        }
    }

    public static void TestMethod() {
        TestConditionalOperator(0);
        TestIfElse(0);
    }
}

以下是IL的两个版本的优化与非优化。 由于它们很长,我使用一个图像来显示,右侧是最优化的一个:

(点击查看全尺寸图片。)

在这两种版本的代码中,条件运算符的IL看起来都比if-else语句短,并且仍然怀疑最终生成的机器代码。 以下是两种方法的说明,前者图像未优化,后者为优化后的图像:

  • 非优化指令:(点击查看全尺寸图片。)

  • 优化说明:(点击查看全尺寸图片。)

在后者中,黄色块是仅在i<=0时才执行的代码,当i>0时是蓝色块。 在任一版本的指令中,if-else语句都较短。

请注意,对于不同的指示,[ CPI ]不一定相同。 从逻辑上讲,对于相同的指令,更多的指令需要更长的周期。 但是如果指令获取时间和管道/缓存也被考虑在内,那么真正的总执行时间取决于处理器。 处理器还可以预测分支。

现代处理器拥有更多的核心,事情可能会更复杂。 如果您是英特尔处理器用户,则可能需要看看[ 英特尔®64和IA-32架构优化参考手册 ]。

我不知道是否有硬件实现的CLR,但如果是的话,那么使用条件运算符可能会更快,因为IL显然较小。

注意:所有机器代码都是x86的。




看看生成的IL,那里的操作比if / else语句少16个(复制和粘贴@ JonSkeet的代码)。 但是,这并不意味着它应该是一个更快的过程!

为了总结IL中的差异,if / else方法转换为与C#代码读取(在分支内执行加法)几乎相同,而条件代码将2或3加载到堆栈上(取决于值)和然后将其添加到条件之外的值。

另一个区别是使用分支指令。 if / else方法使用brtrue(如果为true,则为分支)跳过第一个条件,并使用无条件分支从if语句中的第一个跳出。 条件代码使用bgt(分支如果大于)而不是brtrue,这可能是一个较慢的比较。

另外(刚刚阅读了关于分支预测的内容),分支变小可能会导致性能损失。 条件分支在分支中只有1条指令,但if / else有7条。这也解释了为什么使用long和int有区别,因为改变为int会将if / else分支中的指令数量减少1 (使预读更少)




生成的汇编代码将告诉故事:

a = (b > c) ? 1 : 0;

产生:

mov  edx, DWORD PTR a[rip]
mov  eax, DWORD PTR b[rip]
cmp  edx, eax
setg al

鉴于:

if (a > b) printf("a");
else printf("b");

产生:

mov edx, DWORD PTR a[rip]
mov eax, DWORD PTR b[rip]
cmp edx, eax
jle .L4
    ;printf a
jmp .L5
.L4:
    ;printf b
.L5:

因此, 如果您正在寻找真/假,则三元数可以更短更快,因为使用更少的指令并且不会跳转。 如果您使用的值不是1和0,您将得到与if / else相同的代码,例如:

a = (b > c) ? 2 : 3;

产生:

mov edx, DWORD PTR b[rip]
mov eax, DWORD PTR c[rip]
cmp edx, eax
jle .L6
    mov eax, 2
jmp .L7
.L6:
    mov eax, 3
.L7:

这与if / else相同。




Links