简介
本文是一篇后续文章,详细分析了英特尔® 开发人员专区(英特尔® DZ)论坛1上报告的英特尔® C++ 编译器出现的问题2。
一位英特尔开发人员专区用户在代码现代化研讨会上实施了一个简单的程序,检测到了一个内层 for-loop 问题。以下是与问题相关的代码段:
...
for (std::size_t i = 0; i < nb_cluster; ++i) {
float x = point[k].red - centroid[i].red;
float y = point[k].green - centroid[i].green;
float z = point[k].blue - centroid[i].blue;
float distance = std::pow(x, 2) + std::pow(y, 2) + std::pow(z, 2);
if (distance < best_distance) {
best_distance = distance;
best_centroid = i;
}
...
注:它不是来自 KmcTestAppV1.cpp的自动矢量化内层 for-loop。
这位英特尔开发人员专区用户认为无法进行内层 for-loop 自动矢量化的原因是变量“i”被声明为“std::size_t”数据类型,即为“无符号整数”。
附上未改动的源代码 6。请查阅 KmcTestAppV1.cpp以获取更多详细信息。
需要指出的是,本文不是一篇关于矢量化或并行技术的教程,但是,本文下一部分对这些技术进行了简要概述。
矢量化和并行技术简介
现代软件非常复杂,为了实现峰值性能,尤其在数据密集型处理过程中实现峰值性能,需要充分利用现代 CPU 的矢量化和并行功能,现代 CPU 具有多个内核,每个内核上有若干个逻辑处理单元 (LPU) 和矢量处理单元 (VPU)。
VPU 支持在多个数据集值中同时执行不同的操作,这项技术被称为矢量化,与标量或顺序方式相比,利用矢量化部署相同的处理将提升处理的性能。
并行化是另一种技术,支持不同的 LPU 同时处理数据集的不同部分。
将矢量化和并行化结合后,处理性能将显著提升。
通用矢量化规则
您需要考虑以下源代码矢量化通用规则:
- 需要使用配有矢量化支持的现代 C/C++ 编译器。
- 可以使用两种矢量化技术:自动矢量化 (AV) 和显式矢量化 (EV)。
- 只有相对简单的内层 for-loop 才能实现矢量化。
- 某些内层 for-loop 因为使用了复杂的 C 或 C++ 结构(如标准模板库类别或 C++ 操作符),不能通过 AV 或 EV 技术实现矢量化。
- 当现代 C/C++ 编译器无法矢量化内层 for-loop 时,建议审核和分析全部示例。
如何对内层 for-loop 计数器变量实施声明
因为不需要修改代码,所以 AV 技术被认为是实施简单的内层 for-loop 最有效的方法,在使用优化选项“O2”或“O3”时,默认启用现代 C/C++ 编译器的 AV。
在更复杂的示例中,可以使用 EV 的内置函数或矢量化#pragma指令强制执行矢量化,但是需要修改内层 for-loop。
您可能会有这样的疑问:内层 for-loop 计数器变量怎样得以声明?
有两种声明方法供您选择:
案例 A - 变量“i”被声明为“整数”
...
for( int i = 0; i < n; i += 1 )
{
A[i] = A[i] + B[i];
}
...
和
案例 B - 变量“i”被声明为“无符号整数”
...
for( unsigned int i = 0; i < n; i += 1 )
{
A[i] = A[i] + B[i];
}
...
在案例 A中,变量“i”被声明为带符号数据类型“整数”。
在案例 B中,变量“i”被声明为无符号数据类型“无符号整数”。
将案例 A和 B结合到简单的测试程序 3后,可以对 C/C++ 编译器的矢量化功能进行评估:
////////////////////////////////////////////////////////////////////////////////////////////////////
// TestApp.cpp - 为了生成汇编器列表,需要使用选项“-S”。
// Linux:
// icpc -O3 -xAVX -qopt-report=1 TestApp.cpp -o TestApp.out
// g++ -O3 -mavx -ftree-vectorizer-verbose=1 TestApp.cpp -o TestApp.out
// Windows:
// icl -O3 /QxAVX /Qvec-report=1 TestApp.cpp TestApp.exe
// g++ -O3 -mavx -ftree-vectorizer-verbose=1 TestApp.cpp -o TestApp.exe
#include <stdio.h>
#include <stdlib.h>
//
////////////////////////////////////////////////////////////////////////////////////////////////////
typedef float RTfnumber;
typedef int RTiterator; // Uncomment for Test A
typedef int RTinumber;
// typedef unsigned int RTiterator; // Uncomment for Test B
// typedef unsigned int RTinumber;
////////////////////////////////////////////////////////////////////////////////////////////////////
const RTinumber iDsSize = 1024;
////////////////////////////////////////////////////////////////////////////////////////////////////
int main( void )
{
RTfnumber fDsA[ iDsSize ];
RTfnumber fDsB[ iDsSize ];
RTiterator i;
for( i = 0; i < iDsSize; i += 1 )
fDsA[i] = ( RTfnumber )( i );
for( i = 0; i < iDsSize; i += 1 )
fDsB[i] = ( RTfnumber )( i );
for( i = 0; i < 16; i += 1 )
printf( "%4.1f ", fDsA[i] );
printf( "\n" );
for( i = 0; i < 16; i += 1 )
printf( "%4.1f ", fDsB[i] );
printf( "\n" );
for( i = 0; i < iDsSize; i += 1 )
fDsA[i] = fDsA[i] + fDsB[i]; // Line 49
for( i = 0; i < 16; i += 1 )
printf( "%4.1f ", fDsA[i] );
printf( "\n" );
return ( int )1;
}
结果表明,这两种 for-loop(详见上述代码示例的第 49 行)能够轻松进行矢量化 4(使用了带有前缀“v”的指令,如 vmovups、vaddps等),无论怎样对变量“i”进行声明,C++ 编译器生成相同的矢量化报告:
案例 A 案例 B 的矢量化报告
...
Begin optimization report for: main()
Report from:Interprocedural optimizations [ipo]
INLINE REPORT:(main())
Report from:Loop nest, Vector & Auto-parallelization optimizations [loop, vec, par]
LOOP BEGIN at TestApp.cpp(37,2)
remark #25045:Fused Loops:( 37 39 )
remark #15301:FUSED LOOP WAS VECTORIZED
LOOP END
LOOP BEGIN at TestApp.cpp(39,2)
LOOP END
LOOP BEGIN at TestApp.cpp(42,2)
remark #25460:No loop optimizations reported
LOOP END
LOOP BEGIN at TestApp.cpp(45,2)
remark #25460:No loop optimizations reported
LOOP END
LOOP BEGIN at TestApp.cpp(49,2)
remark #15300:LOOP WAS VECTORIZED
LOOP END
LOOP BEGIN at TestApp.cpp(52,2)
remark #25460:No loop optimizations reported
LOOP END
...
矢量化报告4显示第 49 行3 for-loop 实现了矢量化:
...
LOOP BEGIN at TestApp.cpp(49,2)
remark #15300:LOOP WAS VECTORIZED
LOOP END
...
但是,英特尔 C++ 编译器将两个 for-loop 视为不同的 C 语言结构,因此生成不同的矢量化二进制代码。
以下是汇编器列表的两个核心代码段,与两个案例的第 49 行3 for-loop 相关:
案例 A - 汇编器列表(编译 TestApp.cpp 时,需要使用选项“-S”)
...
..B1.12: # Preds ..B1.12 ..B1.11
vmovups (%rsp,%rax,4), %ymm0 #50.13
vmovups 32(%rsp,%rax,4), %ymm2 #50.13
vmovups 64(%rsp,%rax,4), %ymm4 #50.13
vmovups 96(%rsp,%rax,4), %ymm6 #50.13
vaddps 4128(%rsp,%rax,4), %ymm2, %ymm3 #50.23
vaddps 4096(%rsp,%rax,4), %ymm0, %ymm1 #50.23
vaddps 4160(%rsp,%rax,4), %ymm4, %ymm5 #50.23
vaddps 4192(%rsp,%rax,4), %ymm6, %ymm7 #50.23
vmovups %ymm1, (%rsp,%rax,4) #50.3
vmovups %ymm3, 32(%rsp,%rax,4) #50.3
vmovups %ymm5, 64(%rsp,%rax,4) #50.3
vmovups %ymm7, 96(%rsp,%rax,4) #50.3
addq $32, %rax#49.2
cmpq $1024, %rax #49.2
jb ..B1.12 # Prob 99% #49.2
...
注:请参阅 TestApp.icc.itype.s5.1以获取完整的汇编器列表。
案例 B - 汇编器列表(编译 TestApp.cpp 时,需要使用选项“-S”)
...
..B1.12: # Preds ..B1.12 ..B1.11
lea 8(%rax), %edx #50.13
lea 16(%rax), %ecx #50.13
lea 24(%rax), %esi #50.13
vmovups (%rsp,%rax,4), %ymm0 #50.13
vaddps 4096(%rsp,%rax,4), %ymm0, %ymm1 #50.23
vmovups %ymm1, (%rsp,%rax,4) #50.3
addl $32, %eax #49.2
vmovups (%rsp,%rdx,4), %ymm2 #50.13
cmpl $1024, %eax #49.2
vaddps 4096(%rsp,%rdx,4), %ymm2, %ymm3 #50.23
vmovups %ymm3, (%rsp,%rdx,4) #50.3
vmovups (%rsp,%rcx,4), %ymm4 #50.13
vaddps 4096(%rsp,%rcx,4), %ymm4, %ymm5 #50.23
vmovups %ymm5, (%rsp,%rcx,4) #50.3
vmovups (%rsp,%rsi,4), %ymm6 #50.13
vaddps 4096(%rsp,%rsi,4), %ymm6, %ymm7 #50.23
vmovups %ymm7, (%rsp,%rsi,4) #50.3
jb ..B1.12 # Prob 99% #49.2
...
注:请参阅 TestApp.icc.utype.s5.2以获取完整的汇编器列表。
最后明确的一点是,内层 for-loop 无法自动矢量化的问题(详见论坛贴子 1的开头)和变量“i”的声明方式无关,其他因素 影响了英特尔 C++ 编译器的矢量化引擎。
为了查明矢量化问题产生的根源,需要提出一个问题:无法应用 AV 或 EV 技术时,会生成什么样的编译器消息?
AV 或 EV 技术无法应用时,英特尔 C++ 编译器将生成一系列“循环未进行矢量化”信息,信息列表如下所示:
...loop was not vectorized: not inner loop.
...loop was not vectorized: existence of vector dependence.
...loop was not vectorized: statement cannot be vectorized.
...loop was not vectorized: unsupported reduction.
...loop was not vectorized: unsupported loop structure.
...loop was not vectorized: vectorization possible but seems inefficient.
...loop was not vectorized: statement cannot be vectorized.
...loop was not vectorized: nonstandard loop is not a vectorization candidate.
...loop was not vectorized: dereference too complex.
...loop was not vectorized: statement cannot be vectorized.
...loop was not vectorized: conditional assignment to a scalar.
...warning #13379: loop was not vectorized with "simd".
...loop skipped: multiversioned.
一个信息需要特别注意:
...loop was not vectorized: unsupported loop structure.
在 KmcTestAppV1.cpp6中可以看到,内层 for-loop 由 3 个部分组成:
第 1 部分 - 初始化 x、y 和 z 变量
...
float x = point[k].red - centroid[i].red;
float y = point[k].green - centroid[i].green;
float z = point[k].blue - centroid[i].blue;
...
第 2 部分 - 计算点 x、y 和 z 的距离
...
float distance = std::pow(x, 2) + std::pow(y, 2) + std::pow(z, 2);
...
第 3 部分 - 更新“best_distance”变量
...
if (distance < best_distance) {
best_distance = distance;
best_centroid = i;
}
...
由于这些部分全部位于同一个内层 for-loop,英特尔 C++ 编译器的结构无法匹配预定义矢量化模版。然而,带有条件 if 语句的第 3 部分是矢量化问题的根本原因。
解决矢量化问题一个可行方法是将内层 for-loop 划分为以下 3 个部分:
... // Calculate Distance
for( i = 0; i < nb_cluster; i += 1 )
{
float x = point[k].red - centroid[i].red;
float y = point[k].green - centroid[i].green;
float z = point[k].blue - centroid[i].blue; // Performance improvement:( x * x ) is
distance[i] = ( x * x ) + ( y * y ) + ( z * z ); // used instead of std::pow(x, 2), etc
}
// Best Distance
for( i = 0; i < nb_cluster; i += 1 )
{
best_distance = ( distance[i] < best_distance ) ?( float )distance[i] : best_distance;
}
// Best Centroid
for( i = 0; i < nb_cluster; i += 1 )
{
cluster[k] = ( distance[i] < best_distance ) ?( float )i : best_centroid;
}
...
最重要的两个修改和 for-loop 中的条件 if 语句相关。从通用形式:
...
if( A < B )
{
D = val1
C = val3
}
...
修改为利用两个条件运算符的形式 ( ?:):
...
D = ( A < B ) ?( val1 ) :( val2 )
...
C = ( A < B ) ?( val3 ) :( val4 )
...
又称 三元运算符。现在,当代 C/C++ 编译器能使这个 C 语言结构与预定义矢量化模版匹配。
对未修改和经过修改的源代码进行性能评估
通过 1,000,000 个浮点、1,000 个集群和 10 次迭代完成了两版程序的性能评估,结果如下所示:
...>KmcTestAppV1.exe
Time:111.50
注:原始版本6。
...>KmcTestAppV2.exe
Time:20.48
注:优化和矢量化版本7。
与原始版本相比,经过优化和矢量化后,程序7的速度提升了约 5.5倍,(详见 1或 6),节省了数秒时间。
结论
如果当代 C/C++ 编译器无法对 for-loop 进行矢量化,非常有必要评估它的复杂性。在英特尔 C++ 编译器中,需要使用“opt-report=n”选项(n大于 3)。
多数情况下,由于 C/C++ 编译器结构不能与预定义矢量化模板相匹配,C/C++ 编译器无法矢量化 for-loop。例如,以英特尔 C++ 编译器为例,将报告以下矢量化信息:
...loop was not vectorized: unsupported reduction.
或
...loop was not vectorized: unsupported loop structure.
如果出现这种情况,您需要修改 for-loop,以简化其结构,通过 #pragma指令(如 #pragma simd)使用 EV 技术,或通过内置函数重新实施所需的功能。
关于作者
Sergey Kostrov 是一名经验丰富的 C/C++ 软件工程师,也是一名英特尔® 黑带头衔获得者。他是面向嵌入式和台式机平台的高度便携 C/C++ 软件设计和实施领域的专家,也是大数据集科学算法和高性能计算领域的专家。
资料下载
WhatToDoWhenAVFails.zip
全部资料列表(来源、汇编列表和矢量化报告):
KmcTestAppV1.cpp
KmcTestAppV2.cpp
TestApp.cpp
TestApp.icc.itype.rpt
TestApp.icc.utype.rpt
TestApp.icc.itype.s
TestApp.icc.utype.s
另请参阅
1.无符号整数导致矢量化失败?
https://software.intel.com/zh-cn/forums/intel-c-compiler/topic/698664
2.英特尔开发人员专区中的英特尔 C++ 编译器论坛:
https://software.intel.com/zh-cn/forums/intel-c-compiler
3.演示简单的 for-loop 矢量化过程的测试程序:
TestApp.cpp
4.TestApp.cpp 程序的英特尔 C++ 编译器矢量化报告:
TestApp.icc.itype.rpt
TestApp.icc.utype.rpt
5.1.TestApp.cpp程序案例 A的完整汇编器列表:
TestApp.icc.itype.s
5.2.TestApp.cpp程序案例 B的完整汇编器列表:
TestApp.icc.utype.s
6.未经修改的源代码(原始版 KmcTestAppV1.cpp)
7.经过修改的源代码(经过优化和矢量化的 KmcTestAppV2.cpp)