与其纠结于英特尔如何实现每个循环,不如试着回答您关于指令级并行性的问题。
您的操作受到读取和写入的限制,因此您可以在确定周期数时忽略算术。这是通过 Broadwell 的 Core2 可以做的:
Core2: two 16 byte reads one 16 byte write per 2 clock cycles -> 24 bytes/clock cycle
SB/IB: two 32 byte reads and one 32 byte write per 2 clock cycles -> 48 bytes/clock cycle
HSW/BDW: two 32 byte reads and one 32 byte write per clock cycle -> 96 bytes/clock cycle
正在读取和写入的字节总数为sizeof(double)*100*3=2400
。所以快速估计需要的时间是
Core2: 2400/24 = 100 clock cycles
SB/IB: 2400/48 = 50 clock cycles
HSW/BDW: 2400/96 = 25 clock cycles
现在的问题是如何实现全带宽。
对于通过 Ivy Bridge 的 Core2,其中一个负载可以与其中一个融合,以增加一个微融合微操作的成本。另一个负载需要一个微操作,而负载需要一个微操作。如果你想在每次迭代中都这样做,你需要减少一个指针并进行条件跳转。由于 Nehalem,这些可以进行宏融合,因此每次迭代的微融合/宏融合操作总数为:
Core2 Nehalem through Broadwell
vector add + load 1 1
vector load 1 1
vector store 1 1
scalar add 1 ½
conditional jump 1 ½
--------------------------------------------
total 5 4
对于通过 Ivy Bridge 的 Core2,要么两个加载都需要相同的端口,要么加载和存储需要相同的端口。这需要两个时钟周期。对于 Haswell/Broadwell,可能会在每个时钟周期内完成此操作。但是,由于端口 7 的限制,只有静态分配的数组可以使用绝对 32 位地址 + 偏移寻址来实现这一点(顺便说一句,这在 OSX 上是不可能的)。因此,对于 Haswell/Broadwell,如果数组不是静态分配的,您要么必须展开循环以在每个时钟周期执行操作,要么每次迭代需要 1.5 个时钟周期。以下是每个处理器每次迭代的时钟周期摘要:
Core2: 5 fused micro-ops/every two clock cycles
SB/IB: 4 fused micro-ops/every two clock cycles
HSW/BDW: 4 fused mirco-ops/every clock cycle for statically allocated array
HSW/BDW: 4 fused mirco-ops/every 1.5 clock cycles for non-statically allocated arrays
如果您使用堆栈分配的数组,您可能可以安全地读取缓冲区的末尾。否则,您应该将数组填充到 SIMD 宽度。那么循环的迭代次数为:
SSE2: (100+1)/2 = 51
AVX: (100+3)/4 = 26
根据我的经验,英特尔编译器会展开两次,这样迭代次数就会减半。展开两次的迭代次数为
SSE2: (100+3)/4 = 26
AVX: (100+7)/8 = 13
最后,就时钟周期而言,它是
Core2: 51*2 = 102 clock cycles
SB/IB: 26*2 = 51 clock cycles
HSW/BDW: 26*1.5 = 39 clock cycles for non-statically allocated arrays no-unroll
HSW/BDW: 26*1 = 26 clock cycles for statically allocated arrays no-unroll
HSW/BDW: 26*1 = 26 clock cycles with full unrolling