使用全向量和 -opt-assume-safe-padding 选项

面向英特尔® MIC 架构的编译器方法

向量化要素,使用全向量和 -opt-assume-safe-padding 选项

高效的向量化需要充分利用向量硬件。这意味着用户应尽可能地在内核向量循环(相对于剥离循环和/或剩余循环)中执行最多的代码。

剩余循环:

当 vec-loop 的 trip-count 不是 vec-length(长度)的倍数时,剩余循环执行剩余的迭代。尽管这在许多时候是无法避免的,在剩余循环上花费大量的时间会降低性能和效率。例如,如果 vec-loop trip-count 是 20 而 vec-length 是 16,则意味着内核循环每次执行时,剩余的 4 次迭代必须在剩余循环中执行。尽管 KNC 编译器能够对剩余循环进行向量化处理(正如 -vec-report6 所报告的),但是其效率却不如内核循环。例如,剩余循环将使用屏蔽,而且不得不使用收集/分散,而不是单位步长加载/存储(由于内存故障保护问题)。解决此问题的最好方法是以下这种方式重构算法/代码,即剩余循环不在运行时执行(使 trip-count 成为 vec-length 的倍数来实现),或者使 trip-count 大于 vec-length(这样便可降低剩余循环中的执行操作的开销)。

此外,编译器优化还需要考虑实际的 trip-count 值。因此,如果 trip-count 是 20,那么相对于 trip-count 的 n(符号值) 在运行时正好是 20 的情况(也许是从一个文件读取的输入值),编译器在事先知道这一点的情况下(对于编译器来说,trip-count 在静态情况下是一个已知的常量)会做出更好的决定。对于前者,您可以在执行循环之前在 C/C++ 中使用 "#pragma loop_count (20)" 或者在 Fortran 编译指示/指令中使用 "CDEC$ LOOP COUNT (20)",以便为编译器提供帮助。

此外还需考虑编译器执行的向量循环展开操作(通过查看 -vec-report6 选项的输出结果)。例如,如果编译器对一个循环(trip-count 是 n;vec-length 是 16)进行向量化处理并展开两倍(向量化以后),那么每个内核循环都要执行原始 src-loop 的 32 次迭代。如果动态 trip-count 正好是 20,内核循环会完全跳过,剩余循环负责所有执行任务。如果您遇到此问题,则可以使用 C/C++ 中的 "#pragma nounroll" 或 Fortran 中的 "CDEC$ NOUNROLL" 来关闭向量循环的展开功能。(您还可以使用上面介绍的 loop_count 编译指示来影响编译器推断)。

如果您希望禁用编译器生成的剩余循环的向量化,则可以在循环之前使用 C/C++ 中的 "#pragma vector novecremainder" 或 Fortran 编译指示/指令中的 "CDEC$ vector noremainder"(这也会禁用编译器针对该循环生成的剥离循环的向量化功能)。您还可以使用编译器内部选项 -mP2OPT_hpo_vec_remainder=F 来禁用剩余循环向量化(针对编译范围内的所有循环)。如果您分析向量循环的汇编代码并且希望从行数明确识别向量内核循环,上述方法通常十分有用(否则,您必须在汇编中仔细筛选循环的多个版本 — 内核/剩余/剥离,以发现自己的目标)。

剥离循环:

编译器通常会生成动态剥离循环以便对齐循环中的一个内存访问。该剥离循环会剥离原始 src-loop 的多个迭代,直到候选内存访问对齐。剥离循环确保有一个小于 vector-length 的 trip-count。该优化的目的是确保内核向量循环能够使用更多的对齐加载/存储指令 — 从而提升内核循环的性能和效率。但是剥离循环本身(即便可被编译器向量化)的效率较低(查看编译器的 -vec-report6 输出结果)。解决此问题的最好方法是通过以下这种方式重构算法/代码,即对齐访问,而且编译器知道采用向量化对齐的常用方法(vectorizer alignment)进行对齐。如果编译器知道所有访问已经对齐(假设用户在循环之前正确使用"#pragma vector aligned",编译器能够安全地假定循环中的所有内存访问已经对齐),那么编译器便不会生成剥离循环。

此外,您还可以使用上面介绍的 loop_count 编译指示来影响编译器是否创建剥离循环的决定。

