在.NET 6中的循环对齐

2021-03-26 20:09:27

Warning: Can only detect less than 5000 characters

... 00007FF9A59ECFF6测试EDX,EDX 00007FF9A59ECF8 jle短g_m22313_ig06 00007FF9a59ECFFA对齐[6字节]; ............................... 32B边界................. .............. G_M22313_IG04:00007FF9A59ED000 MOVSXD R8,EAX 00007FF9A59ED003 MOV R8D,DWORD PTR [RCX + 4 * R8 + 16] 00007FF9A59D008 CMP R8D,ESI 00007FF9A59ED00B JGE SHORT G_M22313_IG14 G_M22313_IG05:00007FF9A59D00D ICAX 00007FF9A59ED00F CMP EDX,EAX 00007FF9A59ED011 JG短G_M22313_IG04

一个简单的方法是将填充添加到所有热环。但是,由于我将在下面的记忆成本部分中描述,因此与填充方法的所有循环都有相关的成本。我们需要考虑到很多考虑到稳定的性能提升为热环,并确保性能未降级填充不受填充的循环。

根据处理器的设计,如果热代码在16B,32B或64B对准边界处对齐,则在其上运行的软件利益更多。虽然对齐应该是16种和大型硬件制造商的最多推荐边界的倍数,但是,AMD和ARM是32字节,我们有32个作为我们的默认对齐边界。具有自适应对齐(使用Complus_jitalignLoopAdive环境变量控制并被设置为1默认),我们将尝试在32字节边界处对齐循环。但如果我们没有看到它是有利可图的,可以在32字节边界上对齐一个循环(出于下面列出的原因),我们将尝试在16个字节边界处对齐该循环。使用非自适应对齐(complus_jitalignloopadaptive = 0),我们将始终尝试将循环对齐至32字节对齐。也可以使用Complus_jitalignLoopBound环境变量来更改对齐边界。自适应和非自适应对准的不同之处在于添加的填充字节的量,我将在下面的填充量部分中讨论。

与填充指令有关的成本。虽然NOP指令很便宜,但需要几个周期来获取和解码它。因此,在热代码路径中具有太多的NOP或NOP指令可能会对代码的性能产生不利影响。因此,在方法中对齐每种可能的循环不合适。也就是说,LLVM具有-Align-all-*或GCC具有-Falign-LoOPS标志,以便将控件提供给开发人员,以便他们决定应该对齐哪个循环。因此,我们想做的最重要的事情是识别与对齐最有益的方法中的循环。首先,我们决定只是嵌套循环对齐,其阻止重量符合某个权重阈值(由Comprus_jinitalignLoopMinblock1重量控制)。阻止权重是编译器知道特定块执行的频率的机制,以及根据该块对该块执行各种优化。在下面的示例中,J-LOOP和K环标记为环对齐候选,只要它们更频繁地执行以满足块重量标准。这是在JIT的OPTINTINGIFYLOOPSFORALALARALALIGMMENT方法中完成的。

如果循环呼叫,则会刷新调用者方法的指令,并将加载Callee的指令。在这种情况下,在对准呼叫者内部的循环方面没有任何有益。因此,我们决定不对齐包含方法调用的循环。下面,L循环,虽然是非嵌套的,但它有一个呼叫,因此我们不会对齐它。我们在AddContainsCallAllContainsingLoops中过滤此类循环。

