为什么添加局部变量会使 .NET 代码变慢

本文关键字:代码 NET 添加 局部变量 为什么 | 更新日期: 2023-09-27 18:32:29

为什么注释掉这个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 或 JIT 处理 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] 

为什么添加局部变量会使 .NET 代码变慢

问题应该是"为什么我在机器上看到如此不同的?我无法重现如此巨大的速度差异,并怀疑您的环境有特定的东西。很难说它会是什么。可能是您前段时间设置并忘记它们的一些(编译器(选项。

我已经创建了一个控制台应用程序,在发布模式 (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 编译器。

我无法与 .NET 编译器或其优化交谈,甚至无法与它何时执行优化。

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

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

现代运行时非常复杂,尤其是在运行时引入 JIT 和动态优化时。我很感激代码有时会按照它所说的去做。

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

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

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

我认为

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

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

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

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

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

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

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