GCC自动向量化即使在理论上是“有利可图”的时候,对运行时间也没有影响。

8
我最近花了几天时间学习gcc 4.7的自动向量化。我按照网上看到的一些例子设置好环境,但是当我实际运行代码并在开启或关闭向量化后进行比较时,发现运行时间没有明显的差异。
以下是我使用的代码:
#include <string.h>
#include <stdlib.h>
#include <emmintrin.h>
#include <stdio.h>
#include <math.h>

int main(int argc, char** argv) {

    long b = strtol(argv[2], NULL, 0); 
    unsigned long long int i;
    unsigned long long int n = (int)pow(2,29);                                                                                                                                                                                            
    float total = 0;

    float *__restrict__ x1; 
    float *__restrict__ y1; 

    posix_memalign((void *)&x1, 16, sizeof(float)*n);
    posix_memalign((void *)&y1, 16, sizeof(float)*n);


    float *__restrict__ x = __builtin_assume_aligned(x1,16);
    float *__restrict__ y = __builtin_assume_aligned(y1,16);

    for (i=0;i<n;i++) {
            x[i] = i;
            y[i] = i;
    }   

    for (i=0; i<n; i++) {
            y[i] += x[i];
    }   

    printf("y[%li]: \t\t\t\t%f\n",  b,y[b]);
    printf("correct answer: \t\t\t%f\n", (b)*2);
    return 0;
}

对我来说,这些东西有些多余,但是为了让编译器理解发生了什么(特别是数据对齐的事实),这些都是必要的。从命令行读取的“b”变量只是因为我担心编译器会完全优化掉循环。

当启用向量化时,以下是编译器命令:

gcc47 -ftree-vectorizer-verbose=3 -msse2 -lm -O2 -finline-functions -funswitch-loops -fpredictive-commoning -fgcse-after-reload -fipa-cp-clone test.c -ftree-vectorize -o v

基本上,这相当于只使用 -O3。我自己添加了标志,这样我只需要删除“ftree-vectorize”,就能够测试不带向量化的结果。
以下是 ftree-vectorize-verbose 标志的输出,以显示代码确实正在进行向量化:
Analyzing loop at test.c:29

29: vect_model_load_cost: aligned.
29: vect_model_load_cost: inside_cost = 1, outside_cost = 0 .
29: vect_model_load_cost: aligned.
29: vect_model_load_cost: inside_cost = 1, outside_cost = 0 .
29: vect_model_simple_cost: inside_cost = 1, outside_cost = 0 .
29: vect_model_store_cost: aligned.
29: vect_model_store_cost: inside_cost = 1, outside_cost = 0 .
29: cost model: Adding cost of checks for loop versioning aliasing.

29: Cost model analysis: 
  Vector inside of loop cost: 4
  Vector outside of loop cost: 4
  Scalar iteration cost: 4
  Scalar outside cost: 1
  prologue iterations: 0
  epilogue iterations: 0
  Calculated minimum iters for profitability: 2

29:   Profitability threshold = 3


Vectorizing loop at test.c:29

29: Profitability threshold is 3 loop iterations.
29: created 1 versioning for alias checks.

29: LOOP VECTORIZED.
Analyzing loop at test.c:24

24: vect_model_induction_cost: inside_cost = 2, outside_cost = 2 .
24: vect_model_simple_cost: inside_cost = 2, outside_cost = 0 .
24: not vectorized: relevant stmt not supported: D.5806_18 = (float) D.5823_58;

test.c:7: note: vectorized 1 loops in function.

请注意,向量化在3次迭代后是有益的,我正在运行2^29~=500,000,000次迭代。因此,如果关闭向量化,我应该期望运行时间大不相同,对吧?
好的,这里是代码的运行时间(我连续运行了20次):
59.082s                                                                                                                                                                                                                                       
79.385s
57.557s
57.264s
53.588s
54.300s
53.645s
69.044s
57.238s
59.366s
56.314s
55.224s
57.308s
57.682s
56.083s
369.590s
59.963s
55.683s
54.979s
62.309s

除去那个奇怪的 370 秒的异常值,平均运行时间为 58.7 秒,标准差为 6.0 秒。

接下来,我将使用与之前相同的命令进行编译,但不使用 -ftree-vectorize 标志:

gcc47 -ftree-vectorizer-verbose=3 -msse2 -lm -O2 -finline-functions -funswitch-loops -fpredictive-commoning -fgcse-after-reload -fipa-cp-clone test.c -o nov

