【问题标题】:Why does adding local variables make .NET code slower为什么添加局部变量会使 .NET 代码变慢
【发布时间】:2012-05-09 07:27:35
【问题描述】:

为什么注释掉这个 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 之外运行。 [更新:Repro project,用于验证项目设置。]

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

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

更新: x86 也存在速度差异,但取决于方法的即时编译顺序。见Why does JIT order affect performance?

汇编代码(根据要求):

    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] 

【问题讨论】:

  • 我很想看看不同的汇编代码。可以发一下吗?
  • 你测试过 bool isMultipleOf16 = ...吗?
  • @David.Chu.ca - 这不会有什么不同 - var 是“编译器,请推断这个变量的类型,并假装我写了那个”。在这种情况下,它会为自己推断出bool
  • @EdwardBrey:既然你是在 Debug 模式下这样做的,那么所有的赌注都没有了
  • @EdwardBrey:我目前找不到源,但我相信如果您连接了调试器,抖动和/或其他优化器设置会有所不同完全 (也就是说,如果您从 Visual Studio 运行,即使您在“发布”模式下编译)。尝试从命令行(而不是 VS)运行您的代码,看看会发生什么。

标签: c# .net performance compiler-construction jit


【解决方案1】:

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

我已经创建了一个控制台应用程序,在发布模式 (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 外部运行并将结果在 cmets 中发布到此答案。我不想说“它可以在我的机器上运行”。

编辑

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

【讨论】:

  • 第一种方法:1.8736291s,第二种方法:1.8566318s 在我的机器上,用 Release (x86) 重建,在 VS 外部运行,使用完全相同的代码。
  • 您需要对count 做一些事情(例如将其包含在您的WriteLine 语句中)。否则优化器会做一些选择性的优化,随着时间的变化而变化。
  • @EdwardBrey,我只能在 64 位应用程序中重现它
  • @EdwardBrey 运行您的测试让我在两个平台上的 Multiline 版本上执行速度变慢。但是如果我更改测试以使其运行 4xMultiline 然后 4xSingleline 在 x86 上没有速度差异(x64 不受影响)
  • @Maciej +1 进行了精彩的观察。看起来最重要的是首先调用哪个方法。这似乎几乎可以肯定是由于 JIT 订单。但是为什么 JIT 顺序很重要是令人困惑的。我用您的观察提示的new question 的链接更新了问题正文。
【解决方案2】:

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

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

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

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

【讨论】:

  • 当我看到汇编代码时,我想知道是否以某种方式禁用了优化。我通过在 VS2010 调试器中的断点处停止并使用反汇编窗口来获得汇编代码(而我通过在没有调试器的情况下运行获得的时间)。作为测试,我打开了“工具”>“选项”>“调试”>“常规”>“抑制模块上的 JIT 优化”设置。果然,汇编代码变得更大了。
  • 在原生 C++ 世界中,启用优化时断点和代码顺序出现奇怪现象是完全正常的。同样,像isMultipleOf16 这样的变量在调试器中并不总是可用的。这就是为什么有调试模式。归根结底,我们仍在运行相同的机器代码,所以我不明白为什么 CLR 会让事情变得不同。事实上,当 C# 中发生异常时,即使在调试模式下,我有时也会收到一条关于变量值被“优化掉”的消息,即使在调试模式下也是如此。
  • +1 表示调试设置会影响代码生成。
【解决方案3】:

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

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

更新: Microsoft 已将此问题移至 coreclr 项目。从问题上的 cmets 来看,称其为 bug 似乎有点强;这更像是一个缺失的优化。

【讨论】:

  • 如果每次程序员告诉我“我的代码不起作用。它一定是框架(或编译器或运行时库等)中的错误”时,我都有一美元,并且后来发现是他自己代码的bug,我可以退了。
  • @Jim:我自己已经看过很多次了。我所知道的最好的解药是尽可能从根本上隔离行为并为供应商提供重现。并保持观望态度。这就是我们所处的位置。
  • @TankorSmash 也许微软在将代码移至 GitHub 时将其删除。它不再出现在我的 Connect 仪表板上。我报告的许多问题似乎都消失了。某种通知会很好。我将问题重新提交到 GitHub 项目并相应地更新了答案。
【解决方案4】:

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

哎呀,仅在 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);
    }
}

【讨论】:

  • 我更新了repro project 以包含一个“if”测试。我制作了单行和多行变体。在 x64 和 x86 上,单行版本更快(当没有 alignment penalty 时)。我还制作了一个在循环中根本没有任何条件代码的变体(只是一点数学)。它们在 x86 上并驾齐驱(我还没有检查过组件)。在 x64 上,带有局部变量的版本运行得更快!局部变量的重要性仍然令人惊讶。
【解决方案5】:

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

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

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

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

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

【讨论】:

  • 我会非常小心不要过多阅读 2005 年和 .net 1.1 的文章!过去 7 年发生了很大变化。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-11-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多