为啥即使生成的机器代码几乎相同,C# 的速度却是 C++ 的两倍?

Posted

技术标签:

【中文标题】为啥即使生成的机器代码几乎相同,C# 的速度却是 C++ 的两倍?【英文标题】:Why C# is twice as slow as C++ even though the generated machine code is nearly identical?为什么即使生成的机器代码几乎相同,C# 的速度却是 C++ 的两倍? 【发布时间】:2019-11-22 22:54:30 【问题描述】:

此代码由 .NET Core 3.0 JIT 生成,用于我手动矢量化的 C# 代码:

00007FFE6C7D2103  vmovdqu     xmm5,xmmword ptr [rcx]  
00007FFE6C7D2107  vmovdqu     xmm6,xmmword ptr [rcx+10h]  
00007FFE6C7D210C  vmovdqu     xmm7,xmmword ptr [rcx+20h]  
00007FFE6C7D2111  vmovdqu     xmm8,xmmword ptr [rcx+30h]  
00007FFE6C7D2116  vpand       xmm9,xmm5,xmm0  
00007FFE6C7D211A  vpand       xmm10,xmm6,xmm0  
00007FFE6C7D211E  vpackusdw   xmm9,xmm9,xmm10  
00007FFE6C7D2123  vpslldq     xmm9,xmm9,1  
00007FFE6C7D2129  vpand       xmm10,xmm5,xmm1  
00007FFE6C7D212D  vpand       xmm11,xmm6,xmm1  
00007FFE6C7D2131  vpackusdw   xmm10,xmm10,xmm11  
00007FFE6C7D2136  vpsrldq     xmm5,xmm5,1  
00007FFE6C7D213B  vpsrldq     xmm6,xmm6,1  
00007FFE6C7D2140  vpand       xmm5,xmm5,xmm1  
00007FFE6C7D2144  vpand       xmm6,xmm6,xmm1  
00007FFE6C7D2148  vpackusdw   xmm5,xmm5,xmm6  
                var low = brightness( r, g, b, redMul, greenMul, blueMul );
00007FFE6C7D214D  vpmulhuw    xmm9,xmm9,xmm2  
00007FFE6C7D2151  vpmulhuw    xmm10,xmm10,xmm3  
00007FFE6C7D2155  vpmulhuw    xmm5,xmm5,xmm4  
00007FFE6C7D2159  vpaddusw    xmm6,xmm9,xmm10  
00007FFE6C7D215E  vpaddusw    xmm5,xmm6,xmm5  
00007FFE6C7D2162  vpsrlw      xmm5,xmm5,8  
00007FFE6C7D2167  vpand       xmm6,xmm7,xmm0  
00007FFE6C7D216B  vpand       xmm9,xmm8,xmm0  
00007FFE6C7D216F  vpackusdw   xmm6,xmm6,xmm9  
00007FFE6C7D2174  vpslldq     xmm9,xmm6,1  
00007FFE6C7D2179  vpand       xmm6,xmm7,xmm1  
00007FFE6C7D217D  vpand       xmm10,xmm8,xmm1  
00007FFE6C7D2181  vpackusdw   xmm10,xmm6,xmm10  
00007FFE6C7D2186  vpsrldq     xmm6,xmm7,1  
00007FFE6C7D218B  vpsrldq     xmm7,xmm8,1  
00007FFE6C7D2191  vpand       xmm6,xmm6,xmm1  
00007FFE6C7D2195  vpand       xmm7,xmm7,xmm1  
00007FFE6C7D2199  vpackusdw   xmm6,xmm6,xmm7  
                var hi = brightness( r, g, b, redMul, greenMul, blueMul );
00007FFE6C7D219E  vpmulhuw    xmm7,xmm9,xmm2  
00007FFE6C7D21A2  vpmulhuw    xmm8,xmm10,xmm3  
00007FFE6C7D21A6  vpmulhuw    xmm6,xmm6,xmm4  
00007FFE6C7D21AA  vpaddusw    xmm7,xmm7,xmm8  
00007FFE6C7D21AF  vpaddusw    xmm6,xmm7,xmm6  
00007FFE6C7D21B3  vpsrlw      xmm6,xmm6,8  
00007FFE6C7D21B8  vpackuswb   xmm5,xmm5,xmm6  
                Sse2.Store( dst, bytes );
00007FFE6C7D21BC  vmovdqu     xmmword ptr [rdx],xmm5  

                src += 64;
00007FFE6C7D21C0  add         rcx,40h  
                dst += 16;
00007FFE6C7D21C4  add         rdx,10h  
            while( src < srcEnd )
00007FFE6C7D21C8  cmp         rcx,rax  
00007FFE6C7D21CB  jb          00007FFE6C7D2103 