你可以在 C/C++ 中添加 "#pragma vector unaligned" 或者在 Fortran 编译指示/指令中添加"CDEC$ vector unaligned",以便在源代码中的循环之前命令编译器不要生成动态剥离循环。

您可以结合使用向量编译指示/指令与上面提到的 novecremainder 语句,以便禁用编译器生成的剥离循环的向量化功能。您还可以使用编译器内部选项 -mP2OPT_hpo_vec_peel=F 来禁用剩余循环向量化(针对编译范围内的所有循环)。

示例:

% cat -n t2.c

           1  #include <stdio.h>
           2
           3  void foo1(float *a, float *b, float *c, int n)
           4  {
           5    int i;
           6  #pragma ivdep
           7    for (i=0; i<n; i++) {
           8      a[i] *= b[i] + c[i];
           9    }
          10  }
          11
          12  void foo2(float *a, float *b, float *c, int n)
          13  {
          14    int i;
          15  #pragma ivdep
          16    for (i=0; i<20; i++) {
          17      a[i] *= b[i] - c[i];
          18    }
          19  }

对于第 7 行的循环,编译器生成一个内核向量循环(向量化以后展开两倍)、一个剥离循环和剩余循环,后面两个循环都进行向量化处理。

对于第 16 行的循环,编译器利用 trip-count 是一个常量 (20) 的事实,并生成经过向量化处理的内核循环(未展开)。 剩余循环(4 次迭代)被编译器完全展开(未向量化)。未生成剥离循环。

% icc -O2 -vec-report6 t2.c -c -mmic -inline-level=0
t2.c(8): (col. 5) remark: vectorization support: reference a has aligned access.
t2.c(8): (col. 5) remark: vectorization support: reference a has aligned access.
t2.c(8): (col. 5) remark: vectorization support: reference b has unaligned access.
t2.c(8): (col. 5) remark: vectorization support: reference c has aligned access.
t2.c(8): (col. 5) remark: vectorization support: unaligned access used inside loop body.
t2.c(7): (col. 3) remark: vectorization support: unroll factor set to 2.
t2.c(7): (col. 3) remark: LOOP WAS VECTORIZED.
t2.c(8): (col. 5) remark: vectorization support: reference a has unaligned access.
t2.c(8): (col. 5) remark: vectorization support: reference a has unaligned access.
t2.c(8): (col. 5) remark: vectorization support: reference b has unaligned access.
t2.c(8): (col. 5) remark: vectorization support: reference c has unaligned access.
t2.c(8): (col. 5) remark: vectorization support: unaligned access used inside loop body.
t2.c(7): (col. 3) remark: PEEL LOOP WAS VECTORIZED.
t2.c(8): (col. 5) remark: vectorization support: reference a has aligned access.
t2.c(8): (col. 5) remark: vectorization support: reference a has aligned access.
t2.c(8): (col. 5) remark: vectorization support: reference b has unaligned access.
t2.c(8): (col. 5) remark: vectorization support: reference c has unaligned access.
t2.c(8): (col. 5) remark: vectorization support: reference a has aligned access.
t2.c(8): (col. 5) remark: vectorization support: reference a has aligned access.
t2.c(8): (col. 5) remark: vectorization support: reference b has unaligned access.
t2.c(8): (col. 5) remark: vectorization support: reference c has unaligned access.
t2.c(8): (col. 5) remark: vectorization support: unaligned access used inside loop body.
t2.c(7): (col. 3) remark: REMAINDER LOOP WAS VECTORIZED.
t2.c(17): (col. 5) remark: vectorization support: reference a has unaligned access.
t2.c(17): (col. 5) remark: vectorization support: reference a has unaligned access.
t2.c(17): (col. 5) remark: vectorization support: reference b has unaligned access.
t2.c(17): (col. 5) remark: vectorization support: reference c has unaligned access.
t2.c(17): (col. 5) remark: vectorization support: unaligned access used inside loop body.
t2.c(16): (col. 3) remark: LOOP WAS VECTORIZED.
t2.c(17): (col. 5) remark: vectorization support: reference a has unaligned access.
t2.c(17): (col. 5) remark: vectorization support: reference a has unaligned access.
t2.c(17): (col. 5) remark: vectorization support: reference b has unaligned access.
t2.c(17): (col. 5) remark: vectorization support: reference c has unaligned access.
t2.c(17): (col. 5) remark: vectorization support: unaligned access used inside loop body.
t2.c(16): (col. 3) remark: loop was not vectorized: vectorization possible but seems inefficient.

