Isk*_*pov 1 performance x86 assembly intel
我不知道任何真正的汇编,但可以读取GCC -S输出来评估给定C代码的实际成本.
这个问题并不是关于分析和基准的问题,而是教育问题.我需要有人来解释为什么[1]片段不比第二片段快.
嗯,过去常常这样想:"是的,像MUL这样的操作非常昂贵,但是如果一个组件比另一个组件大X倍,它应该更慢".
在我遇到这两个之前,这是真的:
unsigned char bytes[4] = {0, 0, 0, 5};
// 1
int32_t val = *((int32_t*)bytes);
/* produces:
leaq -16(%rbp), %rax
movl (%rax), %eax
movl %eax, -4(%rbp)
movl $0, %eax
*/
// 2
val = bytes[3] |
(bytes[2] << 8) |
(bytes[1] << 16) |
(bytes[0] << 24);
/* produces:
movzbl -13(%rbp), %eax
movzbl %al, %eax
movzbl -14(%rbp), %edx
movzbl %dl, %edx
sall $8, %edx
orl %eax, %edx
movzbl -15(%rbp), %eax
movzbl %al, %eax
sall $16, %eax
orl %eax, %edx
movzbl -16(%rbp), %eax
movzbl %al, %eax
sall $24, %eax
orl %edx, %eax
movl %eax, -4(%rbp)
movl $0, %eax
*/
Run Code Online (Sandbox Code Playgroud)
基准测试表明,第二个测试速度提高了5-10%.这里发生了什么?
我能想象的唯一显着差异和"理由" LEAQ是非常缓慢的.最后两行是相同的,所以可能MOV价格太高,1额外MOV比指令吨更糟.
这是我用来衡量执行时间的内容:
#include <stdio.h>
#include <time.h>
#include <stdint.h>
#define REPETITIONS 32
#define ITERATIONS 90000
#define CODE1 \
for (int i = 0; i < ITERATIONS; ++i) { \
val = *((int32_t*)bytes); \
}
#define CODE2 \
for (int i = 0; i < ITERATIONS; ++i) { \
val = bytes[3] | \
(bytes[2] << 8) | \
(bytes[1] << 16) | \
(bytes[0] << 24); \
}
int main(void) {
clock_t minTs = 999999999, ts;
unsigned char bytes[4] = {0, 0, 0, 5};
int32_t val;
for (int i = 0; i < REPETITIONS; ++i) {
ts = clock();
CODE1; // Or CODE2
ts = clock() - ts;
if (ts < minTs) minTs = ts;
}
printf("ts: %ld\n", minTs);
return val;
}
Run Code Online (Sandbox Code Playgroud)
更新:额外的加载/存储可以减少Sandybridge系列的存储转发延迟,因此CODE2中的额外工作可能加速了循环承载的依赖链,这两个循环都会出现瓶颈.(因为-O0将循环计数器保留在内存中).
这种效应存在于所有SnB系列CPU中,包括我在编写此答案时使用的Sandybridge,OP的Haswell,Broadwell in 添加冗余分配,在没有优化的情况下编译时加速代码,而Skylake in Loop中的函数调用速度比一个空循环.
如果您对代码如何编译为asm感到好奇,请将代码放在无法优化的函数中.在这个godbolt链接上,你可以看到gcc 5.1和更新(at -O3 -march=native -mtune=native)如何通过ORing一起看到字节并movbe在加载时使用(移动big-endian)动态bwap.icc,clang和较旧的gcc发出加载单个字节并将它们移位/移位的指令.
我很失望编译器没有看到字节的ORing,即使我颠倒了命令来执行小端加载而不是大端加载.(请参阅godbolt上的native,big和little endian函数.)即使将类型更改为uint32_t和uint8_t也不会对gcc> = 5.1以外的编译器有所帮助.
显然,在优化的情况下,编译器会丢弃仅设置未使用变量的循环.他们只调用clock两次,printf,然后立即将答案移入eax.如果你想实际对某些东西进行基准测试,可以将它与将使用常量数据调用它的函数分开编译.这样,您可以使用简单的代码将其工作作为函数参数,并且无法将内联函数内联到传递给它的常量数据.
此外,gcc将其main视为"冷"功能,并不像普通功能那样对其进行优化.在大多数程序中,main不包含内部循环.
-O0asm这么慢?显然,代码是可怕的从-O0,存储到存储器中,并且甚至递增循环计数器在存储器中.有趣的是弄清楚为什么它的运行速度比我想象的要慢,CODE1每时钟下一个insn.
你没有显示任何一段代码的整个循环.可能移除循环体仍然会留下缓慢的循环.我认为循环本身就是问题,并且效率非常低,以至于CPU有时间在CODE2中执行所有额外指令而不会看到任何减速.
TL; DR:两个循环都受到瓶颈的影响add $1, -0x24(%rbp),这会使内存中的循环计数器递增.循环携带依赖链中的6个循环延迟解释了为什么两个循环都以相同的速度瓶颈.
我不确切地知道为什么CODE2中的额外指令以某种方式帮助接近每次迭代理论最大值的6个周期,但这不是任何人都会编写的代码中应该出现的瓶颈.将循环计数器保留在寄存器中,并且不要在循环携带的依赖链中包含相同地址的读 - 修改 - 写指令.(每次迭代在不同地址递增内存很好,例如对于CountingSort.)
有关我对代码所做的更改,请参阅godbolt.(随着ITERATIONS增加了100倍,因此运行时控制了启动开销的噪音.)该链接已启用优化,这有利于前3个功能.
godbolt没有C模式,只有C++,而且我从gcc 4.9.2本地获得了一个比godbolt节目更糟糕的循环.(g ++ for(){}完全按照写入的方式实现一个循环,顶部有一个cmp/jcc,底部有一个jmp.gcc 甚至在-O0使用一个do{} while(count++ < ITERATIONS);结构时,底部只有一个cmp/jle.
我不知道任何真正的汇编,但可以读取GCC -S输出来评估给定C代码的实际成本.
嗯,过去常常这样想:"是的,像MUL这样的操作非常昂贵,但是如果一个组件比另一个组件大X倍,它应该更慢".
首先要考虑的是吞吐量和延迟瓶颈.在Intel上,这意味着每个时钟吞吐量为4 uop,如果长依赖链是一个限制,则更少.然后是每执行端口的瓶颈.例如,每个时钟有两个存储器操作,其中至多一个是存储器.mul每个时钟最多一个,因为3个ALU端口中只有一个具有整数乘法器.
有关优化指南,微架构文档以及可运行的指令延迟/吞吐量/ uops /端口表,请参阅Agner Fog的站点.
通过将循环计数器保留在内存中,您的循环严重受到瓶颈.在SandyBridge(我的系统)和Haswell(你的)上,Agner Fog的表具有add6个时钟的内存目标的延迟.没有办法比每次迭代每6个时钟一次迭代运行得更快.循环中有6条指令,每循环1个insn.
在实践中,我的吞吐量要低于此.也许商店作为add读取 - 修改 - 写入操作的一部分有时会被循环中的其他加载/存储延迟.IDK为什么CODE2略快,这很奇怪.也许它以不同的方式对事物进行排序,因此循环携带依赖性add的延迟较少.
循环体使用lea和一个32位的负载明显快.IDK为什么你认为这lea很慢.
这不是一个对齐/ uop-cache问题.循环应该从循环缓冲区流出,即使在32B代码块中有超过18个uop(意味着它不能进入uop缓存).当我们的每个时钟的insns如此之低时,前端瓶颈(除了分支错误预测,我们没有)不会成为问题.前端可以很容易地保留大量的uops排队等待调度程序发送.
从中perf report,分析每条指令的时钟:CODE1的内部循环.计数不是周期精确的.我们很可能看到贴在右边的指令的CPU 之后的add $1, mem,这一点我敢肯定是瓶颈循环携带依赖性.它需要在下一次迭代时将存储转发到负载,这仍需要6个时钟.
###### CODE1 inner loop, profiling on cycles
13.97 ?400680: lea -0x10(%rbp),%rax
?400684: mov (%rax),%eax
?400686: mov %eax,-0x2c(%rbp)
?400689: addl $0x1,-0x24(%rbp)
13.84 ?40068d: cmpl $0x89543f,-0x24(%rbp)
72.19 ?400694: ? jle 400680 <code1+0x4a>
## end of the loop
400696: callq 4004e0 <clock@plt>
40069b: sub -0x18(%rbp),%rax
Run Code Online (Sandbox Code Playgroud)
#CODE2
15.08 ?400738: movzbl -0xd(%rbp),%eax
0.88 ?40073c: movzbl %al,%eax
0.05 ?40073f: movzbl -0xe(%rbp),%edx
?400743: movzbl %dl,%edx
13.91 ?400746: shl $0x8,%edx
0.70 ?400749: or %eax,%edx
0.05 ?40074b: movzbl -0xf(%rbp),%eax
?40074f: movzbl %al,%eax
12.70 ?400752: shl $0x10,%eax
0.60 ?400755: or %eax,%edx
0.09 ?400757: movzbl -0x10(%rbp),%eax
0.05 ?40075b: movzbl %al,%eax
13.03 ?40075e: shl $0x18,%eax
0.70 ?400761: or %edx,%eax
0.14 ?400763: mov %eax,-0x2c(%rbp)
0.09 ?400766: addl $0x1,-0x24(%rbp)
13.63 ?40076a: cmpl $0x89543f,-0x24(%rbp)
28.29 ?400771: ? jle 400738 <code2+0x4a>
## end of the loop
400773: ? callq 4004e0 <clock@plt>
400778: sub -0x18(%rbp),%rax
Run Code Online (Sandbox Code Playgroud)
哇,这很有趣.从8位内存位置movzbl %al, %eax加载后,gcc执行冗余.%eaxmovzbl
因此,每次迭代6个时钟,CPU可以处理所有忙于加载组合字节的工作吗?是.
movzx reg, mem:4个加载端口uops.(P2/P3)movzx reg, reg任意ALU端口4x :4 uops(p015)shl reg, imm:ALU端口p0/p5为3 uopsor reg, reg:任何ALU端口均为3 uop(p015)mov mem, reg:1 uop融合域:1个存储数据(p4),1个存储地址(p23)add mem, imm:2个融合域.unfused:1 ALU uop(p015),1 load uop(p23),1 store-data(p4),1 store-address(p23)cmp mem, imm:p015为1 uop,p23为1.jlepx为1x :1 uop.(不能用cmp进行宏观融合,因为imm和mem)总融合域uops:4 + 4 + 3 + 3 + 1 + 2 + 1 + 1 = 19.这适用于28uop循环流缓冲区,避免了uop-cache瓶颈的任何可能性,并且可以在5个时钟周期内发出.(每个循环4个,最后一个循环仅发出3个循环).
加载uops:4 + 1 + 1 = 6.存储uops:2.
ALU uops:4 + 3 + 3 + 1 + 1 + 1 = 13.SnB的3个ALU uop端口可以在4.33个时钟内处理.大多数uop可以在任何端口上运行,因此没有一个端口是瓶颈.(Haswell有一个第四个ALU端口,p6.它有一个更容易的时间.但ALU uops不是瓶颈.)
循环体的延迟无关紧要,因为下一次迭代不会读取任何结果.每次迭代都会读取一些数据并将其存储,与之前的迭代无关.许多循环都是这样的.通常情况下,这样的循环每次都会从不同的地址加载并存储到不同的地址,但CPU只是按照它所说的去做.
无论如何,即使每个循环中的依赖链花费超过6个时钟,多次迭代的工作也可以在飞行中.除了具有内存目标的循环计数器增量外,一次迭代中的任何内容都不必等待前一次迭代.
因此,CODE2循环中的所有工作根本不是瓶颈.
对于SnB/HSW,具有内存目标的add-immediate是2 uops,而内存目标上的inc是3,根据Agner Fog的表,这很奇怪.我想知道这是一个错误,还是英特尔CPU在使用inc内存目的地而不是add $1?
测试时间(来自gcc 4.9.2).我没有看到任何明显可以解释为什么CODE2接近每6个时钟一次迭代的理论最大值的原因.我唯一的猜测是CODE1在call之后被右边的混淆jle,但CODE1不是?可能是uops的性能记录
Sandybridge i5(2500k):
## CODE1 ##
Performance counter stats for './a.out 1' (4 runs):
589.117019 task-clock (msec) # 0.999 CPUs utilized ( +- 1.30% )
2,068,118,847 cycles # 3.511 GHz ( +- 0.48% )
1,729,505,746 instructions # 0.84 insns per cycle
# 0.86 stalled cycles per insn ( +- 0.01% )
2,018,533,639 uops_issued_any # 3426.371 M/sec ( +- 0.02% )
5,648,003,370 uops_dispatched_thread # 9587.235 M/sec ( +- 2.51% )
3,170,497,726 uops_retired_all # 5381.779 M/sec ( +- 0.01% )
2,018,126,488 uops_retired_retire_slots # 3425.680 M/sec ( +- 0.01% )
1,491,267,343 stalled-cycles-frontend # 72.11% frontend cycles idle ( +- 0.66% )
27,192,780 stalled-cycles-backend # 1.31% backend cycles idle ( +- 68.75% )
0.589651097 seconds time elapsed ( +- 1.32% )
Run Code Online (Sandbox Code Playgroud)
看到uops_dispatched_thread与uops_retired_all不匹配是非常不寻常的.它们通常都是相同的,并且等于循环中指令的未融合微指令的数量.Fused-domain uops_issued_any和uops_retired_retire_slots通常都是相等的,在这种情况下它们是相同的.也许内存目的地ALU ops在dispatched与retired_all中的计数方式不同?(微融合).我认为我以前的测试只关注负载的微观融合.
我认为它不会发布不需要的微博.(这不是分支错误预测的问题;我检查过,两个版本都有0.00%的分支误预测.(288M分支只有~10k).)
## CODE2 ##
peter@tesla:~/src/SO$ ocperf.py stat -r4 -e task-clock,cycles,instructions,uops_issued.any,uops_dispatched.thread,uops_retired.all,uops_retired.retire_slots,stalled-cycles-frontend,stalled-cycles-backend ./a.out 2
perf stat -r4 -e task-clock,cycles,instructions,cpu/event=0xe,umask=0x1,name=uops_issued_any/,cpu/event=0xb1,umask=0x1,name=uops_dispatched_thread/,cpu/event=0xc2,umask=0x1,name=uops_retired_all/,cpu/event=0xc2,umask=0x2,name=uops_retired_retire_slots/,stalled-cycles-frontend,stalled-cycles-backend ./a.out 2
CODE2: ts: 16499
CODE2: ts: 16535
CODE2: ts: 16517
CODE2: ts: 16529
Performance counter stats for './a.out 2' (4 runs):
543.886795 task-clock (msec) # 0.999 CPUs utilized ( +- 1.01% )
2,007,623,288 cycles # 3.691 GHz ( +- 0.01% )
5,185,222,133 instructions # 2.58 insns per cycle
# 0.11 stalled cycles per insn ( +- 0.00% )
5,474,164,892 uops_issued_any # 10064.898 M/sec ( +- 0.00% )
7,586,159,182 uops_dispatched_thread # 13948.048 M/sec ( +- 0.00% )
6,626,044,316 uops_retired_all # 12182.764 M/sec ( +- 0.00% )
5,473,742,754 uops_retired_retire_slots # 10064.121 M/sec ( +- 0.00% )
566,873,826 stalled-cycles-frontend # 28.24% frontend cycles idle ( +- 0.03% )
3,744,569 stalled-cycles-backend # 0.19% backend cycles idle ( +- 2.32% )
0.544190971 seconds time elapsed ( +- 1.01% )
Run Code Online (Sandbox Code Playgroud)
在融合域中,已发布和已退役的时隙匹配CODE2.
| 归档时间: |
|
| 查看次数: |
394 次 |
| 最近记录: |