英特尔自动矢量化行程计数解释?
Posted
技术标签:
【中文标题】英特尔自动矢量化行程计数解释?【英文标题】:Intel Auto-Vectorization Trip Count Explanation? 【发布时间】:2016-01-23 00:53:47 【问题描述】:我已经完成了相当多的线程级和进程级并行性,现在我正在尝试使用英特尔 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 操作的假设是否正确?有没有不适用的情况?
提前致谢
【问题讨论】:
您介意添加一个。我可以编译的代码,b。用于编译的命令行,b。操作系统/编译器版本信息,c。您指的是诊断吗?注意:向量化和向量化都是正确的(第一个更常见的恕我直言),只要尝试坚持一个:) 这是用于什么硬件的? 【参考方案1】:与其纠结于英特尔如何实现每个循环,不如尝试回答您有关指令级并行性的问题。
您的操作受到读取和写入的限制,因此您可以在确定周期数时忽略算术。这是 Core2 通过 Broadwell 可以做的:
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,其中一个负载可以与其中一个负载融合,以增加一个微融合微操作的成本。另一个负载需要一个微操作,而负载需要一个微操作。如果您想在每次迭代中执行此操作,请need to decrease a pointer and do a conditional jump as well。由于 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,可能会在每个时钟周期内完成此操作。但是,due to limitations on port 7 only statically allocated arrays can achieve this 使用绝对 32 位地址 + 偏移寻址 (which incidentally is not possible on 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
【讨论】:
Port7 可以处理单寄存器寻址模式。 gcc 可以将循环中的数组引用编译为递增指针,而不是使用 [r + r*scale] 寻址模式。所以haswell和后来的CPU都很好。另请注意,如果某些操作为 32B,则 SnB/IvB 只能实现每个时钟周期 2*16B 读取 + 16B 写入。哦,OP使用的是double
,不是整数类型,所以没有AVX2的AVX1可以做32B ALU运算。
@PeterCordes,“gcc 可以在循环中将数组引用编译为递增指针,而不是使用 [r + r*scale] 寻址模式”到底是什么意思。我对汇编和寻址模式有点不习惯。
@PeterCordes,我指的是我在here 所做的三元组函数。在 OPs 的情况下,他只有两个基本指针,所以我猜每次迭代只需要五个微操作。这仍然超过 4,但也许它比不使用 port7 更有效。我没想到。
@PeterCordes,所以即使我将lea
用于存储,我仍然存在带负载的 ALU 使用两个寄存器寻址模式的问题,因此它不会融合。另一个lea
显然无济于事......所以我认为在商店中使用lea
实际上是6 个融合的微操作,因为vfmadd231ps ymm1, ymm2, [rsi+rax]
不能融合。
@PeterCordes,展开 16 次获得 94%。【参考方案2】:
如果不展开,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
【讨论】:
我的经验是英特尔展开两次,这样可以解释两倍的因子(每次迭代八个元素)。以上是关于英特尔自动矢量化行程计数解释?的主要内容,如果未能解决你的问题,请参考以下文章
如果在 Xeon Phi 上编译时不知道循环计数,则性能下降
任务是使用 p 线程并行化矩阵乘法并使用英特尔 ISPC 编译器进行矢量化