Vtune Assembly Analysis

超算机器用vtune的命令行文件分析

  1. 首先找到vtune程序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    > module load intel/2022.1                                    
    > which icc
    /public1/soft/oneAPI/2022.1/compiler/latest/linux/bin/intel64/icc
    > cd /public1/soft/oneAPI/2022.1
    > find . -executable -type f -name "*vtune*"
    ./vtune/2022.0.0/bin64/vtune-worker-crash-reporter
    ./vtune/2022.0.0/bin64/vtune-gui.desktop
    ./vtune/2022.0.0/bin64/vtune-gui
    ./vtune/2022.0.0/bin64/vtune-agent
    ./vtune/2022.0.0/bin64/vtune-self-checker.sh
    ./vtune/2022.0.0/bin64/vtune-backend
    ./vtune/2022.0.0/bin64/vtune-worker
    ./vtune/2022.0.0/bin64/vtune
    ./vtune/2022.0.0/bin64/vtune-set-perf-caps.sh
  2. vtune-gui获取可执行命令

    1
    /opt/intel/oneapi/vtune/2021.1.1/bin64/vtune -collect hotspots -knob enable-stack-collection=true -knob stack-size=4096 -data-limit=1024000 -app-working-dir /home/shaojiemike/github/IPCC2022first/build/bin -- /home/shaojiemike/github/IPCC2022first/build/bin/pivot /home/shaojiemike/github/IPCC2022first/src/uniformvector-2dim-5h.txt
  3. 编写sbatch_vtune.sh

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #!/bin/bash
    #SBATCH -o ./slurmlog/job_%j_rank%t_%N_%n.out
    #SBATCH -p IPCC
    #SBATCH -t 15:00
    #SBATCH --nodes=1
    #SBATCH --exclude=
    #SBATCH --cpus-per-task=64
    #SBATCH --mail-type=FAIL
    #SBATCH [email protected]

    source /public1/soft/modules/module.sh
    module purge

    module load intel/2022.1

    logname=vtune
    export OMP_PROC_BIND=close; export OMP_PLACES=cores
    # ./pivot |tee ./log/$logname
    /public1/soft/oneAPI/2022.1/vtune/2022.0.0/bin64/vtune -collect hotspots -knob enable-stack-collection=true -knob stack-size=4096 -data-limit=1024000 -app-working-dir /public1/home/ipcc22_0029/shaojiemike/slurm -- /public1/home/ipcc22_0029/shaojiemike/slurm/pivot /public1/home/ipcc22_0029/shaojiemike/slurm/uniformvector-2dim-5h.txt |tee ./log/$logname
  4. log文件如下,但是将生成的trace文件r000hs导入识别不了AMD

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    > cat log/vtune
    dim = 2, n = 500, k = 2
    Using time : 452.232000 ms
    max : 143 351 58880.823709
    min : 83 226 21884.924801
    Elapsed Time: 0.486s
    CPU Time: 3.540s
    Effective Time: 3.540s
    Spin Time: 0s
    Overhead Time: 0s
    Total Thread Count: 8
    Paused Time: 0s

    Top Hotspots
    Function Module CPU Time % of CPU Time(%)
    --------------- ------ -------- ----------------
    SumDistance pivot 0.940s 26.6%
    _mm256_add_pd pivot 0.540s 15.3%
    _mm256_and_pd pivot 0.320s 9.0%
    _mm256_loadu_pd pivot 0.300s 8.5%
    Combination pivot 0.250s 7.1%
    [Others] N/A 1.190s 33.6%

汇编

1
2
objdump -Sd ../build/bin/pivot > pivot1.s
gcc -S -O3 -fverbose-asm ../src/pivot.c -o pivot_O1.s

汇编分析技巧

https://blog.csdn.net/thisinnocence/article/details/80767776

如何设置GNU和Intel汇编语法


vtune汇编实例

(没有开O3,默认值)


偏移 -64 是k

-50 是ki


CDQE复制EAX寄存器双字的符号位(bit 31)到RAX的高32位。