增加数组的大小并使用选项 -opt-assume-safe-padding 来提高性能:

该选项决定了编译器是否假设该变量以及动态分配的内存被填充到对象的末端。

当指定 -opt-assume-safe-padding 时,编译器假定填充变量以及动态分配的内存。也就是说,该代码可以访问高达 64 字节(超出程序中所指定的)。使用该选项时,编译器不会针对静态和自动对象添加任何填充,但是它可以假定该代码能够访问对象末尾处的高达 64 字节,而无论该对象位于程序中的何处。为满足该假定条件,在使用该选项时,您必须增加程序中静态和自动对象的大小。

1. 此选项可在编译器针对向量剩余循环和向量剥离循环生成的序列中提供帮助。该选项可提高此类循环中内存操作的性能。

如果在上述编译中使用该选项,编译器将会假定数组 a、b 和 c 至少具有 64 字节(超过 n)的填充。

如果使用 malloc 来分配数组,例如:

ptr = (float *)malloc(sizeof(float) * n);

那么用户应对其进行如下更改:

ptr = (float *)malloc(sizeof(float) * n + 64);

完成更改后(满足使用该选项的合法性要求),如果您向上述编译添加该选项,您将在第 7 行处得到针对循环生成的剥离循环的下列(高性能)序列:

..B2.7:                         # Preds ..B2.9 ..B2.6 Latency 13
        vpcmpgtd  %zmm0, %zmm2, %k0                             #7.3 c1
        nop                                                     #7.3 c5
        knot      %k0, %k1                                      #7.3 c9
        jkzd      ..B2.9, %k1   # Prob 20%                      #7.3 c13
                                # LOE rdx rbx rbp rsi rdi r9 r10 r11 r12 r13 r14
 r15 eax ecx r8d zmm0 zmm1 zmm2 zmm3 k1
..B2.8:                         # Preds ..B2.7 Latency 53
        vmovaps   %zmm1, %zmm4                                  #8.13 c1
        vmovaps   %zmm1, %zmm5                                  #8.20 c5
        vmovaps   %zmm1, %zmm6                                  #8.5 c9
        vloadunpacklps (%rsi,%r10,4), %zmm4{%k1}                #8.13 c13
        vloadunpacklps (%rdx,%r10,4), %zmm5{%k1}                #8.20 c17
        vloadunpacklps (%rdi,%r10,4), %zmm6{%k1}                #8.5 c21
        vloadunpackhps 64(%rsi,%r10,4), %zmm4{%k1}              #8.13 c25
        vloadunpackhps 64(%rdx,%r10,4), %zmm5{%k1}              #8.20 c29
        vloadunpackhps 64(%rdi,%r10,4), %zmm6{%k1}              #8.5 c33
        vaddps    %zmm5, %zmm4, %zmm7                           #8.20 c37
        vmulps    %zmm7, %zmm6, %zmm8                           #8.5 c41
        nop                                                     #8.5 c45
        vpackstorelps %zmm8, (%rdi,%r10,4){%k1}                 #8.5 c49
        vpackstorehps %zmm8, 64(%rdi,%r10,4){%k1}               #8.5 c53
        movb      %al, %al                                      #8.5 c53
                                # LOE rdx rbx rbp rsi rdi r9 r10 r11 r12 r13 r14
 r15 eax ecx r8d zmm0 zmm1 zmm2 zmm3
..B2.9:                         # Preds ..B2.7 ..B2.8 Latency 9
        addq      $16, %r10                                     #7.3 c1
        vpaddd    %zmm3, %zmm2, %zmm2                           #7.3 c5
        cmpq      %r11, %r10                                    #7.3 c5
        jb        ..B2.7        # Prob 82%                      #7.3 c9

如果不使用该选项,编译器会使用收集/分散在第 7 行处生成剥离循环的低性能序列。

