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
*/
基准测试表明,第二个测试速度提高了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;
}
更新:额外的加载/存储可以减少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
       #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 
哇,这很有趣.从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% )
看到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% )
在融合域中,已发布和已退役的时隙匹配CODE2.
| 归档时间: | 
 | 
| 查看次数: | 394 次 | 
| 最近记录: |