gpt4 book ai didi

c# - 调用方法时过多的汇编说明

转载 作者:太空宇宙 更新时间:2023-11-03 23:17:24 25 4
gpt4 key购买 nike

我正在读Pro .net Performance书。它指出:


  以下是托管方法的典型序言和结尾
  编译为32位机器代码(这不是实际的生产代码
  由JIT编译器制作,并进行了大量优化
  在第10章中讨论)。该方法有四个局部变量,它们的
  在序言中立即分配存储,并在其中立即回收
  结语:


书籍声称这种方法:

int Calculation(int a, int b)
{
int x = a + b;
int y = a - b;
int z = b - a;
int w = 2 * b + 2 * a;
return x + y + z + w;
}


将被翻译为:

; parameters are passed on the stack in [esp+4] and [esp+8]
push ebp
mov ebp, esp
add esp, 16 ; allocates storage for four local variables
mov eax, dword ptr [ebp+8]
add eax, dword ptr [ebp+12]
mov dword ptr [ebp-4], eax
; ...similar manipulations for y, z, w
mov eax, dword ptr [ebp-4]
add eax, dword ptr [ebp-8]
add eax, dword ptr [ebp-12]
add eax, dword ptr [ebp-16] ; eax contains the return value
mov esp, ebp ; restores the stack frame, thus reclaiming the local storage space
pop ebp
ret 8 ; reclaims the storage for the two parameters


为了测试它,我创建了以下类:

class TestCall
{
public static void Main(string[] args)
{
TestCall testCall=new TestCall();
int sum=0;
for (int i = 0; i < 5; i++)
{
sum += testCall.Calculation(5, 6);
}
Console.WriteLine(sum);
}
int Calculation(int a, int b)
{
int x = a + b;
int y = a - b;
int z = b - a;
int w = 2 * b + 2 * a;
return x + y + z + w;
}
}


调试时,Visual Studio的“反汇编”窗口显示 Calculation方法:

    23:         {
005B2EB8 push ebp
005B2EB9 mov ebp,esp
005B2EBB push edi
005B2EBC push esi
005B2EBD push ebx
005B2EBE sub esp,48h
005B2EC1 mov esi,ecx
005B2EC3 lea edi,[ebp-38h]
005B2EC6 mov ecx,0Bh
005B2ECB xor eax,eax
005B2ECD rep stos dword ptr es:[edi]
005B2ECF mov ecx,esi
005B2ED1 mov dword ptr [ebp-3Ch],ecx
005B2ED4 mov dword ptr [ebp-40h],edx
005B2ED7 cmp dword ptr ds:[12C668h],0
005B2EDE je 005B2EE5
005B2EE0 call 6970CB2D
005B2EE5 xor edx,edx
005B2EE7 mov dword ptr [ebp-54h],edx
005B2EEA xor edx,edx
005B2EEC mov dword ptr [ebp-4Ch],edx
005B2EEF xor edx,edx
005B2EF1 mov dword ptr [ebp-50h],edx
005B2EF4 xor edx,edx
005B2EF6 mov dword ptr [ebp-48h],edx
005B2EF9 xor edx,edx
005B2EFB mov dword ptr [ebp-44h],edx
005B2EFE nop
24: int x = a + b;
005B2EFF mov eax,dword ptr [ebp-40h]
005B2F02 add eax,dword ptr [ebp+8]
005B2F05 mov dword ptr [ebp-44h],eax
25: int y = a - b;
005B2F08 mov eax,dword ptr [ebp-40h]
005B2F0B sub eax,dword ptr [ebp+8]
005B2F0E mov dword ptr [ebp-48h],eax
26: int z = b - a;
005B2F11 mov eax,dword ptr [ebp+8]
005B2F14 sub eax,dword ptr [ebp-40h]
005B2F17 mov dword ptr [ebp-4Ch],eax
27: int w = 2*b + 2*a;
005B2F1A mov eax,dword ptr [ebp+8]
005B2F1D add eax,eax
005B2F1F mov edx,dword ptr [ebp-40h]
27: int w = 2*b + 2*a;
005B2F22 add edx,edx
005B2F24 add eax,edx
005B2F26 mov dword ptr [ebp-50h],eax
28: return x + y + z + w;
005B2F29 mov eax,dword ptr [ebp-44h]
005B2F2C add eax,dword ptr [ebp-48h]
005B2F2F add eax,dword ptr [ebp-4Ch]
005B2F32 add eax,dword ptr [ebp-50h]
005B2F35 mov dword ptr [ebp-54h],eax
005B2F38 nop
005B2F39 jmp 005B2F3B
29: }
005B2F3B mov eax,dword ptr [ebp-54h]
005B2F3E lea esp,[ebp-0Ch]
005B2F41 pop ebx
005B2F42 pop esi
005B2F43 pop edi
005B2F44 pop ebp
005B2F45 ret 4