..B2.7:                         # Preds ..B2.9 ..B2.6 Latency 13
        vpcmpgtd  %zmm0, %zmm2, %k0                             #7.3 c1
        nop                                                     #7.3 c5
        knot      %k0, %k4                                      #7.3 c9
        jkzd      ..B2.9, %k4   # Prob 20%                      #7.3 c13
                                # LOE rax rdx rbx rbp rsi rdi r9 r11 r13 r15 ecx
 r8d r10d zmm0 zmm1 zmm2 zmm3 k4
..B2.8:                         # Preds ..B2.7 Latency 57
        vmovaps   .L_2il0floatpacket.10(%rip), %zmm8            #8.5 c1
        vmovaps   %zmm1, %zmm4                                  #8.13 c5
        lea       (%rsi,%r13), %r14                             #8.13 c5
        vmovaps   %zmm1, %zmm5                                  #8.20 c9
        kmov      %k4, %k2                                      #8.13 c9
..L15:                                                          #8.13
        vgatherdps (%r14,%zmm8,4), %zmm4{%k2}                   #8.13
        jkzd      ..L14, %k2    # Prob 50%                      #8.13
        vgatherdps (%r14,%zmm8,4), %zmm4{%k2}                   #8.13
        jknzd     ..L15, %k2    # Prob 50%                      #8.13
..L14:                                                          #
        vmovaps   %zmm1, %zmm6                                  #8.5 c21
        kmov      %k4, %k3                                      #8.20 c21
        lea       (%rdx,%r13), %r14                             #8.20 c25
        lea       (%rdi,%r13), %r12                             #8.5 c25
..L17:                                                          #8.20
        vgatherdps (%r14,%zmm8,4), %zmm5{%k3}                   #8.20
        jkzd      ..L16, %k3    # Prob 50%                      #8.20
        vgatherdps (%r14,%zmm8,4), %zmm5{%k3}                   #8.20
        jknzd     ..L17, %k3    # Prob 50%                      #8.20
..L16:                                                          #
        vaddps    %zmm5, %zmm4, %zmm7                           #8.20 c37
        kmov      %k4, %k1                                      #8.5 c37
..L19:                                                          #8.5
        vgatherdps (%r12,%zmm8,4), %zmm6{%k1}                   #8.5
        jkzd      ..L18, %k1    # Prob 50%                      #8.5
        vgatherdps (%r12,%zmm8,4), %zmm6{%k1}                   #8.5
        jknzd     ..L19, %k1    # Prob 50%                      #8.5
..L18:                                                          #
        vmulps    %zmm7, %zmm6, %zmm9                           #8.5 c49
        nop                                                     #8.5 c53
..L21:                                                          #8.5
        vscatterdps %zmm9, (%r12,%zmm8,4){%k4}                  #8.5
        jkzd      ..L20, %k4    # Prob 50%                      #8.5
        vscatterdps %zmm9, (%r12,%zmm8,4){%k4}                  #8.5
        jknzd     ..L21, %k4    # Prob 50%                      #8.5
..L20:                                                          #
                                # LOE rax rdx rbx rbp rsi rdi r9 r11 r13 r15 ecx
 r8d r10d zmm0 zmm1 zmm2 zmm3
..B2.9:                         # Preds ..B2.7 ..B2.8 Latency 9
        addq      $16, %rax                                     #7.3 c1
        addq      $64, %r13                                     #7.3 c1
        vpaddd    %zmm3, %zmm2, %zmm2                           #7.3 c5
        cmpq      %r9, %rax                                     #7.3 c5
        jb        ..B2.7        # Prob 82%                      #7.3 c9

2. 另外一个关于使用该选项的示例是处理较短的整数类型转换。在这种情况下,可通过添加选项 -opt-assume-safe-padding 来改进编译器在默认选项下生成的代码。

void foo(short * restrict a, short *restrict b, short * restrict c)
{
   int i;

   for(i = 0; i < N; i++) {
       a[i] = b[i] + c[i];
   }
}

在主内核循环中,编译器为每个加载/存储添加检查功能(防止内存发生故障)。在剩余循环中发出收集/分散指令:

采用默认选项的主内核循环:

..B1.6:
        lea       (%rax,%rsi), %r10
        vloadunpackld (%rax,%rsi){sint16}, %zmm1
        andq      $63, %r10
        cmpq      $32, %r10
        jle       ..L3
        vloadunpackhd 64(%rax,%rsi){sint16}, %zmm1