此代码由 VC++ 2015 在编译我的手动矢量化 C++ 时生成。

    
        VecInteger r, g, b;

        loadRgb( src, r, g, b );
00007FF735AD11C0  vmovdqu     xmm6,xmmword ptr [rcx-10h]
00007FF735AD11C5  vmovdqu     xmm7,xmmword ptr [rcx-20h]

        loadRgb( src + 2, r, g, b );
00007FF735AD11CA  vmovdqu     xmm9,xmmword ptr [rcx]
00007FF735AD11CE  vmovdqu     xmm8,xmmword ptr [rcx+10h]
    
        VecInteger r, g, b;

        loadRgb( src, r, g, b );
00007FF735AD11D3  vpand       xmm3,xmm10,xmm6  
00007FF735AD11D7  vpand       xmm1,xmm11,xmm6  
00007FF735AD11DB  vpand       xmm0,xmm11,xmm7  
00007FF735AD11DF  vpackusdw   xmm1,xmm0,xmm1  
00007FF735AD11E4  vpslldq     xmm2,xmm1,1  
        const auto low = brightness( r, g, b );
00007FF735AD11E9  vpmulhuw    xmm4,xmm2,xmm12  
00007FF735AD11EE  vpand       xmm0,xmm10,xmm7  
00007FF735AD11F2  vpackusdw   xmm1,xmm0,xmm3  
        const auto low = brightness( r, g, b );
00007FF735AD11F7  vpmulhuw    xmm2,xmm1,xmm13  
00007FF735AD11FC  vpaddusw    xmm5,xmm4,xmm2  
    
        VecInteger r, g, b;

        loadRgb( src, r, g, b );
00007FF735AD1200  vpsrldq     xmm0,xmm6,1  
00007FF735AD1205  vpand       xmm3,xmm0,xmm10  
00007FF735AD120A  vpsrldq     xmm1,xmm7,1  
00007FF735AD120F  vpand       xmm2,xmm1,xmm10  
00007FF735AD1214  vpackusdw   xmm0,xmm2,xmm3  
        const auto low = brightness( r, g, b );
00007FF735AD1219  vpmulhuw    xmm3,xmm0,xmm14  
00007FF735AD121E  vpaddusw    xmm1,xmm5,xmm3  
00007FF735AD1222  vpsrlw      xmm6,xmm1,8  

        loadRgb( src + 2, r, g, b );
00007FF735AD1227  vpand       xmm2,xmm11,xmm8  
00007FF735AD122C  vpand       xmm0,xmm11,xmm9  
00007FF735AD1231  vpackusdw   xmm1,xmm0,xmm2  
00007FF735AD1236  vpslldq     xmm2,xmm1,1  
        const auto hi = brightness( r, g, b );
00007FF735AD123B  vpmulhuw    xmm4,xmm2,xmm12  

        loadRgb( src + 2, r, g, b );
00007FF735AD1240  vpand       xmm0,xmm10,xmm9  
00007FF735AD1245  vpand       xmm3,xmm10,xmm8  
00007FF735AD124A  vpackusdw   xmm1,xmm0,xmm3  
        const auto hi = brightness( r, g, b );
00007FF735AD124F  vpmulhuw    xmm2,xmm1,xmm13  
00007FF735AD1254  vpaddusw    xmm5,xmm4,xmm2  

        loadRgb( src + 2, r, g, b );
00007FF735AD1258  vpsrldq     xmm1,xmm9,1  
00007FF735AD125E  vpand       xmm2,xmm1,xmm10  
00007FF735AD1263  vpsrldq     xmm0,xmm8,1  
00007FF735AD1269  vpand       xmm3,xmm0,xmm10  
00007FF735AD126E  vpackusdw   xmm0,xmm2,xmm3  
        const auto hi = brightness( r, g, b );
00007FF735AD1273  vpmulhuw    xmm3,xmm0,xmm14  
00007FF735AD1278  vpaddusw    xmm1,xmm5,xmm3  
00007FF735AD127C  vpsrlw      xmm2,xmm1,8  

        src += 4;
00007FF735AD1281  lea         rcx,[rcx+40h]  

        const auto bytes = packus_epi16( low, hi );
00007FF735AD1285  vpackuswb   xmm0,xmm6,xmm2  
    VecInteger* dest = (VecInteger*)destinationBytes;

    while( src < srcEnd )
00007FF735AD1289  lea         rax,[rcx-20h]  
        storeu_all( dest, bytes );
00007FF735AD128D  vmovdqu     xmmword ptr [rdx],xmm0  
        dest++;
00007FF735AD1291  lea         rdx,[rdx+10h]  
00007FF735AD1295  cmp         rax,r8  
00007FF735AD1298  jb          Sse::convertToGrayscale+80h (07FF735AD11C0h)  