这里的movsdq的q在intel里的64位,相当于使用了128位的寄存器,做了64位的事情,并没有自动向量化。

生成带代码注释的O3汇编代码

如果想把 C 语言变量的名称作为汇编语言语句中的注释,可以加上 -fverbose-asm 选项:

1
gcc -S -O3 -fverbose-asm ../src/pivot.c -o pivot_O1.s
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.L15:
# ../src/pivot.c:38: double dis = fabs(rebuiltCoordFirst - rebuiltCoordSecond);
movsd (%rax), %xmm0 # MEM[base: _15, offset: 0B], MEM[base: _15, offset: 0B]
subsd (%rax,%rdx,8), %xmm0 # MEM[base: _15, index: _21, step: 8, offset: 0B], tmp226
addq $8, %rax #, ivtmp.66
# ../src/pivot.c:38: double dis = fabs(rebuiltCoordFirst - rebuiltCoordSecond);
andpd %xmm2, %xmm0 # tmp235, dis
maxsd %xmm1, %xmm0 # chebyshev, dis
movapd %xmm0, %xmm1 # dis, chebyshev
# ../src/pivot.c:35: for(ki=0; ki<k; ki++){
cmpq %rax, %rcx # ivtmp.66, _115
jne .L15 #,
.L19:
# ../src/pivot.c:32: for(j=i+1; j<n; j++){
addl $1, %esi #, j
# ../src/pivot.c:41: chebyshevSum += chebyshev;
addsd %xmm1, %xmm4 # chebyshev, <retval>
addl %r14d, %edi # k, ivtmp.75
# ../src/pivot.c:32: for(j=i+1; j<n; j++){
cmpl %esi, %r15d # j, n
jg .L13 #,
# ../src/pivot.c:32: for(j=i+1; j<n; j++){
addl $1, %r10d #, j
# ../src/pivot.c:32: for(j=i+1; j<n; j++){
cmpl %r10d, %r15d # j, n
jne .L16 #,

vtune O3汇编分析

原本以为O3是看不了原代码与汇编的对应关系的,但实际可以-g -O3 是不冲突的。

指令的精简合并

  1. 访存指令的合并
    1. r9 mov到 rax里,
      1. leaq (%r12,%r8,8), %r9。其中r12rebuiltCoord,所以r8原本存储的是[i*k]的值
      2. raxrebuiltCoord+[i*k]的地址,由于和i有关,index的计算在外层就计算好了。
    2. rdx的值减去r8存储在rdx
      1. rdx原本存储的是[j*k]的地址
      2. r8原本存储的是[i*k]的值
      3. rdx之后存储的是[(j-i)*k]的地址
    3. data16 nop是为了对齐插入的nop
    1. 值得注意的是取最大值操作,这里变成了maxsd
    2. xmm0缓存值
    3. xmm1chebyshev
    4. xmm2fabs的掩码
    5. xmm4chebyshevSum

自动循环展开形成流水

    1. r14d存储k的值,所以edi存储j*k
    2. Block22后的指令验证了rdx原本存储的是[j*k]的地址
    1. 最外层循环
    2. 因为r14d存储k的值,r8r11d存储了i*k的值

从汇编看不出有该操作,需要开启编译选项

自动向量化

从汇编看不出有该操作,需要开启编译选项

自动数据预取

从汇编看不出有该操作,需要开启编译选项

问题

为什么求和耗时这么多

添加向量化选项

gcc

Baseline

-mavx2 -march=core-avx2

  1. 阅读文档, 虽然全部变成了vmov,vadd的操作,但是实际还是64位的工作。
    1. 这点add rax, 0x8没有变成add rax, 0x16可以体现
    2. 但是avx2不是256位的向量化吗?用的还是xmm0这类的寄存器。
1
2
3
4
5
6
7
8
VADDSD (VEX.128 encoded version)
DEST[63:0] := SRC1[63:0] + SRC2[63:0]
DEST[127:64] := SRC1[127:64]
DEST[MAXVL-1:128] := 0