..L3:
        vprefetch1 256(%rax,%rsi)
        lea       (%rax,%rdx), %r10
        vloadunpackld (%rax,%rdx){sint16}, %zmm2
        andq      $63, %r10
        cmpq      $32, %r10
        jle       ..L4
        vloadunpackhd 64(%rax,%rdx){sint16}, %zmm2
..L4:
        vprefetch0 128(%rax,%rsi)
        vpaddd    %zmm2, %zmm1, %zmm3
        vprefetch1 256(%rax,%rdx)
        vpandd    %zmm0, %zmm3, %zmm4
        vprefetch0 128(%rax,%rdx)
        addq      $16, %rcx
        vprefetch1 256(%rax,%rdi)
        lea       (%rax,%rdi), %r10
        andq      $63, %r10
        cmpq      $32, %r10
        jle       ..L5
        vpackstorehd %zmm4{uint16}, 64(%rax,%rdi)
..L5:
        vpackstoreld %zmm4{uint16}, (%rax,%rdi)
        vprefetch0 128(%rax,%rdi)
        addq      $32, %rax
        cmpq      $992, %rcx
        jb        ..B1.6

编译器采用默认选项时生成的剩余循环:

..L9:
        vpgatherdd 1984(%rdx,%zmm3,2){sint16}, %zmm1{%k2}
        jkzd      ..L8, %k2
        vpgatherdd 1984(%rdx,%zmm3,2){sint16}, %zmm1{%k2}
        jknzd     ..L9, %k2
..L8:
        vpaddd    %zmm1, %zmm0, %zmm2
        vpandd    .L_2il0floatpacket.3(%rip), %zmm2, %zmm4
        nop
..L11:
        vpscatterdd %zmm4{uint16}, 1984(%rdi,%zmm3,2){%k3}
        jkzd      ..L10, %k3    
        vpscatterdd %zmm4{uint16}, 1984(%rdi,%zmm3,2){%k3}
        jknzd     ..L11, %k3    
..L10:

添加选项 -opt-assume-safe-padding 时,编译器为主内核循环和剩余循环生成下列更高性能的版本:

采用-opt-assume-safe-padding 选项的主内核循环:

..B1.6:
        vloadunpackld (%rax,%rsi){sint16}, %zmm1
        vprefetch1 256(%rax,%rsi)
        vloadunpackld (%rax,%rdx){sint16}, %zmm2
        vprefetch0 128(%rax,%rsi)
        vloadunpackhd 64(%rax,%rsi){sint16}, %zmm1
        vprefetch1 256(%rax,%rdx)
        vloadunpackhd 64(%rax,%rdx){sint16}, %zmm2
        vprefetch0 128(%rax,%rdx)
        vpaddd    %zmm2, %zmm1, %zmm3
        vprefetch1 256(%rax,%rdi)
        vpandd    %zmm0, %zmm3, %zmm4
        vprefetch0 128(%rax,%rdi)
        addq      $16, %rcx
        movb      %dl, %dl
        vpackstoreld %zmm4{uint16}, (%rax,%rdi)
        vpackstorehd %zmm4{uint16}, 64(%rax,%rdi)
        addq      $32, %rax
        cmpq      $992, %rcx
        jb        ..B1.6

添加 -opt-assume-safe-padding 选项时的剩余循环(不采用收集/分散的高性能版本):

vloadunpackld 1984(%rsi){sint16}, %zmm0{%k1}
vloadunpackld 1984(%rdx){sint16}, %zmm1{%k1}
vloadunpackhd 2048(%rsi){sint16}, %zmm0{%k1}
vloadunpackhd 2048(%rdx){sint16}, %zmm1{%k1}
vpaddd    %zmm1, %zmm0, %zmm2
vpandd    .L_2il0floatpacket.3(%rip), %zmm2, %zmm3
nop
vpackstoreld %zmm3{uint16}, 1984(%rdi){%k1}
vpackstorehd %zmm3{uint16}, 2048(%rdi){%k1}
movb      %al, %al

下一步

要在英特尔® 至强 融核™ 架构上成功调试您的应用,请务必通读此指南,并点击文中的超链接查看相关内容。本指南提供了实现最佳应用性能所要执行的步骤。

返回到主章节 “向量化要素” 矢量化要素。

有关编译器优化的更完整信息,请参阅优化通知