再次连续运行程序20次,得到以下时间:

69.471s                                                                                                                                                                                                                                       
57.134s
56.240s
57.040s
55.787s
56.530s
60.010s
60.187s
324.227s
56.377s
55.337s
54.110s
56.164s
59.919s
493.468s
63.876s
57.389s
55.553s
54.908s
56.828s

再次排除异常值,这给出了平均运行时间为57.9秒,标准偏差为3.6秒。
因此,这两个版本的运行时间在统计上无法区分。
有人能告诉我我做错了什么吗?编译器输出的“盈利门槛”不是我想象的意思吗?我非常感谢大家能给我任何帮助,我过去一周一直在试图弄清楚这个问题。
编辑:我实现了@nilspipenbrinck建议的更改,似乎已经奏效。我将向量化循环放入函数中,并多次调用该函数。没有向量化的相对运行时间现在为24.0秒(小于0.1秒的sigma),向量化的相对运行时间为20.8秒(小于0.2秒的sigma),或者速度提高了13%。虽然不如我所希望的那样快,但至少现在我知道它正在工作!感谢您抽出时间查看我的问题并写出答案,我非常感激。

有没有特别的原因要在过时的编译器版本上进行测试?你的硬件是什么? - Ivan Aksamentov - Drop
1
参考局部性在现代处理器中非常重要。如果您的程序没有任何参考局部性,那么它的运行时间将完全受到访问RAM的成本支配。 - Hans Passant
@Drop 我计划在实验室的许多不同电脑上运行此程序,并且它们都使用这个编译器。实际上,它们使用的版本比这个还要旧,gcc4.4或更低版本。但是我想尝试的一些函数直到4.7才出现。我没有升级编译器的能力,否则我会这样做的。 - user2635263
@HansPassant 感谢您的建议,我现在正在查阅关于引用位置的信息。 - user2635263
这是一个重复的问题 https://dev59.com/Eeo6XIcBkEYKwwoYLhbh#18159503 - Z boson
2个回答

5
您并不需要做太多的算术运算,因此您的测试代码的运行时间取决于内存绑定。例如,您会花费大部分时间在CPU和内存之间移动数据。

此外,您的n非常大,具有2^29个元素,因此您无法从第一级和第二级高速缓存中受益。

如果您想使用SSE看到改进,请使用较小的n,使您仅触摸8或16千字节的数据。还要确保数据是“热的”,例如最近已被CPU访问。这样,数据就不必从主内存中移动,而是从缓存中移动,这将快几个数量级。

作为替代方案,您也可以执行更多的算术运算。这将为内存预取系统提供机会,在您利用CPU进行数学计算时,从主内存中后台获取数据。

总结:如果算术比您的系统移动内存的能力更快,则您将看不到任何好处。内存访问时间将成为瓶颈,并且使用SSE指令集节省的少量周期将在内存访问时间的噪音中丢失。


啊,这很有道理。在8千字节的数据上操作不会真正产生可测量的运行时间,但我可以将那个向量化的循环放入一个函数中,然后调用那个函数一百万次。感谢您的建议,我现在就去尝试一下。 - user2635263
1
@user2635263 运行时间可以被测量。只需使用通过RDTSC指令读取的循环计数器即可。在Google上搜索,您会找到基准测试框架。 - Nils Pipenbrinck
虽然说这是内存带宽限制,但并不意味着不能有显著的改进。如果 OP 使用多线程和非临时存储,很可能会有两倍的提升。 - Z boson

2
有几个因素决定了向量化代码的盈利能力。在这种情况下(基于您提供的输出),编译器仅向量化了一个循环,我认为这是第二个循环,因为第一个循环通常会被忽略,因为没有足够的计算量使其向量化变得有利可图。
您发布的运行时间是整个代码的时间,而不是仅仅循环的时间,因此向量化对于总体运行时间只能做出有限的贡献。如果您真的想看到向量化带来多少改进,我建议运行AMD Code XL、Intel Vtune、OProfile等分析工具,它们将告诉您针对该循环在时间和性能方面带来多少改进。
现在我正在评估向量化编译器,我发现使用向量化可以使代码运行速度高达60倍,但有时加速效果并不那么显著,这完全取决于循环、编译器和您使用的架构。

明白了。感谢您的回答。自从发布这个问题以来,我学到了很多关于代码向量化的知识! - user2635263

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接