4

我已经完成了相当多的线程级和进程级并行性,现在我正尝试使用英特尔 C++ 编译器进入指令级并行性,这是一个相当大的挑战。

在对循环进行一些自动矢量化并分析编译器日志时,我发现了一些我无法弄清楚的“估计循环的最大行程计数”。

例子:

double a[100],x[100],y[100]
...
for (i=0; i< 100; i++) {
   a[i] = x[i] + y[i];
}

此循环输出 12 次行程的最大行程计数估计值。我在某处读到,向量化过程每次行程总共可以处理 8 个元素,只要每个循环的过程成本小于 6 个 u 操作,据我所知,这个示例循环的成本为 1存储、2 次读取和 1 次算术运算。

所以理论上,我的旅行次数应该是 100/8 = 12.5 次旅行,因此是 13 次旅行。

这是编译器进行的汇总吗?还是在后台进行任何其他优化以使该过程少于 13 次行程?

还有一个问题,我的每周期 6 次 u 操作假设是否正确?有没有不适用的情况?

提前致谢

4

2 回答 2

3

与其纠结于英特尔如何实现每个循环,不如试着回答您关于指令级并行性的问题。

您的操作受到读取和写入的限制,因此您可以在确定周期数时忽略算术。这是通过 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
于 2015-10-26T11:11:55.623 回答
0

如果不展开,6-uops 听起来像是一个正确的估计。如有必要,英特尔编译器通常可以很好地展开自动矢量化循环。在这种特殊情况下,即使完全展开也可能有意义。

不知道您是如何一次获得 8 个元素的,因为即使使用 AVX,您也只能在单个 256 位ymm寄存器中获得 4 个双精度值。

关于行程数。即使您一次可以处理 8 个元素,它也将是 12 而不是 13,因为最后几个元素(一次不能处理 8 个)将使用标量代码完成。

所以从编译器的角度来看,它看起来像:

int i=0;
for(; i<(100 & ~7); i+=8) // 12 iterations
    // Do vector code

for(;i<100; ++i)
    // Process loop remainder using scalar code
于 2015-10-24T02:51:34.353 回答