唉,忙活半天,白瞎了。继续吧。
改进的方法很多,其中一个方法是交换ADD和SHL的顺序:
点击(此处)折叠或打开
- ; Example 5.6b. P4 transfer data between execution units
- ;clock ex.unit subunit
- and eax, 00Fh ; 0.0 - 0.5 ALU0 LOGIC
- xor ebx, 30h ; 0.5 - 1.0 ALU0 LOGIC
- shl eax, 3 ; 1.0 - 5.0 INT MMX SHIFT
- add eax, 8 ; 6.0 - 6.5 ALU1 ADD
- sub eax, ecx ; 6.5 - 7.0 ALU0/1 ADD
- mov edx, eax ; 7.0 - 7.5 ALU0/1 MOV
- imul edx, 100 ; 8.0 - 22.0 INT FP MUL
- or edx, ebx ; 22.0 -22.5 ALU0/1 MOV
这一段需要仔细的阅读,才能理解。但是我看了好多遍,怎么都感觉不太合理啊!!!!!
1 Example 5.6b中 shl执行前为何没有延时了,anger描述说“节省了shl前的半个时钟”,哪呢啊???
2 SHL EAX,3可以替换为3×(ADD EAX,EAX)???这里貌似也不正确啊,难道不是4X(ADD EAX,EAX)???
所以我把这段修改如下,不过我的修改也不能保证完全正确,大家谁仔细阅读并且明白的话,一定说给我,我做修改:
点击(此处)折叠或打开
- ; Example 5.6c. P4 transfer data between execution units
- ;clock ex.unit subunit
- and eax, 00Fh ; 0.0 - 0.5 ALU0 LOGIC
- shl eax, 3 ; 1.0 - 5.0 INT MMX SHIFT
- xor ebx, 30h ; 6.0 - 6.5 ALU0 LOGIC
- add eax, 8 ; 6.0 - 6.5 ALU1 ADD
- sub eax, ecx ; 6.5 - 7.0 ALU0/1 ADD
- mov edx, eax ; 7.0 - 7.5 ALU0/1 MOV
- imul edx, 100 ; 8.0 - 22.0 INT FP MUL
- or edx, ebx ; 22.0 -22.5 ALU0/1 MOV
三个0.5周期,这样到了imul开始执行时,正好也是只要等待半个时钟就落在整数时钟上。这样才能使半时钟数量减少2个。这段序列也就只需要22.5个时钟完成。另外一点就是5.6a中的第三行
add eax, 1,移动到shl后面就变成add eax, 8了,因为shl左移3位,那么1左移3位就是8了。
如果我们想知道为什么从一个执行单元转到另一个执行单元会有额外的延迟,我认为有三种可能的解释:
解释A:硅芯片上执行单元之间的物理距离很大,由于导线中的感应和电容,这可能会导致电信号从一个单元到另一个单元的传播传播延迟。
解释B:执行单元之间的“逻辑距离”意味着数据必须经过各种寄存器,缓冲区,端口,总线和多路复用器才能到达正确的目的地。 设计人员已实现各种快捷方式来绕过这些延迟元素,并将结果直接转发到等待这些结果的执行单元。 这些快捷方式可能仅连接到同一端口下的执行单元。
解释C:如图5.4所示,如果将128位操作数以每次64位的错位方式进行处理,那么在两半部分(也就是高64位和低64位)合并时,在128位指令的末尾,我们将有1个时钟延迟。 例如,P4的128位寄存器中存放双精度浮点数,然后执行加法运算。如果低64位操作数的add在时间T=0处开始,则它将在T=4处结束。 高64位操作数在时间T=1处开始,并在T=5处结束。如果下一个有依赖关系的操作也是加法,则第二个加法可以在第一个加法较高操作数准备就绪之前的时间T=4处开始对第二个加法的低64位操作数进行操作。一条这样的指令链的等待时间似乎是每个128位操作4个时钟周期。 如果128位寄存器上的所有操作都以这种方式重叠,那么我们将永远不会看到128位操作具有比相应的64位操作更高的延迟。 但是,如果将数据传输到另一个执行单元,那就必须等待128位凑齐一起传输,也就是这里需要一个高低64位操作数的同步,这样我们将获得1个时钟周期的额外延迟,如图5.5所示。
同样,P4上的双速单元ALU0和ALU1将32位操作处理为两个16位操作,每个操作占用半个时钟周期。 但是,如果同时需要所有32位,则将产生半个时钟的额外延迟。 至于执行单元之间的数据总线是32位,64位还是128位宽就不知道了。
5.7 Retirement(退出)
在P4和P4E中,执行的μop的退出工作与第六代处理器中的工作相同。该过程在PPro P2和P3部分进行了说明。退出站每个时钟周期可以处理3μop。这看起来似乎不是问题,因为追踪缓存中的吞吐量已经限制为每个时钟3μops。但是退出站还有一个严重的限制,即jump只能从退出站三个端口中的第一个端口中退出。有时这会限制小循环的吞吐量。如果循环中的μop数不是3的倍数,则循环底部的跳回指令可能会进入非第一个端口,为了让jump从第一个端口退出,需要一个时钟周期的代价。
因此,建议在小的关键循环中,μop(而非指令)的数量最好为3的倍数。在某些情况下,您可以通过在循环中添加一个或两个NOP来使uops的总数可以被3整除,进而每次迭代可以节省一个时钟周期。 但是这仅在期望每个时钟周期的吞吐量为3μops时适用。
5.8 Partial registers and partial flags(部分寄存器与标志寄存器操作)
寄存器AL,AH和AX都是EAX寄存器的一部分。 这些称为部分寄存器。在第6代微处理器上(Ppro/P2/P3),可以将部分寄存器拆分为单独的临时寄存器,以便可以相互独立地处理不同的部分。每当需要将寄存器的不同部分合并为一个完整的寄存器时,这都会导致严重的延迟。 此问题在Ppro/P2/P3已经描述过,后面还会在PM系列再次说明。
而我们这里说的P4/P4E的处理方式与Ppro/P2/P3和PM不一样,P4/P4E解决此问题的方式是始终将整个寄存器保持在一起。即不管你操作AL/AH/AX,都给你锁住一个AX,不让你操作同一寄存器的其它部分。但是,该解决方案也不完美。第一个缺点是引入了虚假的依赖关系。 如果先前对AH的写入被延迟,则对AL的任何读或写都将被延迟。另一个缺点是访问部分寄存器有时需要额外的μop,如下:
点击(此处)折叠或打开
- ; Example 5.7. Partial register access
- mov eax, [mem32] ; 1 uop
- mov ax, [mem16] ; 2 uops
- mov al, [mem8] ; 2 uops
- mov ah, [mem8] ; 2 uops
- add al, bl ; 1 uop
- add ah, bh ; 1 uop
- add al, bh ; 2 uops
- add ah, bl ; 2 uops
1 避免使用AH、BH、CH、DH 8位寄存器;
2 读取8/16位内存操作数时,使用movzx操作32位整个寄存器,即使是在16位模式下也推荐这么操作;
3 如果需要符号扩展,则推荐MOVSX与尽可能大的目标寄存器一起使用,即在16或32位模式下为32位寄存器,在64位模式下为64位寄存器;
4 如果数据是整形并且可以packed,则使用MMX或XMM寄存器处理8位和16位数据;
当指令修改某些标志但其它标志保持不变时,标志寄存器也会出现部分访问的问题。
由于历史原因,INC和DEC指令除了进位标志,其它算术标志都会被写入。 这会导致虚假依赖,如果先前有对标志寄存器的操作,就会导致一个额外的μop。 为避免这些问题,建议您始终使用ADD和SUB而不是INC和DEC。 例如,INC EAX应替换为ADD EAX,1。
SAHF保持溢出标志不变,但会更改其他算术标志。这会导致对标志寄存器的先前值的虚假依赖,但不会产生额外的μop。
BSF和BSR更改零标志,但其他标志保持不变。这导致对标志的先前值的虚假依赖,并导致一个额外的μop。
BT,BTC,BTR和BTS更改进位标志,但其他标志保持不变。这导致对标志的先前值的虚假依赖,并导致一个额外的μop。使用TEST,AND,OR和XOR代替这些指令。在P4E上,还可以使用更有效率的shift指令。例如,如果后面代码不再需要RAX的值,则可以用SHR RAX,41 / JCX代替BT RAX,40 / JCX。
5.9 Store forwarding stalls(存储前置停顿)
访问部分memory带来的问题比部分寄存器操作延时更严重的多。这和前面提到的Ppro/P2/P3一样,intel一直没有解决这个问题,并且在P4/P4E上更严重一些。如下:
点击(此处)折叠或打开
- ; Example 5.8a. Store forwarding stall
- mov dword [mem1], eax
- mov dword [mem1+4], 0
- fild qword [mem1] ; Large penalty
点击(此处)折叠或打开
- ; Example 5.8b. Avoid store forwarding stall
- movd xmm0, eax
- movq qword [mem1], xmm0
- fild qword [mem1] ; No penalty
5.10 Memory intermediates in dependency chains(内存操作依赖链停顿)
P4有一个相当失败的策略,就是在准备就绪之前尝试读取内存操作数,如下面的指令:
点击(此处)折叠或打开
- ; Example 5.9. Memory intermediate in dependency chain
- imul eax, 5
- mov [mem1], eax
- mov ebx, [mem1]
- add ebx, ecx
上面的例子中,最好的解决办法是用mov EBX,EAX替代mov EBX,[MEM1]。另外的一个解决策略是在同一个内存地址的存储和加载之间让处理器执行一切其它操作。
其中第一种方式简单粗暴,那就是直接使用寄存器,但是有2种情况无法使用寄存器保存数据,一种是在16/32位模式下的高级语言调用,即参数通过栈传递,第二种情况是浮点寄存器与其它寄存器之间传输数据,只能使用内存做中介。如果是现阶段的芯片的话,方法倒是不少,如arm可以使用neon vmov即可。
1 传参
在32位模式,C++中带有一个整形参数的典型函数调用形式如下:
只要是调用程序或被调用函数是用高级语言编写的,就可能必须遵守在堆栈上传递参数的约定。当函数声明为__fastcall时,大多数C ++编译器可以在寄存器中传输2或3个整数参数。
但是,此方法尚未标准化。不同的编译器使用不同的寄存器进行参数传输。所以为避免类似依赖链中的内存操作问题,你可能必须将整个依赖链都保留为汇编语言,才能合理使用寄存器。
在64位模式下,只要参数不是过分的多,一般没有此问题,至少5-6参数可以在寄存器中传递。
2 浮点和其它类型寄存器数据传递
在浮点寄存器和其它寄存器之间传输数据,别无他法,除了通过内存。如下:
点击(此处)折叠或打开
- ; Example 5.11. Memory intermediate in integer to f.p. conversion
- imul eax, ebx
- mov [temp], eax
- fild [temp] ; Transfer data from integer register to f.p.
- fsqrt
- fistp [temp] ; Transfer data from f.p. register to integer
- mov eax, [temp]
点击(此处)折叠或打开
- ; Example 5.12. Avoid stall in integer to f.p. conversion
- mov [temp], eax
- and eax, 0 ; Make eax = 0, but keep dependence
- fild [temp+eax] ; Make read address depend on eax
上面的例子是由整形到浮点寄存器。而相似的由浮点到整形寄存器的依赖,就稍微复杂一点,最简单的解决方法就是:
点击(此处)折叠或打开
- ; Example 5.13. Avoid stall in f.p. to integer conversion
- fistp [temp]
- fnstsw ax ; Transfer status after fistp to ax
- and eax, 0 ; Set to 0
- mov eax, [temp+eax] ; Make dependent on eax
标注:这种重复机制的详细研究由Victor Kartunov等发表的“重复:NetBurst核心的未知功能”,www.xbitlabs.com/articles/cpu/print/replay.html。还可参见美国专利6,163,838;5,455,752; 5,200,000。 6,094,717; 6,385,715。但是我发现这个链接已经改头换面了。找不到文章,usa专利我也找不到,所以没有细究,有兴趣的同学可以查找看看。
5.11 Breaking depenency chains(解除依赖链)
将寄存器设置为零的常见方法是XOR EAX,EAX或SUB EBX,EBX。 P4/P4E处理器知道这些指令与寄存器的先前值无关。因此,在寄存器使用新值的任何指令都不必在XOR或SUB指令准备好之前等待该值。
具有64位或128位寄存器的PXOR指令也是如此,但任何指令除外:具有8位或16位寄存器的XOR或SUB,SBB,PANDN,PSUB,XORPS,XORPD ,SUBPS,SUBPD,FSUB。
XOR,SUB和PXOR指令对于解除不必要的依赖关系很有用,但是不适用于PM处理器。
你也可以使用这些指令来打破对标志的依赖。例如,rotate指令对P4中的标志有虚假依赖,可以通过以下方式将其解除:
点击(此处)折叠或打开
- ; Example 5.14. Break false dependence on flags
- ror eax, 1
- sub edx, edx ; Remove false dependence on the flags
- ror ebx, 1
5.12 Choosing the optimal instructions(选用最优指令)
在很多场景,都可以用高效指令替代低效指令,下面是总结的几个重要的,并且经常遇见的情况:
5.12.1 INC和DEC
他们会引起部分标志寄存器虚假依赖问题,前面解释过,所以一般用add eax, 1类似指令来替代 inc eax。
5.12.2 8/16位整数
用MOVZX EAX,BYTE [MEM8]替代MOV AL,BYTE [MEM8];
用MOVZX EBX,WORD [MEM16]替代MOV BX,WORD [MEM16];
避免使用高8位寄存器AH,BH,CH,DH;
如果可以packed并且并行处理8位或16位整数,请使用MMX或XMM寄存器;
5.12.3 内存存储
大多数存储器存储指令使用2μops。如果存储器操作数没有SIB字节,则如MOV [MEM],EAX类型的简单存储指令仅需要一个μop。如果有一个以上的指针寄存器,或者有一个索引寄存器,
或者如果ESP用作基本指针,则需要一个SIB字节。简短的存储指令一般使用通用寄存器。如下:
点击(此处)折叠或打开
- ; Example 5.15. uop counts for memory stores
- mov array[ecx], eax ; 1 uop
- mov array[ecx*4], eax ; 2 uops because of scaled index
- mov [ecx+edi], eax ; 2 uops because of two index registers
- mov [ebp+8], ebx ; 1 uop
- mov [esp+8], ebx ; 2 uops because esp used
- mov [es:mem8], cl ; 1 uop
- mov [es:mem8], ch ; 2 uops because high 8-bit register used
- movq [esi], mm1 ; 2 uops because not a general purp.register
- fstp dword [mem32] ; 2 uops because not a general purp.register
5.12.4 移位(shift)和循环移位(rotate)
在P4上,整数寄存器的移位和循环移位速度非常慢,因为整数执行单元会将数据传输到MMX移位单元,然后再传回来。左移可会被替换成加法。例如,SHL EAX,3被替换为3次ADD EAX,EAX。在P4E上没有这种情况,因为P4E的移位操作与加操作速度一样快。带进位循环移位操作(RCL,RCR),次数为 1或者由cl指定,效果不一样。如果代码包含许多整数移位和乘法运算,则在P4上的MMX或XMM寄存器中执行它可能会更有效。
5.12.5 整数乘法
在P4上,整数乘法速度很慢,因为整数执行单元会将数据传输到FP-MUL单元,操作完再传回来。如果代码具有许多整数乘法,则用MMX或XMM寄存器处理数据可能会更高效。常数的整数乘法可以用加法代替。当然,仅仅应在关键的依赖链中用长序列的ADD指令代替单个乘法指令。
5.12.6 LEA指令
LEA指令在P4和P4E上拆分为加法和移位。具有比例因子的LEA指令最好用加操作代替,这仅适用于LEA指令,不适用于带比例因子的内存操作数的任何其他指令。在64位模式下,具有RIP相对地址的LEA效率低下。用MOV RAX,OFFSET MEM替换LEA RAX,[MEM]更理想。
5.12.7 包含FP、mmx、xmm寄存器的寄存器间mov操作
以下将一个寄存器复制到另一个寄存器的指令在P4上的延迟分别为6个时钟,在P4E上的延迟为7个时钟:MOVQ MM,MM,MOVDQA XMM,XMM,MOVAPS XMM,XMM,MOVAPD XMM,XMM,FLD ST(X ),FST ST(X),FSTP ST(X)。这些指令本身是没有额外的延迟。等待时间较长的可能原因是它们使用与存储访问相同的执行单元(端口0)。这些延迟可以通过以下方式避免:
A 有时可以通过重复使用同一寄存器作为其他指令的源而不是目的地来消除复制寄存器的需要;
B 使用浮点寄存器时,通常可以通过使用FXCH来消除将数据从一个寄存器移到另一个寄存器的需要。 FXCH指令没有等待时间;
C 如果确实需要复制寄存器的值,则在最关键的依赖路径中使用旧副本,而在不太关键的路径中使用新副本;
点击(此处)折叠或打开
- ; Example 5.16. Optimize register-to-register moves
- fld qword [a]
- fadd qword [b] ;a+b
- fld st ;Copy a+b,使st1和st0内容相同,
- fxch ;Get old copy,交换st0和st1,
- fsqrt ;(a+b)0.5
- fxch ;Get new (delayed) copy
- fmul st,st ;(a+b)2
- fmul ;(a+b)2.5
- fstp qword [y]
D 如果以上这些方法都不能解决问题,并且延迟比吞吐量更重要,请使用一下更快的替代方法:
D.1 对于80位浮点寄存器:
点击(此处)折叠或打开
- fld st0 ;copy register
点击(此处)折叠或打开
- fldz ; make an empty register
- xor eax, eax ; set zero flag
- fcmovz st0, st1 ; conditional move
D.2 对于64位mmx寄存器:
点击(此处)折叠或打开
- movq mm1, mm0
点击(此处)折叠或打开
- pshufw mm1, mm0, 11100100B
D.3 对于128位xmm寄存器:
点击(此处)折叠或打开
- movdqa xmm1, xmm0
点击(此处)折叠或打开
- pshufd xmm1, xmm0, 11100100B
点击(此处)折叠或打开
- pxor xmm1, xmm1 ; Set new register to 0
- por xmm1, xmm0 ; OR with desired value
5.13 Bottlenecks in P4 and P4E(P4/P4E的瓶颈)
在优化一段代码时,找到执行速度的限制因素很重要。调整错误的因素不会产生任何有益的影响。在下面的段落中,我将解释每个可能的限制因素。您必须考虑每个因素,以确定哪个是最狭窄的瓶颈,然后将优化工作集中在那个因素上,直到不再是最狭窄的瓶颈为止。
5.13.1 内存访问
如果程序需要访问大量数据,或者数据在内存中比较分散,那么将有很多数据缓存未命中。访问未缓存的数据非常耗时,相对而言,其它所有优化注意事项就都不重要了。
缓存被组织为64字节的对齐行。如果你正在访问对齐的64字节块中的一个字节,则可以确定这个字节附近的所有64字节将都被加载到1级数据高速缓存中,并且可以无代价访问。
为了改善缓存效果,建议将程序中某部分代码要访问的数据存储在一起。您还可以按64字节对齐大型数组和结构。另外如果没有足够的寄存器,则将局部变量存储在堆栈中,也就是定义为局部变量。
一级数据缓存在P4上只有8kb,在P4E上只有16kb。这可能不足以保存所有数据,但是P4/P4E上的二级缓存比以前的处理器更有效。从2级缓存中获取数据仅需多花费几个时钟周期。
不太可能被缓存的数据可以在使用之前被预取。如果连续访问内存地址,则将自动预取它们。因此,您最好以线性方式组织数据,以便可以连续访问它们。并在程序的关键部分访问最多四个大型数组,因为P4/P4E上L1-cache是8路级联。如果少于4个就更好了,总之越少越好,数组的个数越少,cache就不用跳跃了。
PREFETCH指令可以在访问未缓存的数据并且不能依赖自动预取的情况下提高性能。但是,过多使用PREFETCH指令会降低P4上的程序吞吐量。如果你不确定PREFETCH指令是否会对程序有利,那么您可以简单地将所需的数据加载到闲置寄存器中,而不必使用PREFETCH指令。如果没有闲置寄存器,则使用一条不会更改任何寄存器但是可读取内存操作数的指令(例如CMP或TEST)。
由于堆栈指针不太可能成为任何关键依赖关系链的一部分,因此预取数据的一种有用方法是CMP ESP [MEM],它将仅更改标志。执行cmp后,[mem]操作数就会被cache缓存。
当写入接下来不太可能很快再次访问的存储位置时,可以使用non-temporal写入指令MOVNTI等,但是过多使用non-temporal会降低P4的性能。这里的“non-temporal”术语,意思就是不需要refill cache行,直接写内存。有关内存访问的更多准则,请参阅“ Intel Pentium 4和Intel Xeon处理器优化参考手册”。
5.13.2 执行延迟
依赖关系链的执行时间可以根据手册4:“指令表”中列出的延迟来计算。当后续指令转到不同的执行单元时,许多指令的额外延迟为1个时钟周期。有关更多说明,参考5.6 Transfer of data between execution units部分。如果长的依赖链限制了程序的性能,则可以通过选择低延迟的指令,
最大程度地减少执行单元之间的转换次数,拆分依赖链为子表达式,利用所有机会对子表达式并行计算来提高性能。始终避免依赖项链中的内存操作,如5.10 Memory intermediates in dependency chains所述。
5.13.3 执行单元吞吐量
如果依赖链很短,或者正在并行处理多个依赖链,则程序很可能受吞吐量而不是延迟的限制。不同的执行单元具有不同的吞吐量。
ALU0和ALU1处理简单的整数指令和其他常见的μop,每个时钟周期的吞吐量均为2条指令。大多数其他执行单元每个时钟周期的吞吐量为一条指令。
使用128位寄存器时,吞吐量通常为每两个时钟周期一条指令。除法和平方根的吞吐量最低。每个执行单元吞吐量大小均适用于在同一执行子单元中执行的所有μop(请参考5.4)。也就是说每个执行子单元执行特定uop均有特定的吞吐量限制。如果执行吞吐量限制了代码性能,则尝试将一些计算转移到其它执行子单元。
5.13.4 端口吞吐量
每个执行端口每个时钟周期可以接收一个μop。如果uops进入端口0和端口1的双速单元ALU0和ALU1,则它们在每半个时钟周期就可以接收一个μop。如果代码关键部分中的μop进入端口1或0下的单速单元,则吞吐量将被限制为每个时钟周期1μop。如果能将μops最佳地分布在四个端口之间,则每个时钟周期的吞吐量可能高达6μops。但是,如此高的吞吐量只能在短时间段内实现,因为追踪缓存和退出站将平均吞吐量限制为每个时钟周期小于3μop。如果端口吞吐量限制了代码,则尝试将某些μop移至其他端口。
例如,可以将MOV REGISTER,IMMEDIATE替换为MOV REGISTER,MEMORY。
5.13.5 追踪缓存分发
追踪高速缓存最多可提供约每个时钟周期3μop。在P4上,某些μop需要一个以上的追踪缓存条目,如5.2.1章节所述。
对于包含许多分支的代码以及内部带有分支的微小循环,传输速率可以在每个时钟周期小于3μop(请参考5.2.4章节)。
如果上述因素均未限制程序性能,则您可以将吞吐量目标定为大约每个时钟周期3μop。选择产生最小数量的μop的指令。避免在使用需要多个追踪缓存条目的μops。
5.13.6 追踪缓存大小
使用追踪缓存,由于其本身的特点需要占用更多物理空间,所以使用相同数量的物理芯片空间,追踪缓存可以比传统代码缓存保留更少的代码。如果程序的关键部分不适合追踪高速缓存,则追踪高速缓存的有限空间大小可能是一个严重的瓶颈。
5.13.7 uop退出
退出站每个时钟周期可处理3μop。分支只能由退休站中三个插槽中的第一个插槽处理。如果您希望每个时钟周期的平均吞吐量为3μop,那么请避免过多的跳转,调用和分支。这是老生常谈的问题了,要想快就少拐弯。这让我想起大众的老式1.8T宝来车型,人称直道王,弯道亡。另外小的关键循环中的uops数尽量能被3整除。
5.13.8 指令译码
如果代码的关键部分不适合追踪高速缓存,则流水线中的限制可能是指令译码。译码器可以在每个时钟周期处理一条指令,前提是该指令产生的指令不超过4μops,没有微码,并且前缀不能过多。如果译码是瓶颈,那么应该尝试减少指令数量而不是μop数量。
5.13.9 分支预测
延迟和吞吐量的计算仅在所有分支均得到预测的情况下才有效。当延迟或吞吐量是限制因素时,分支错误预测会严重降低性能。预测错误后P4无法消除已经存在的μops,这会严重降低性能。
避免在代码的关键部分中出现预测不良的分支,即使通过替代方案(例如条件移动),但是这会增加额外依赖关系和延迟。除非有更优秀的替代方案。
5.13.10 uops的重复执行
P4在缓存未命中、store-to-load失败等情况下不断重复并且独占执行预存的μops通常会浪费过多的资源。这可能会导致性能严重下降,尤其是当长依赖链中存在内存操作时。