void somemethod(int n,int m){for(int i = 0; i< n; i ++){//j循环是(int j = 0; j< j ++的对齐候选者){// body}}如果(条件){return; } // k循环是(int k = 0; k + k ++){// body}的对齐候选者(int l = 0; l lt; m; l ++){/ /身体其他方法; }}

一旦在早期阶段确定了循环,我们向前前进了先进的检查,以查看填充是有益的,如果是,则应该是填充金额。所有这些计算都发生在EmitcalculatePaddingForloOpAlignment中。

如果循环很小,则对齐循环是有益的。随着循环尺寸的增长,填充的效果消失,因为已经存在很多指令获取,解码和控制流程,这与存在循环的第一指令的地址无关紧要。我们默认循环大小为96个字节,为3 x 32字节块。换句话说,将考虑足够小的任何内部环,每个内环都能适合每个32b的32b,以便对准。对于实验,可以使用Complus_jinitalignLoopMaxCodesize环境变量来更改该限制。

接下来,我们检查循环是否已经在所需的对齐边界处对齐(32个字节或16个字节,用于自适应对准和32字节用于非自适应对准)。在这种情况下,不需要额外的填充。下面,IG10的环路在地址0x00007FF9a9a9a9a9a9a9a9a9a9a9a9a9a91f5980 == 0(mod 32)处已经处于所需的偏移,并且不需要额外的填充来进一步对准。

00007FF9A91F597A CMP DWORD PTR [RBP + 8],R8D 00007FF9A91F597E JL短G_M24050_IG12; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ ................... 00007FF9A91F5980对齐[0字节] G_M24050_IG10:00007FF9A91F5980 MOVSXD RDX,ECX 00007FF9A91F5983 MOV R9,QWORD PTR [RBP + 8 * RDX + 16] 00007FF9A91F5988 MOV QWORD PTR [RSI + 8 * RDX + 16],R9 00007FF9A91F598D INC ECX 00007FF9A91F598F CMP R8D,ECX 00007FF9A91F5992 JG短G_M24050_IG10

我们还添加了一个“几乎对齐的循环”后卫。可以有循环在32b边界处完全没有开始,但它们足够小,以完全适合单个32b块。可以使用单个指令获取请求获取此类环路的所有代码。在下面的示例中,两个32B边界(用32B边界标记)之间的指令适合在32字节的单个块中。循环IG04是该块的一部分,如果我们将额外的填充添加到32B边界,则不会提高其性能。即使没有填充,也将在单个请求中获取整个循环。因此,没有点对齐这样的环。

; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ..................... 00007FF9A921A903 CALL CORINFO_HELP_NEWARR_1_VC 00007FF9A921A908 XOR ECX,ECX 00007FF9A921A90A MOV EDX,DWORD PTR [RAX + 8] 00007FF9A921A90D测试EDX,EDX 00007FF9A921A90F JLE SHORT G_M24251007FF9A921A911对齐[ 0字节] G_M24257_IG04:00007ff9a921a911 movsxd R8,ECX 00007ff9a921a914 MOV qword的PTR [RAX + 8 * R8 + 16],RSI 00007ff9a921a919 INC ECX 00007ff9a921a91b CMP EDX,ECX 00007ff9a921a91d JG SHORT G_M24257_IG04 G_M24257_IG05:00007ff9a921a91f加RSP,40; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ...................

这是我们在循环对齐逻辑中添加的重要警卫。如果没有这个,想象一个以偏移Mod(32)+ 1开始的大小20个字节的循环。要对齐这种循环,它需要31个字节的填充,在某些方案中可能没有有益,其中31字节NOP指令在热代码路径上。 。 “几乎对齐的循环”保护我们免受这种情况。

“几乎对齐的循环”检查不限于仅适用于单个32B块的小环。对于任何循环,我们计算适合循环代码所需的最小块数。现在,如果循环已经对齐,使其占用这些最小块块,那么我们可以安全地忽略循环,因为填充不会更好地使其更好。

在下面的示例中,循环IG04是37字节长(00007FF9A921C690-00007FF9A921C66B = 37)。它需要最少2个32b块以适合。如果循环在Mod(32)和Mod(32)+(34-37)之间的任何位置开始,我们可以安全地跳过填充,因为循环已经放置,使其主体将在2个请求中获取(第1次请求32字节下一个请求中的5个字节)。

; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^(xor:2)32b边界............ ..................... 00007FF9A921C662 MOV R12D,DWORD PTR [R14 + 8] 00007FF9A921C666测试R12D,R12D 00007FF9A921C669 JLE短G_M11250_IG07 00007FF9A921C66B对齐[0字节] G_M11250_IG04:00007FF9A921C66B CMP R15D ,EBX 00007FF9A921C66E JAE G_M11250_IG19 00007FF9A921C674 MOVSXD RAX,R15D 00007FF9A921C677 SHL RAX,5 00007FF9A921C67B VMOVUPD YMM0,YMMWORD PTR [RSI + RAX + 16]; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^(movupd:1)32b边界............ ................... 00007ff9a921c681 vmovupd ymmword的ptr [R14 + RAX + 16],YMM0 00007ff9a921c688 INC r15d 00007ff9a921c68b CMP R 12D,R r15d 00007ff9a921c68e JG SHORT G_M11250_IG04 G_M11250_IG05:00007ff9a921c690 JMP SHORT G_M11250_IG07 ; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^ ^^^^^^^^ ...................

要回顾,我们已经确定了一种需要填充的方法中的热嵌套循环,过滤掉了一个呼叫的方法,过滤了大于我们的阈值的那些,并验证了循环的第一个指令是否放置了这样的循环指令额外的填充将在所需的对齐边界处对齐该指令。

为了对齐循环,在循环开始之前需要插入NOP指令,使得循环的第一指令在作为Mod(32)或Mod(16)的地址处开始。它可以是我们需要添加多少填充以对齐循环的设计选择。例如,为了将循环对准到32B边界,我们可以选择添加31个字节的最大填充,或者可以对填充金额有限制。由于填充或NOP指令不含免费,因此它们将被执行(作为方法流的一部分,或者将对齐的循环嵌套在另一个循环内),因此我们需要仔细选择应该添加多少填充。具有非自适应方法,如果对准需要在N字节边界处发生,我们将尝试以最多的N-1字节添加以对齐循环的第一个指令。因此,使用32B或16B非自适应技术,我们将尝试将循环与大多数31个字节或15个字节添加到32字节或16字节边界。

但是,如上所述,我们意识到添加了很多填充物回归代码的性能。例如,如果长度为15个字节的循环,从偏移Mod(32)+ 2开始,具有非自适应32b方法,我们将添加30个字节的填充以将该循环对准到下一个32b边界地址。因此,要对准15个字节长的循环,我们已添加额外的30个字节以使其对齐。如果我们对齐的循环是嵌套循环,则处理器将在外循环的每个迭代中获取和解码这些30字节的NOP指令。我们还将方法的大小增加了30个字节。最后,由于我们将始终尝试在32B边界处对齐循环,因此我们可能会添加更多填充所需的填充量,因为我们必须在16B边界处对齐环路。通过所有这些缺点,我们提出了一种自适应对准算法。

在自适应对齐方面,我们将限制根据循环的大小添加的填充量。在这种技术中,将添加的最大可能的填充是一个适用于一个32B块的循环的15个字节。如果循环更大并且适合两个32B块,则我们将填充金额将填充量减少到7个字节等。背后的推理是,循环变得更大,它将对对齐的影响较小。通过这种方法,如果需要填充为1字节,我们可以对准需要4 32B块的循环。使用32B非自适应方法,我们永远不会对齐这样的循环(因为Comprus_JinitalignLoopMaxCodeSize Limit)。

接下来,由于填充限制,如果我们无法使循环达到32b边界,则算法将尝试将循环对准到16b边界。如果我们在下表中看到的话,我们会减少最大填充限制。

利用自适应对准模型,而不是完全限制循环的填充(由于填充限制为32B),我们仍将尝试对准下一个更好的对准边界上的循环。

如果决定需要填充并且我们计算填充金额,则重要的设计选择是放置填充指令的位置。在.NET 6中,通过将填充指令放置在循环开始之前,它是Naiï。但如上所述,这可能对性能产生不利影响,因为填充指令可以落在执行路径上。更聪明的方式是在循环之前检测代码中的一些盲点,并将其放置在使得填充指令不被执行或被很少执行。例如,如果我们在方法代码中有一个无条件跳转,我们可以在无条件跳转之后添加填充指令。通过这样做,我们将确保从未执行填充指令,但我们仍然会在右边界处得到对齐的循环。可以添加这种填充的另一个地方是代码块或者很少执行的块(基于轮廓引导优化数据)。我们选择的盲点应该在我们尝试对齐的循环之前词汇。

00007ff9a59feb6b JMP SHORT G_M17025_IG30 G_M17025_IG29:00007ff9a59feb6d MOV RAX,RCX G_M17025_IG30:00007ff9a59feb70 MOV ECX,EAX 00007ff9a59feb72 SHR ECX,3 00007ff9a59feb75 XOR R 8d各自,R8D 00007ff9a59feb78测试ECX,ECX 00007ff9a59feb7a JBE SHORT G_M17025_IG32 00007ff9a59feb7c对齐[4字节]; ............................... 32B边界................. .............. G_M17025_IG31:00007ff9a59feb80 vmovupd XMM0,xmmword的ptr [RDI] 00007ff9a59feb84 vptest XMM0,xmm6 00007ff9a59feb89 JNE SHORT G_M17025_IG33 00007ff9a59feb8b vpackuswb XMM0,XMM0,XMM0 00007ff9a59feb8f vmovq xmmword的ptr [RSI],XMM0 00007FF9A59FEB93添加RDI,16 00007FF9A59FEB97 ADD RSI,8 00007FF9A59FEB9B INC R8D 00007FF9A59FEB9E CMP R8D,ECX; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^(cmp:1)32b边界............ ................... 00007FF9A59FEBA1 JB短G_M17025_IG31

在上面的示例中,我们将循环IG31对齐4个字节填充,但我们已在循环的第一个指令之前插入填充。相反,我们可以在00007FF9A59FEB6B的JMP指令之后添加该填充。这样,将永远不会执行填充,但IG31仍将在所需边界处变为对齐。

最后,有一个nee

......