为什么会有这么大的序幕?我的Windows是32位的。项目目标是.net 4.5。调试打开,优化关闭。

最佳答案

不要从调试器运行应用程序。而是将类似Debugger.Launch的内容放入代码中,然后运行而无需调试。当您在调试器中启动时,JIT编译器会针对调试进行优化,此处您要避免这种情况。

我在调试器中运行32位调试版本,

002E2EC2  in          al,dx  
002E2EC3 push edi
002E2EC4 push esi
002E2EC5 push ebx
002E2EC6 sub esp,48h
002E2EC9 mov esi,ecx
002E2ECB lea edi,[ebp-38h]
002E2ECE mov ecx,0Bh
002E2ED3 xor eax,eax
002E2ED5 rep stos dword ptr es:[edi]
002E2ED7 mov ecx,esi
002E2ED9 mov dword ptr [ebp-3Ch],ecx
002E2EDC mov dword ptr [ebp-40h],edx
002E2EDF cmp dword ptr ds:[28C670h],0
002E2EE6 je 002E2EED
002E2EE8 call 730FCB2D
002E2EED xor edx,edx
002E2EEF mov dword ptr [ebp-54h],edx
002E2EF2 xor edx,edx
002E2EF4 mov dword ptr [ebp-4Ch],edx
002E2EF7 xor edx,edx
002E2EF9 mov dword ptr [ebp-50h],edx
002E2EFC xor edx,edx
002E2EFE mov dword ptr [ebp-48h],edx
002E2F01 xor edx,edx
002E2F03 mov dword ptr [ebp-44h],edx
002E2F06 nop
int x = a + b;
002E2F07 mov eax,dword ptr [ebp-40h]
002E2F0A add eax,dword ptr [ebp+8]
002E2F0D mov dword ptr [ebp-44h],eax
int y = a - b;
002E2F10 mov eax,dword ptr [ebp-40h]
int y = a - b;
002E2F13 sub eax,dword ptr [ebp+8]
002E2F16 mov dword ptr [ebp-48h],eax
int z = b - a;
002E2F19 mov eax,dword ptr [ebp+8]
002E2F1C sub eax,dword ptr [ebp-40h]
002E2F1F mov dword ptr [ebp-4Ch],eax
int w = 2 * b + 2 * a;
002E2F22 mov eax,dword ptr [ebp+8]
002E2F25 add eax,eax
002E2F27 mov edx,dword ptr [ebp-40h]
002E2F2A add edx,edx
002E2F2C add eax,edx
002E2F2E mov dword ptr [ebp-50h],eax
return x + y + z + w;
002E2F31 mov eax,dword ptr [ebp-44h]
002E2F34 add eax,dword ptr [ebp-48h]
002E2F37 add eax,dword ptr [ebp-4Ch]
002E2F3A add eax,dword ptr [ebp-50h]
002E2F3D mov dword ptr [ebp-54h],eax
002E2F40 nop
002E2F41 jmp 002E2F43


足够接近拆卸部件。 64位使这更好,但是在调试器之外运行发行版会产生更好的效果……嗯,实际上,整个方法都是内联的。糟糕:)在处理.NET性能时,这实际上是唯一对您重要的情况-该示例是您在调试器之外看不到的。在生产现实中,根本没有理由分配任何变量,更重要的是,即使使用变量,也不一定要将它们放在堆栈中。不要将.NET虚拟机(所有内容都传递到堆栈上)与在x86“虚拟机”上执行的实际代码相混淆:P

