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

时间:2012-04-29 03:17:56

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

为什么要注释掉这个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或JITing 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] 

5 个答案:

答案 0 :(得分: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编译器中存在错误。

答案 1 :(得分:4)

我不能谈论.NET编译器或其优化,甚至不能说它执行优化。

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

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

现代运行时非常复杂,特别是如果你在运行时投入JIT和动态优化。感谢代码完成它所说的话。

答案 2 :(得分:3)

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

嗯,我真的只是猜测,但我提交了一个关于 Microsoft Connect 的错误报告,看看他们说了什么。在Microsoft删除该报告后,我在GitHub上的roslyn项目中重新提交了该报告。

更新:Microsoft已将问题移至coreclr项目。从关于这个问题的评论来看,把它称为bug似乎有点强烈;它更像是一种缺失的优化。

答案 3 :(得分:2)

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

oops,仅限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);
    }
}

答案 4 :(得分:1)

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

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

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

我发现在C#中编写真正快速代码的一件事是,在您提到的任何优化之前,您经常遇到严重的GC瓶颈会产生影响。就像你分配数百万个对象一样。 C#在避免成本方面给你留下的很少:你可以使用结构数组,但结果代码相比之下真的很难看。我的观点是,关于C#和.NET的许多其他决策使得这样的特定优化不如它们在C ++编译器中那样值得。哎呀,他们甚至dropped the CPU-specific optimizations in NGEN,为程序员(调试器)效率交易性能。

说完这一切之后,我喜欢 C#实际上利用了自20世纪90年代以来C ++使用的优化。不要以牺牲async / await等功能为代价。