我使用英特尔®架构代码分析器(IACA)发现了一些意想不到的东西(对我而言).
以下指令使用[base+index]寻址
addps xmm1, xmmword ptr [rsi+rax*1]
Run Code Online (Sandbox Code Playgroud)
根据IACA没有微熔丝.但是,如果我用[base+offset]这样的
addps xmm1, xmmword ptr [rsi]
Run Code Online (Sandbox Code Playgroud)
IACA报告它确实融合了.
英特尔优化参考手册的第2-11节给出了以下"可以由所有解码器处理的微融合微操作"的示例
FADD DOUBLE PTR [RDI + RSI*8]
Run Code Online (Sandbox Code Playgroud)
和Agner Fog的优化装配手册也给出了使用[base+index]寻址的微操作融合的例子.例如,请参见第12.2节"Core2上的相同示例".那么正确的答案是什么?
此循环在英特尔Conroe/Merom上每3个周期运行一次,imul按预期方式在吞吐量方面存在瓶颈.但是在Haswell/Skylake上,它每11个循环运行一次,显然是因为setnz al它依赖于最后一个循环imul.
; synthetic micro-benchmark to test partial-register renaming
mov ecx, 1000000000
.loop: ; do{
imul eax, eax ; a dep chain with high latency but also high throughput
imul eax, eax
imul eax, eax
dec ecx ; set ZF, independent of old ZF. (Use sub ecx,1 on Silvermont/KNL or P4)
setnz al ; ****** Does this depend on RAX as well as ZF?
movzx eax, al
jnz .loop ; }while(ecx);
Run Code Online (Sandbox Code Playgroud)
如果setnz al …
当试图理解汇编(启用编译器优化)时,我看到这种行为:
这样一个非常基本的循环
outside_loop;
while (condition) {
statements;
}
Run Code Online (Sandbox Code Playgroud)
经常被编译成(伪代码)
; outside_loop
jmp loop_condition ; unconditional
loop_start:
loop_statements
loop_condition:
condition_check
jmp_if_true loop_start
; outside_loop
Run Code Online (Sandbox Code Playgroud)
但是,如果未打开优化,则会编译为通常可理解的代码:
loop_condition:
condition_check
jmp_if_false loop_end
loop_statements
jmp loop_condition ; unconditional
loop_end:
Run Code Online (Sandbox Code Playgroud)
根据我的理解,编译后的代码更像是这样的:
goto condition;
do {
statements;
condition:
}
while (condition_check);
Run Code Online (Sandbox Code Playgroud)
我看不到巨大的性能提升或代码可读性提升,为什么经常出现这种情况呢?是否有此循环样式的名称,例如"尾随条件检查"?
我想知道各种大小的循环如何在最近的x86处理器上执行,作为uop数的函数.
以下是彼得·科德斯(Peter Cordes)的一句话,他在另一个问题中提出了非多数的问题:
我还发现,如果循环不是4 uop的倍数,则循环缓冲区中的uop带宽不是每个循环的常数4.(即它是abc,abc,......;不是abca,bcab,......).遗憾的是,Agner Fog的microarch doc对循环缓冲区的这种限制并不清楚.
问题是关于循环是否需要是N uop的倍数才能以最大uop吞吐量执行,其中N是处理器的宽度.(即最近的英特尔处理器为4).在谈论"宽度"和计算微动时,有很多复杂因素,但我大多想忽略这些因素.特别是,假设没有微观或宏观融合.
Peter给出了以下一个循环,其中包含7个uop的循环:
一个7-uop循环将发出4 | 3 | 4 | 3 | ...的组我没有测试更大的循环(不适合循环缓冲区),看看是否有可能从下一个指令开始迭代发布在与其分支相同的组中,但我不假设.
更一般地说,声称是x在其体内具有uops 的循环的每次迭代将至少进行ceil(x / 4)迭代,而不是简单地迭代x / 4.
对于部分或全部最新的x86兼容处理器,这是真的吗?
performance x86 assembly cpu-architecture micro-optimization
在代码的关键部分考虑条件函数调用时,我发现gcc和clang都会在调用中分支.例如,对于以下(通常是微不足道的)代码:
int32_t __attribute__((noinline)) negate(int32_t num) {
return -num;
}
int32_t f(int32_t num) {
int32_t x = num < 0 ? negate(num) : num;
return 2*x + 1;
}
Run Code Online (Sandbox Code Playgroud)
GCC和clang都基本上编译如下:
.global _f
_f:
cmp edi, 0
jg after_call
call _negate
after_call:
lea rax, [rax*2+1]
ret
Run Code Online (Sandbox Code Playgroud)
这让我想到:如果x86有一个像ARM这样的条件调用指令怎么办?想象一下,如果有这样的指令"ccall cc "与语义如cmov cc.然后你可以这样做:
.global _f
_f:
cmp edi, 0
ccalll _negate
lea rax, [rax*2+1]
ret
Run Code Online (Sandbox Code Playgroud)
虽然我们无法避免分支预测,但我们确实消除了分支.也就是说,在实际的GCC/clang输出中,我们被迫分支,无论是否num < 0.如果num < 0我们必须分支两次.这似乎很浪费.
现在这样的指令在amd64中不存在,但我设计了一种模拟这种指令的方法.我通过分解call func其组成部分来做到这一点:( push rip技术上很好[rip+label_after_call_instruction])然后jmp …
我正在使用一个非常简单的循环开始调查我的Haswell端口0上的分支单元的功能:
BITS 64
GLOBAL _start
SECTION .text
_start:
mov ecx, 10000000
.loop:
dec ecx ;|
jz .end ;| 1 uOP (call it D)
jmp .loop ;| 1 uOP (call it J)
.end:
mov eax, 60
xor edi, edi
syscall
Run Code Online (Sandbox Code Playgroud)
使用perf我们看到循环以1c/iter运行
Performance counter stats for './main' (50 runs):
10,001,055 uops_executed_port_port_6 ( +- 0.00% )
9,999,973 uops_executed_port_port_0 ( +- 0.00% )
10,015,414 cycles:u ( +- 0.02% )
23 resource_stalls_rs ( +- 64.05% )
Run Code Online (Sandbox Code Playgroud)
我对这些结果的解释是:
但是,我们也可以看到RS永远不会满员.
它最多可以以2 uOPs/c的速率发送uOP,但理论上可以得到4 …
考虑以下x86-64程序集:
inner:
...
ret
outer:
.top:
call inner
dec rdi
jnz .top
ret
Run Code Online (Sandbox Code Playgroud)
该函数outer只是重复call地对函数inner(其主体未显示) - 它可能是空的.
内部的一系列call指令outer和相应的ret指令是否inner在实践中形成一个依赖链(为了估计性能)?
这条链可以形成多种方式.例如,是否ret依赖于前一条call指令的延迟,然后后续call指令是否依赖于ret,形成call -> ret -> call链?或者也许ret是独立但call不是,形成一个call -> call链?如果有链,是通过内存,寄存器,堆栈引擎,返回地址预测器1还是什么?
动机:这个问题起源于对另一个问题的一系列评论,主要是这个评论和早期评论.
1这里的术语可能有些不清楚:堆栈引擎通常被理解为将转换rsp修改指令处理为具有适当偏移的单个访问,因此push rax; push rbx可能会转换为某些临时寄存器,mov [t0], rax; mov [t0 - 8], …
首先,我在 IvyBridge 上进行了以下设置,我将在注释位置插入测量有效负载代码。前 8 个字节buf存储buf自身的地址,我用它来创建循环携带依赖:
section .bss
align 64
buf: resb 64
section .text
global _start
_start:
mov rcx, 1000000000
mov qword [buf], buf
mov rax, buf
loop:
; I will insert payload here
; as is described below
dec rcx
jne loop
xor rdi, rdi
mov rax, 60
syscall
Run Code Online (Sandbox Code Playgroud)
我插入到有效载荷位置:
mov qword [rax+8], 8
mov rax, [rax]
Run Code Online (Sandbox Code Playgroud)
perf显示循环为 5.4c/iter。有点理解,因为L1d延迟是4个周期。
我颠倒了这两条指令的顺序:
mov rax, [rax]
mov qword [rax+8], 8
Run Code Online (Sandbox Code Playgroud)
结果突然变成9c/iter。我不明白为什么。因为下一次迭代的第一条指令不依赖于当前迭代的第二条指令,所以这个设置应该和 case 1 没有区别。
我也用IACA工具对这两种情况进行静态分析,但是该工具不可靠,因为两种情况预测的结果都是5.71c/iter,与实验相矛盾。 …
x86 assembly micro-optimization microbenchmark micro-architecture
assembly ×8
x86 ×7
performance ×3
intel ×2
cpu ×1
iaca ×1
intel-pmu ×1
loops ×1
optimization ×1
x86-64 ×1