好的,所以是调试器之外的32位调试版本:

00350543  sub         esp,20h  
00350546 xor eax,eax
00350548 mov dword ptr [ebp-20h],eax
0035054B mov dword ptr [ebp-4],ecx
0035054E mov dword ptr [ebp-8],edx
00350551 cmp dword ptr ds:[1D4268h],0
00350558 je 0035055F
0035055A call 730FCB2D
0035055F xor edx,edx
00350561 mov dword ptr [ebp-0Ch],edx
00350564 xor edx,edx
00350566 mov dword ptr [ebp-1Ch],edx
00350569 xor edx,edx
0035056B mov dword ptr [ebp-14h],edx
0035056E xor edx,edx
00350570 mov dword ptr [ebp-18h],edx
00350573 xor edx,edx
00350575 mov dword ptr [ebp-10h],edx
00350578 nop
Debugger.Launch();
00350579 call 725B0220
0035057E mov dword ptr [ebp-20h],eax
00350581 nop
Debugger.Break();
00350582 call 725B0178
00350587 nop

int x = a + b;
00350588 mov eax,dword ptr [ebp-8]

int x = a + b;
0035058B add eax,dword ptr [ebp+8]
0035058E mov dword ptr [ebp-0Ch],eax
int y = a - b;
00350591 mov eax,dword ptr [ebp-8]
00350594 sub eax,dword ptr [ebp+8]
00350597 mov dword ptr [ebp-10h],eax
int z = b - a;
0035059A mov eax,dword ptr [ebp+8]
0035059D sub eax,dword ptr [ebp-8]
003505A0 mov dword ptr [ebp-14h],eax
int w = 2 * b + 2 * a;
003505A3 mov eax,dword ptr [ebp+8]
003505A6 add eax,eax
003505A8 mov edx,dword ptr [ebp-8]
003505AB add edx,edx
003505AD add eax,edx
003505AF mov dword ptr [ebp-18h],eax
return x + y + z + w;
003505B2 mov eax,dword ptr [ebp-0Ch]
003505B5 add eax,dword ptr [ebp-10h]
003505B8 add eax,dword ptr [ebp-14h]
003505BB add eax,dword ptr [ebp-18h]
003505BE mov dword ptr [ebp-1Ch],eax
003505C1 nop
003505C2 jmp 003505C4


您会看到它比“附加的调试器”版本短很多,但也没有示例简单。这很可能与一段时间内添加到.NET或JIT编译器中的各种新功能有关。但这仍然是学术性的-这是未经优化的代码,专门为简化调试而设计。首先与示例相同-这是一种理想化的情况,专门用于展示一些概念,并带有一些奇怪的情况(您何时最后一次看到一个从底部填充的堆栈?)。

如果您对调试器之外的64位发行版中的真实x86代码感到好奇,它看起来像这样:

000007FE94220495  xor         esi,esi  
for (int i = 0; i < 5; i++)
000007FE94220497 xor edi,edi
{
sum += testCall.Calculation(5, 6);
000007FE94220499 call 000007FEF2FB7AB0
000007FE9422049E call 000007FEF2FB79C0
000007FE942204A3 add esi,21h
for (int i = 0; i < 5; i++)
000007FE942204A6 inc edi
000007FE942204A8 cmp edi,5
000007FE942204AB jl 000007FE94220499
}
Console.WriteLine(sum);
000007FE942204AD mov ecx,esi
000007FE942204AF call 000007FEF2FBAC10
000007FE942204B4 nop
000007FE942204B5 add rsp,28h
000007FE942204B9 pop rsi
000007FE942204BA pop rdi
000007FE942204BB ret


那么它是怎样工作的?您可以忽略循环中的两个调用-分别是 Debugger.LaunchDebugger.Break。循环的整个过程就是

000007FE942204A3  add         esi,21h  


嗯将 sum变量增加21h(注意它不在堆栈中)?

让我们分析一下我们的功能:

