44

为什么注释掉这个 for 循环的前两行并取消注释第三行会导致 42% 的加速?

int count = 0;
for (uint i = 0; i < 1000000000; ++i) {
    var isMultipleOf16 = i % 16 == 0;
    count += isMultipleOf16 ? 1 : 0;
    //count += i % 16 == 0 ? 1 : 0;
}

时序背后是截然不同的汇编代码:循环中有 13 条与 7 条指令。该平台是运行 .NET 4.0 x64 的 Windows 7。启用了代码优化,并且测试应用在 VS2010 之外运行。[更新: 复制项目,用于验证项目设置。]

消除中间布尔值是一项基本优化,是我 1980 年代Dragon Book中最简单的优化之一。在生成 CIL 或 JITing x64 机器代码时,如何优化没有得到应用?

是否有“真正的编译器,我希望你优化这段代码,请”开关?虽然我对过早优化类似于爱钱的观点表示同情,但我可以看到尝试分析一个复杂算法时的挫败感,这种算法在其例程中散布着这样的问题。您将通过热点工作,但没有任何迹象表明可以通过手动调整我们通常认为编译器理所当然的内容来大大改善更广泛的温暖区域。我当然希望我在这里遗漏了一些东西。

更新: x86 也存在速度差异,但取决于方法的即时编译顺序。请参阅为什么 JIT 顺序会影响性能?

汇编代码(根据要求):

    var isMultipleOf16 = i % 16 == 0;
00000037  mov         eax,edx 
00000039  and         eax,0Fh 
0000003c  xor         ecx,ecx 
0000003e  test        eax,eax 
00000040  sete        cl 
    count += isMultipleOf16 ? 1 : 0;
00000043  movzx       eax,cl 
00000046  test        eax,eax 
00000048  jne         0000000000000050 
0000004a  xor         eax,eax 
0000004c  jmp         0000000000000055 
0000004e  xchg        ax,ax 
00000050  mov         eax,1 
00000055  lea         r8d,[rbx+rax] 
    count += i % 16 == 0 ? 1 : 0;
00000037  mov         eax,ecx 
00000039  and         eax,0Fh 
0000003c  je          0000000000000042 
0000003e  xor         eax,eax 
00000040  jmp         0000000000000047 
00000042  mov         eax,1 
00000047  lea         edx,[rbx+rax] 
4

5 回答 5

9

问题应该是“为什么我在我的机器上看到这样的差异?”。我无法重现如此巨大的速度差异,并怀疑您的环境存在特定的问题。很难说它可能是什么。可以是您之前设置并忘记的一些(编译器)选项。

我已经创建了一个控制台应用程序,在发布模式(x86)下重建并在 VS 之外运行。结果几乎相同,两种方法均为 1.77 秒。这是确切的代码:

static void Main(string[] args)
{
    Stopwatch sw = new Stopwatch();
    sw.Start();
    int count = 0;

    for (uint i = 0; i < 1000000000; ++i)
    {
        // 1st method
        var isMultipleOf16 = i % 16 == 0;
        count += isMultipleOf16 ? 1 : 0;

        // 2nd method
        //count += i % 16 == 0 ? 1 : 0;
    }

    sw.Stop();
    Console.WriteLine(string.Format("Ellapsed {0}, count {1}", sw.Elapsed, count));
    Console.ReadKey();
}

请任何有 5 分钟时间的人复制代码、重建、在 VS 外部运行并在此答案的评论中发布结果。我想避免说“它适用于我的机器”。

编辑

可以肯定的是,我创建了一个64 位Winforms 应用程序,结果与问题中的结果相似——第一种方法(1.57 秒)比第二种方法(1.05 秒)慢。我观察到的差异是 33% - 仍然很多。似乎 .NET4 64 位 JIT 编译器中存在错误。

于 2012-04-30T09:46:59.077 回答
4

我无法谈论 .NET 编译器,或者它的优化,甚至它何时执行它的优化。