上面两个sn-ps都只包含程序的主循环。如您所见,它们的指令几乎相同,但 C# 的速度是 C++ 的两倍。

具体来说,当用 511M 像素进行测试时,我的 PC (AMD Ryzen 5 3600) C++ 代码需要 221 毫秒,C# 代码需要 410 毫秒。

为什么?


有关 C# 源代码,请参阅 Why is C# twice as slow as C++ even though the generated machine code is nearly identical?。

C++源代码:https://github.com/Const-me/IntelIntrinsics/blob/master/CppDemo/brightness.cpphttps://github.com/Const-me/IntelIntrinsics/blob/master/CppDemo/brightness.inl

【问题讨论】:

堆栈溢出问题必须是独立的。请将相关代码复制到您的问题中。 @fuz 我刚刚做了。 你如何测试这个...你是如何从你的基准测试范式的方程中消除抖动的? 所以您是说您只是在运行指令(测试时不涉及 .net 应用程序)?我问这样我们就可以取消 .net 本身,只关注程序集。您还需要告诉我们您是如何测试的,如何获得基准测试结果。 您可能应该链接包含您的 C# 代码的 RGB -> 灰度 SO 问题,并且至少链接从中编译的原始 C++。即使我想在自己的 CPU 上分析这些指令,问题中的表格也不适合复制/粘贴到任何内容中。如果没有像 brightness() 这样的 C 函数定义,很难理解这么多代码。例如为什么在你的 C 输出中有一对 vpsrldq by 1 字节指令,然后是 vpand? .NET 输出的效率是否明显低于您所注意到的某些方面? 【参考方案1】:

原因是 JIT 开销。在对 .NET 代码进行基准测试时,您应该始终放弃第一个度量,因为它包括运行时从 IL 生成 x86 代码所花费的时间。

这是我测量了 3 次而不是 1 次(对于 5.11 亿像素)后测试应用打印的内容:

#1 391.1885 ms, #2 216.985 ms, #3 235.5549 ms

源码:https://gist.github.com/Const-me/0f0c283a0b998aa9977550d85fa33958

这 ~220 毫秒非常接近等效 C++ 代码的性能。所以 C# SIMD 毕竟没那么糟糕。

【讨论】:

对于未来的读者应该注意的是,性能基准是一件很难实现的事情,应该使用能够预热和运行多个解析以及排除抖动等问题的适当工具和延迟加载程序集(除其他外),以​​及使用 StopWatch 的限制 同意。这些天来,我们通常将人们指向BenchmarkDotNet。虽然它不能解决所有与基准测试相关的问题,但它可以解决很多常见问题。 除了您解决的问题之外,您可能还学到了其他东西:在 SO 中的标题中使用 C# 与 C++ 是一个坏主意。在人们能够查看您的代码之前,它会引发很多(不良)情绪。也许,您应该在标题中公开您的实际问题(例如“几乎相同的 asm 的不同运行时”),并在脚注的底部提到 C++ 与 C#... ;-) 但是,尊重,除了一开始的所有负面反馈之外,您还敢于进行第二轮。 @Scheff “在人们能够查看您的代码之前,它会引发很多(不良)情绪”我仍然不明白为什么这里有这么多人将关于他们相对表现的问题视为神圣战争什么的。我多年来一直在编写这两种语言,而且经常在同一个项目中。这允许利用两者的优势,并解决他们的弱点。顺便说一句,这就是我有这个问题的原因:***.com/q/58881359/126995github.com/dotnet/coreclr/issues/27909 作为圣战之类的 这很简单:C++ 家伙嫉妒 C# 家伙运行他们的东西的速度有多快。 C# 家伙嫉妒 C++ 中细粒度的内存管理(没有 GC,当迫切需要性能时开始清理)。 :-) 说真的:您的多语言方法对我来说似乎是合理的。只是:快速给出投票 - 无需仔细阅读 - 即使提问者表现出重要的代表。有一次,你有一个投票,他们可能会总结,尤其是反对票。我们人类是群居动物... ;-)

以上是关于为啥即使生成的机器代码几乎相同,C# 的速度却是 C++ 的两倍?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 SSE 和 AVX 具有相同的效率?

TCPDF:为啥即使单击不同单元格的按钮,“生成 pdf”按钮也会显示相同的数据?

为啥 ArrayList 以 1.5 的速度增长,而 Hashmap 却是 2?

如何查看 C# 编译器生成的 MSIL / CIL?为啥叫组装?

几乎相同的代码运行速度要慢得多

c# 为啥从sql server中提取float型,提取9.99结果却是9.9900000000000002