x + y == 2 * a
(x + y + z) == a + b
(x + y + z + w) == 3 * a + 3 * b


a始终为5, b始终为6,因此该函数始终返回33,即21h。因此,编译器“足够聪明”,但是对于判断.NET中的函数调用不是很有用。

让我们公开该方法,以便它必须是外部合同的一部分,并且代码不再可以仅假定传递的实际参数有效。在调试器之外再次运行,我们得到了……完全一样的东西。编译器将编译公共方法,但仍将内联我们使用它的解决方案,因为它仍然是100%安全的。

好的,让我们通过从输入中读取参数来使它们可变:)函数仍内联。没有内联方法是没有意义的。我们如何强制代码避免内联?好吧,我们可以将方法设为 virtual,这样编译器就无法确定到底要调用哪种方法,从而防止内联。另外,您可以使用 之类的JIT属性:

            Debugger.Launch();
000007FE94220530 push rdi
000007FE94220531 push rsi
000007FE94220532 sub rsp,28h
000007FE94220536 mov esi,edx
000007FE94220538 mov edi,r8d
000007FE9422053B call 000007FEF2FB7AB0
Debugger.Break();
000007FE94220540 call 000007FEF2FB79C0

int x = a + b;
000007FE94220545 mov eax,esi
000007FE94220547 sub eax,edi
000007FE94220549 mov edx,edi
000007FE9422054B sub edx,esi
000007FE9422054D mov ecx,esi
000007FE9422054F shl ecx,1
000007FE94220551 lea ecx,[rcx+rdi*2]
000007FE94220554 lea r8d,[rsi+rdi]
000007FE94220558 add eax,r8d
000007FE9422055B add eax,edx
000007FE9422055D add eax,ecx
000007FE9422055F add rsp,28h
000007FE94220563 pop rsi
000007FE94220564 pop rdi
000007FE94220565 ret


根据您的定义,可能没有序言,或者序言仅包含保留我们在方法中使用的少量寄存器-64位调用约定在这里有很大帮助,但是从根本上说,编译器只是尝试寻找最便宜的方式要做的事情-在这种情况下,它避免完全使用堆栈,并且所有参数都通过寄存器传递。为什么还要调整 NoInlining?我不知道。也许对调试或异常处理有所帮助?我对此只能猜测。但它当然与功能的正常运行无关。随后的移动(将参数移至其他寄存器)是针对我的CPU(2012 Xeon)量身定制的-它允许更好的流水线处理。我不确定启发式算法是如何工作的以及针对特定CPU或CPU系列的尝试程度,但是对于我的系统而言,它当然非常有用:)您期望这种代码比手动天真的汇编慢一些,但是不是。流水线和分支预测是当今CPU吞吐量的最重要因素,并且在幼稚的解决方案中,它们使CPU停顿了很多。

让我们分析一下。首先,编译器不再尝试简化表达式-实际上,它确实分别评估x,y,z和w。但是,您可以看到它仍然没有使用堆栈作为变量(尽管它可能会使用堆栈来传递32位的两个参数)。 rsp包含b,而 esi包含a;所以我们先做 edi,然后做 b - a-到目前为止很简单。 a - b包含z, eax包含y。然后我们将b放入 edx并向左移动一次( ecx)。

但是,等等,那个 2 * b是怎么回事?编译器实际上滥用CPU,以使其在单条指令中完成其余的计算,同时仍以最佳方式使用流水线和所有内容,并在尽可能多的CPU上分散工作负载(在我的CPU上,地址计算与“真正的” ALU)。令人印象深刻的一个例子,也许是一个例子,证明JITted代码实际上比实际的预编译代码可以更好地适应特定的CPU模型。当然,这些也是我们在手工组装中与较旧的硬件一起使用的技巧,但是……令人印象深刻。

简而言之,请小心您的假设。有时,编译器放弃了“显而易见的”优化,有时它比您想象的要聪明。每当优化性能的.NET代码时,只需确保在调试器外部运行发行版,并确保分别分析32位和64位代码-有时32位更快,有时64位位是。

关于c# - 调用方法时过多的汇编说明,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36791544/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com