ADDSD (128-bit Legacy SSE version)
DEST[63:0] := DEST[63:0] + SRC[63:0]
DEST[MAXVL-1:64] (Unmodified)

-march=skylake-avx512

汇编代码表面没变,但是快了10s(49s - 39s)

下图是avx2的

下图是avx512的

猜测注意原因是

  1. nop指令导致代码没对齐
  2. 不太可能和红框里的代码顺序有关

添加数据预取选项

判断机器是否支持

1
2
lscpu|grep pref
3dnowprefetch //3DNow prefetch instructions

应该是支持的

汇编分析

虽然时间基本没变,主要是对主体循环没有进行预取操作,对其余循环(热点占比少的)有重新调整。如下图增加了预取指令

添加循环展开选项

变慢很多(39s -> 55s)

-funroll-loops

汇编实现,在最内层循环根据k的值直接跳转到对应的展开块,这里k是2。

默认是展开了8层,这应该和xmm寄存器总数有关

分析原因

  1. 循环展开的核心是形成计算和访存的流水
    1. 不是简单的少几个跳转指令
    2. 这种简单堆叠循环核心的循环展开,并不能形成流水。所以时间不会减少
  2. 但是完全无法解释循环控制的时间增加
    1. 比如图中cmp的次数应该减半了,时间反而翻倍了

手动分块

由于数据L1能全部存储下,没有提升

手动数据预取

并没有形成想象中预取的流水。每512位取,还有重复。


每次预取一个Cache Line,后面两条指令预取的数据还有重复部分(导致时间增加 39s->61s)


想预取全部,循环每次预取了512位=64字节

手动向量化

avx2

