1

有 3 个代码做同样的事情,但是它们在 x64 版本中的性能不同。

我想这是因为Branch Prediction。任何人都可以进一步详细说明?

有条件的: 需要 41 毫秒

for (int j = 0; j < 10000; j++)
{
    ret = (j * 11 / 3 % 5) + (ret % 11 == 4 ? 2 : 1);
}

正常: 需要 51 毫秒

for (int j = 0; j < 10000; j++)
{
    if (ret % 11 == 4)
    {
        ret = 2 + (j * 11 / 3 % 5);
    }
    else
    {
        ret = 1 + (j * 11 / 3 % 5);
    }
}

缓存: 需要 44 毫秒

for (int j = 0; j < 10000; j++)
{
    var tmp = j * 11 / 3 % 5;
    if (ret % 11 == 4)
    {
        ret = 2 + tmp;
    }
    else
    {
        ret = 1 + tmp;
    }
}
4

2 回答 2

2

编辑 3 如果我在修复了时序错误的情况下返回原始测试,我会得到与此类似的输出。

有条件的耗时 67ms

正常耗时 83ms

缓存耗时 73ms

这表明三元/条件运算符在for循环中可以稍微快一些。鉴于先前的发现,当逻辑分支被抽象出循环时,该if块优于三元/条件运算符,我们可以推断,当迭代使用条件/三元运算符时,编译器能够进行额外的优化,至少在一些案例。

我不清楚为什么这些优化不适用于或不适用于标准if块。实际差异相当小,我认为这是一个有争议的问题。

编辑 2

此处突出显示的测试代码中有一个明显的错误

Stopwatch调用之间没有重置,当我使用而Stopwatch.Restart不是Stopwatch.Start迭代到 1000000000 时,我得到了结果

条件耗时 22404ms

正常耗时 21403ms

这更像是我所期待的结果,并被提取的 CIL 所证实。因此,当与周围代码隔离时,“正常”if实际上比三元\条件运算符快一点。

编辑

经过下面概述的调查后,我建议使用逻辑条件在两个常量或文字之间进行选择时,条件/三元运算符可能标准if块快得多。在我的测试中,它的速度大约是原来的两倍。

但是我不能完全弄清楚为什么。正常生成的 CILif更长,但对于这两个函数,平均执行路径似乎是 6 行,包括 3 次加载和 1 或 2 次跳转,有什么想法吗?.


使用此代码,

using System.Diagnostics;

class Program
{
    static void Main()
    {
        var stopwatch = new Stopwatch();

        var conditional = Conditional(10);
        var normal = Normal(10);
        var cached = Cached(10);

        if (new[] { conditional, normal }.Any(x => x != cached))
        {
            throw new Exception();
        }

        stopwatch.Start();
        conditional = Conditional(10000000);
        stopWatch.Stop();
        Console.WriteLine(
            "Conditional took {0}ms", 
            stopwatch.ElapsedMilliseconds);

        ////stopwatch.Start(); incorrect
        stopwatch.Restart();
        normal = Normal(10000000);
        stopWatch.Stop();
        Console.WriteLine(
            "Normal took {0}ms", 
            stopwatch.ElapsedMilliseconds);

        ////stopwatch.Start(); incorrect
        stopwatch.Restart();
        cached = Cached(10000000);
        stopWatch.Stop();
        Console.WriteLine(
            "Cached took {0}ms", 
            stopwatch.ElapsedMilliseconds);

        if (new[] { conditional, normal }.Any(x => x != cached))
        {
            throw new Exception();
        }

        Console.ReadKey();
    }

    static int Conditional(int iterations)
    {
        var ret = 0;
        for (int j = 0; j < iterations; j++)
        {
            ret = (j * 11 / 3 % 5) + (ret % 11 == 4 ? 2 : 1);
        }

        return ret;
    }

    static int Normal(int iterations)
    {
        var ret = 0;
        for (int j = 0; j < iterations; j++)
        {
            if (ret % 11 == 4)
            {
                ret = 2 + (j * 11 / 3 % 5);
            }
            else
            {
                ret = 1 + (j * 11 / 3 % 5);
            }
        }

        return ret;
    }

    static int Cached(int iterations)
    {
        var ret = 0;
        for (int j = 0; j < iterations; j++)
        {
            var tmp = j * 11 / 3 % 5;
            if (ret % 11 == 4)
            {
                ret = 2 + tmp;
            }
            else
            {
                ret = 1 + tmp;
            }
        }

        return ret;
    }
}

在 x64 发布模式下编译,经过优化,无需附加调试器即可运行。我得到这个输出,

有条件的耗时 65ms

正常耗时 148ms

缓存耗时 217ms

并且没有抛出异常。


使用 ILDASM 反汇编代码,我可以确认这三种方法的 CIL 不同,方法的代码Conditional稍微短一些。


要真正回答“为什么”的问题,我需要了解编译器的代码。我可能需要知道为什么编译器是这样写的。


您可以进一步分解它,以便您实际上只比较逻辑函数而忽略所有其他活动。

static int Conditional(bool condition, int value)
{
    return value + (condition ? 2 : 1);
}

static int Normal(bool condition, int value)
{
    if (condition)
    {
        return 2 + value;
    }

    return 1 + value;
}

你可以用它迭代

static int Looper(int iterations, Func<bool, int, int> operation)
{
    var ret = 0;
    for (var j = 0; j < iterations; j++)
    {
        var condition = ret % 11 == 4;
        var value = ((j * 11) / 3) % 5;
        ret = operation(condition, value);
    }
}

该测试仍然显示出性能差异,但现在以另一种方式简化了下面的 IL。

... Conditional ...
{
     : ldarg.1      // push second arg
     : ldarg.0      // push first arg
     : brtrue.s T   // if first arg is true jump to T
     : ldc.i4.1     // push int32(1)
     : br.s F       // jump to F
    T: ldc.i4.2     // push int32(2)
    F: add          // add either 1 or 2 to second arg
     : ret          // return result
}

... Normal ...
{
     : ldarg.0      // push first arg
     : brfalse.s F  // if first arg is false jump to F
     : ldc.i4.2     // push int32(2)
     : ldarg.1      // push second arg
     : add          // add second arg to 2
     : ret          // return result
    F: ldc.i4.1     // push int32(1)
     : ldarg.1      // push second arg
     : add          // add second arg to 1
     : ret          // return result
}
于 2012-09-17T11:37:56.867 回答
1

有 3 个代码做同样的事情,但是它们的性能不同

这并不奇怪,不是吗?写一些不同的东西,你会得到不同的时机。

我想这是因为分支预测。

这可以部分解释为什么第一个片段更快。但请注意,?:它仍在分支。
另一件需要注意的是,它只是一个大表达式,是优化器的理想领域。

问题是您不能像这样查看代码并得出某个运算符更快/更慢的结论。周围的代码至少同样重要。

于 2012-09-17T11:55:42.300 回答