但是在这种特定情况下,如果编译器将该布尔变量折叠到实际语句中,并且您要尝试调试此代码,则优化后的代码将与编写的代码不匹配。您将无法单步执行 isMulitpleOf16 分配并检查它的值。

这只是可能关闭优化的一个例子。可能还有其他人。优化可能发生在代码的加载阶段,而不是来自 CLR 的代码生成阶段。

现代运行时非常复杂,特别是如果您在运行时投入 JIT 和动态优化。我很感激代码有时完全按照它所说的去做。

于 2012-04-29T04:20:10.837 回答
3

这是 .NET Framework 中的一个错误。

好吧,我真的只是在猜测,但我在Microsoft Connect上提交了一份错误报告,看看他们怎么说。在微软删除该报告后,我在 GitHub 上的roslyn项目上重新提交了它。

更新:微软已将该问题移至coreclr项目。从对该问题的评论来看,称其为错误似乎有点强;这更像是一个缺失的优化。

于 2012-04-30T11:39:18.830 回答
2

我认为这与您的另一个问题有关。当我如下更改您的代码时,多行版本获胜。

哎呀,仅在 x86 上。在 x64 上,多行是最慢的,并且条件轻松地击败了它们。

class Program
{
    static void Main()
    {
        ConditionalTest();
        SingleLineTest();
        MultiLineTest();
        ConditionalTest();
        SingleLineTest();
        MultiLineTest();
        ConditionalTest();
        SingleLineTest();
        MultiLineTest();
    }

    public static void ConditionalTest()
    {
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        int count = 0;
        for (uint i = 0; i < 1000000000; ++i) {
            if (i % 16 == 0) ++count;
        }
        stopwatch.Stop();
        Console.WriteLine("Conditional test --> Count: {0}, Time: {1}", count, stopwatch.ElapsedMilliseconds);
    }

    public static void SingleLineTest()
    {
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        int count = 0;
        for (uint i = 0; i < 1000000000; ++i) {
            count += i % 16 == 0 ? 1 : 0;
        }
        stopwatch.Stop();
        Console.WriteLine("Single-line test --> Count: {0}, Time: {1}", count, stopwatch.ElapsedMilliseconds);
    }

    public static void MultiLineTest()
    {
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        int count = 0;
        for (uint i = 0; i < 1000000000; ++i) {
            var isMultipleOf16 = i % 16 == 0;
            count += isMultipleOf16 ? 1 : 0;
        }
        stopwatch.Stop();
        Console.WriteLine("Multi-line test  --> Count: {0}, Time: {1}", count, stopwatch.ElapsedMilliseconds);
    }
}
于 2012-05-02T04:01:41.323 回答
1

我倾向于这样想:从事编译器工作的人每年只能做这么多事情。如果那时他们可以实现 lambdas 或许多经典优化,我会投票给 lambdas。C# 是一种在代码读取和编写工作方面而不是在执行时间方面高效的语言。

因此,团队专注于最大化读/写效率的特性是合理的,而不是在某个极端情况下(其中可能有数千个)的执行效率。

我相信最初的想法是 JITter 会做所有的优化。不幸的是,JITting 需要相当长的时间,任何高级优化都会使情况变得更糟。所以这并没有像人们希望的那样成功。

我发现在 C# 中编写真正快速的代码的一件事是,在您提到的任何优化都会产生影响之前,您经常会遇到严重的 GC 瓶颈。就像您分配数百万个对象一样。C# 在避免成本方面给您留下的余地很少:您可以改用结构数组,但相比之下,生成的代码真的很难看。我的观点是,关于 C# 和 .NET 的许多其他决定使得此类特定优化不如在 C++ 编译器之类的东西中有价值。哎呀,他们甚至放弃了 NGEN 中特定于 CPU 的优化,用性能换取程序员(调试器)的效率。

说了这么多,我喜欢C#,它实际上利用了自 1990 年代以来 C++ 使用的优化。只是不以牺牲诸如异步/等待之类的功能为代价。

于 2012-05-06T19:23:06.143 回答