(能便于编译器自动展开来使用所有的向量寄存器,avx2

39s -> 10s -> 8.4s 编译器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
for(i=0; i<n-blockSize; i+=blockSize){
for(j=i+blockSize; j<n-blockSize; j+=blockSize){
for(ii=i; ii<i+blockSize; ii++){
__m256d vi1 = _mm256_broadcast_sd(&rebuiltCoord[0*n+ii]);
__m256d vi2 = _mm256_broadcast_sd(&rebuiltCoord[1*n+ii]);

__m256d vj11 = _mm256_loadu_pd(&rebuiltCoord[0*n+j]); //读取4个点
__m256d vj12 = _mm256_loadu_pd(&rebuiltCoord[1*n+j]);

__m256d vj21 = _mm256_loadu_pd(&rebuiltCoord[0*n+j+4]); //读取4个点
__m256d vj22 = _mm256_loadu_pd(&rebuiltCoord[1*n+j+4]);

vj11 = _mm256_and_pd(_mm256_sub_pd(vi1,vj11), vDP_SIGN_Mask);
vj12 = _mm256_and_pd(_mm256_sub_pd(vi2,vj12), vDP_SIGN_Mask);

vj21 = _mm256_and_pd(_mm256_sub_pd(vi1,vj21), vDP_SIGN_Mask);
vj22 = _mm256_and_pd(_mm256_sub_pd(vi2,vj22), vDP_SIGN_Mask);

__m256d tmp = _mm256_add_pd(_mm256_max_pd(vj11,vj12), _mm256_max_pd(vj21,vj22));
_mm256_storeu_pd(vchebyshev1, tmp);

chebyshevSum += vchebyshev1[0] + vchebyshev1[1] + vchebyshev1[2] + vchebyshev1[3];

// for(jj=j; jj<j+blockSize; jj++){
// double chebyshev = 0;
// int ki;
// for(ki=0; ki<k; ki++){
// double dis = fabs(rebuiltCoord[ki*n + ii] - rebuiltCoord[ki*n + jj]);
// chebyshev = dis>chebyshev ? dis : chebyshev;
// }
// chebyshevSum += chebyshev;
// }
}
}
}

明明展开了一次,但是编译器继续展开了,总共8次。用满了YMM 16个向量寄存器。

下图是avx512,都出现寄存器ymm26了。

vhaddpd是水平的向量内加法指令

avx512

当在avx512的情况下展开4次,形成了相当工整的代码。

  1. 向量用到了寄存器ymm18,估计只能展开到6次了。
    1. avx2 应该寄存器不够

最后求和的处理,编译器首先识别出了,不需要实际store。还是在寄存器层面完成了计算。并且通过三次add和两次数据 移动指令自动实现了二叉树型求和。

avx2 寄存器不够会出现下面的情况。

avx求和的更快速归约

假如硬件存在四个一起归约的就好了,但是对于底层元件可能过于复杂了。


1
2
__m256d _mm256_hadd_pd (__m256d a, __m256d b);
VEXTRACTF128 __m128d _mm256_extractf128_pd (__m256d a, int offset);


如果可以实现会节约一次数据移动和一次数据add。没有分析两种情况的寄存器依赖。可能依赖长度是一样的,导致优化后时间反而增加一点。

对于int还有这种实现

将横向归约全部提取到外面

并且将j的循环展开变成i的循环展开

手动向量化+手动循环展开?

支持的理由:打破了循环间的壁垒,编译器会识别出无效中间变量,在for的jump指令划出的基本块内指令会乱序执行,并通过寄存器重命名来形成最密集的计算访存流水。

不支持的理由:如果编译器为了形成某一指令的流水,占用了太多资源。导致需要缓存其他结果(比如,向量寄存器不够,反而需要额外的指令来写回,和产生延迟。

理想的平衡: 在不会达到资源瓶颈的情况下展开。

支持的分析例子

手动展开后,识别出来了连续的访存应该在一起进行,并自动调度。将+1的偏移编译器提前计算了。

如果写成macro define,可以发现编译器自动重排了汇编。

不支持的分析例子

avx2可以看出有写回的操作,把值从内存读出来压入栈中。

寄存器足够时没有这种问题

寻找理想的展开次数

由于不同代码对向量寄存器的使用次数不同,不同机器的向量寄存器个数和其他资源数不同。汇编也难以分析。在写好单次循环之后,最佳的展开次数需要手动测量。如下图,6次应该是在不会达到资源瓶颈的情况下展开来获得最大流水。

1
2
3
4
5
6
for(j=beginJ; j<n-jBlockSize; j+=jBlockSize){  /
//展开jBlockSize次
}
for(jj=j; jj<n; jj++){ //j初始值继承自上面的循环
//正常单次
}

由于基本块内乱序执行,代码的顺序也不重要。
加上寄存器重命名来形成流水的存在,寄存器名也不重要。当然数据依赖还是要正确。

对于两层循环的双层手动展开

思路: 外层多load数据到寄存器,但是运行的任何时候也不要超过寄存器数量的上限(特别注意在内层循环运行一遍到末尾时)。

左图外层load了8个寄存器,但是右边只有2个。

特别注意在内层循环运行一遍到末尾时:

如图,黄框就有16个了。

注意load的速度也有区别

所以内层调用次数多,尽量用快的

1
2
_mm256_loadu_ps >> _mm256_broadcast_ss > _mm256_set_epi16
0.04 >> 0.5
1
2
3
4
5
6
7
vsub  vmax    ps 0.02      Latency 4
vand Latency 1

vadd ps 0.80 Throughput 0.5
vhadd Latency 7
vcvtps2pd 2.00 Latency 7
vextractf128 0.50 Latency 3

|指令|精度|时间(吞吐延迟和实际依赖导致)|Latency|Throughput
|-|-|-|-|-|-|
|_mm256_loadu_ps /_mm256_broadcast_ss|||7|0.5
|vsub vmax | ps| 0.02 | 4|0.5
vand ||0.02| 1|0.33
vadd |ps |0.80 |4| 0.5
vhadd ||0.8| 7|2
vcvtps2pd || 2.00 | 7|1
vextractf128 || 0.50 | 3|1

向量化double变单精度没有提升

17条avx计算 5load 2cvt 2extract

单位时间 avx计算 load cvt extract
2.33 3.68 12.875 4.1

可见类型转换相当耗费时间,最好在循环外,精度不够,每几次循环做一次转换。

GCC编译器优化

-march=skylake-avx512是一条指令

-mavx2 是两条指令

1
2
vmovupd xmm7, xmmword ptr [rdx+rsi*8]
vinsertf128 ymm1, ymm7, xmmword ptr [rdx+rsi*8+0x10], 0x1

原因是不对齐的访存在老架构上可能更快

O3对于核心已经向量化的代码还有加速吗?

将IPCC初赛的代码去掉O3发现还是慢了10倍。

为什么连汇编函数调用也慢这么多呢?

这个不开O3的编译器所属有点弱智了,一条指令的两个操作数竟然在rbp的栈里存来存去的。

需要进一步的研究学习

暂无

遇到的问题

暂无

开题缘由、总结、反思、吐槽~~

参考文献

Intel Compile Options

Win与Linux的区别

选项区别

对于大部分选项,Intel编译器在Win上的格式为:/Qopt,那么对应于Lin上的选项是:-opt。禁用某一个选项的方式是/Qopt-和-opt-。

Intel的编译器、链接器等

在Win上,编译器为icl.exe,链接器为xilink.exe,VS的编译器为cl.exe,链接器为link.exe。

在Linux下,C编译器为icc,C++编译器为icpc(但是也可以使用icc编译C++文件),链接器为xild,打包为xiar,其余工具类似命名。

GNU的C编译器为gcc,C++编译器为g++,链接器为ld,打包为ar

并行化

-qopenmp

-qopenmp-simd

如果选项 O2 或更高版本有效,则启用 OpenMP* SIMD 编译。

-parallel

告诉自动并行程序为可以安全地并行执行的循环生成多线程代码。

要使用此选项,您还必须指定选项 O2 或 O3。
如果还指定了选项 O3,则此选项设置选项 [q 或 Q]opt-matmul。

-qopt-matmul

启用或禁用编译器生成的矩阵乘法(matmul)库调用。

向量化(SIMD指令集)

-xHost

必须至少与-O2一起使用,在Linux系统上,如果既不指定-x也不指定-m,则默认值为-msse2。

-fast

On macOS* systems: -ipo, -mdynamic-no-pic,-O3, -no-prec-div,-fp-model fast=2, and -xHost

On Windows* systems: /O3, /Qipo, /Qprec-div-, /fp:fast=2, and /QxHost

On Linux* systems: -ipo, -O3, -no-prec-div,-static, -fp-model fast=2, and -xHost

指定选项 fast 后,您可以通过在命令行上指定不同的特定于处理器的 [Q]x 选项来覆盖 [Q]xHost 选项设置。但是,命令行上指定的最后一个选项优先。

-march

必须至少与-O2一起使用,如果同时指定 -ax 和 -march 选项,编译器将不会生成特定于 Intel 的指令。

指定 -march=pentium4 设置 -mtune=pentium4。

-x

告诉编译器它可以针对哪些处理器功能,包括它可以生成哪些指令集和优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
AMBERLAKE
BROADWELL
CANNONLAKE
CASCADELAKE
COFFEELAKE
GOLDMONT
GOLDMONT-PLUS
HASWELL
ICELAKE-CLIENT (or ICELAKE)
ICELAKE-SERVER
IVYBRIDGE
KABYLAKE
KNL
KNM
SANDYBRIDGE
SILVERMONT
SKYLAKE
SKYLAKE-AVX512
TREMONT
WHISKEYLAKE

-m

告诉编译器它可能针对哪些功能,包括它可能生成的指令集。

-ax

生成基于多个指令集的代码。

HLO

High-level Optimizations,高级(别)优化。O1不属于

-O2

更广泛的优化。英特尔推荐通用。

在O2和更高级别启用矢量化。

在使用IA-32体系结构的系统上:执行一些基本的循环优化,例如分发、谓词Opt、交换、多版本控制和标量替换。

此选项还支持:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
内部函数的内联
文件内过程间优化,包括:
内联
恒定传播
正向替代
常规属性传播
可变地址分析
死静态函数消除
删除未引用变量
以下性能增益功能:
恒定传播
复制传播
死码消除
全局寄存器分配
全局指令调度与控制推测
循环展开
优化代码选择
部分冗余消除
强度折减/诱导变量简化
变量重命名
异常处理优化
尾部递归
窥视孔优化
结构分配降低与优化
死区消除

-O3

O3选项对循环转换(loop transformations)进行更好的处理来优化内存访问。

比-O2更激进,编译时间更长。建议用于涉及密集浮点计算的循环代码。

既执行O2优化,并支持更积极的循环转换,如Fusion、Block Unroll和Jam以及Collasing IF语句。

此选项可以设置其他选项。这由编译器决定,具体取决于您使用的操作系统和体系结构。设置的选项可能会因版本而异。

当O3与options-ax或-x(Linux)或options/Qax或/Qx(Windows)一起使用时,编译器执行的数据依赖性分析比O2更严格,这可能会导致更长的编译时间。

O3优化可能不会导致更高的性能,除非发生循环和内存访问转换。在某些情况下,与O2优化相比,优化可能会减慢代码的速度。

O3选项建议用于循环大量使用浮点计算和处理大型数据集的应用程序。

与非英特尔微处理器相比,共享库中的许多例程针对英特尔微处理器进行了高度优化。

-Ofast

-O3 plus some extras.

IPO

Interprocedural Optimizations,过程间优化。

典型优化措施包括:过程内嵌与重新排序、消除死(执行不到的)代码以及常数传播和内联等基本优化。

过程间优化,当程序链接时检查文件间函数调用的一个步骤。在编译和链接时必须使用此标志。使用这个标志的编译时间非常长,但是根据应用程序的不同,如果与-O*标志结合使用,可能会有明显的性能改进。

内联

内联或内联展开,简单理解,就是将函数调用用函数体代替,主要优点是省去了函数调用开销和返回指令的开销,主要缺点是可能增大代码大小。

PGO

PGO优化是分三步完成的,是一个动态的优化过程。

PGO,即Profile-Guided Optimizations,档案导引优化。

具体选项详解

-mtune=processor

此标志对特定的处理器类型进行额外的调整,但是它不会生成额外的SIMD指令,因此不存在体系结构兼容性问题。调优将涉及对处理器缓存大小、指令优先顺序等的优化。

为支持指定英特尔处理器或微体系结构代码名的处理器优化代码。

-no-prec-div

不启用 提高浮点除法的精度。

-static

不用动态库

-fp-model fast=2

自动向量化时按照固定精度,与OpenMP的选项好像有兼容性的问题

-funroll-all-loops

展开所有循环,即使进入循环时迭代次数不确定。此选项可能会影响性能。

-unroll-aggressive / -no-unroll-aggressive

此选项决定编译器是否对某些循环使用更激进的展开。期权的积极形式可以提高绩效。

此选项可对具有较小恒定递增计数的回路进行积极的完全展开。

falign-loops

将循环对齐到 2 的幂次字节边界。

-falign-loops[=n]是最小对齐边界的可选字节数。它必须是 1 到 4096 之间的 2 的幂,例如 1、2、4、8、16、32、64、128 等。如果为 n 指定 1,则不执行对齐;这与指定选项的否定形式相同。如果不指定 n,则默认对齐为 16 字节。

-O0 / -Od

关闭所有优化选项,-O等于-O2 (Linux* and macOS*)

-O1

在保证代码量不增加的情况下编译,

  1. 实现全局优化;这包括数据流分析、代码运动、强度降低和测试替换、分割生存期分析和指令调度。
  2. 禁用某些内部函数的内联。

遇到的问题

1
icpc -dM -E -x c++ SLIC.cpp

https://stackoverflow.com/questions/34310546/how-can-i-see-which-compilation-options-are-enabled-on-intel-icc-compiler

parallel 与mpicc 或者mpiicc有什么区别呢

开题缘由、总结、反思、吐槽~~

讲实话,IPO PGO我已经晕了,我先列个list,之后再研究

参考文献

https://blog.csdn.net/gengshenghong/article/details/7034748

按字母顺序排列的intel c++编译器选项列表