自动向量化:说服编译器不需要进行别名检查

20

我正在进行一些图像处理,借助向量化技术获得了好处。我有一个可以向量化的函数,但是我无法说服编译器输入和输出缓冲区没有重叠,因此不需要进行别名检查。我应该能够使用__restrict__来实现这一点,但如果在作为函数参数到达时未将缓冲区定义为__restrict__,就无法让编译器确信我绝对确定两个缓冲区永远不会重叠。

这是这个函数:

__attribute__((optimize("tree-vectorize","tree-vectorizer-verbose=6")))
void threshold(const cv::Mat& inputRoi, cv::Mat& outputRoi, const unsigned char th) {

    const int height = inputRoi.rows;
    const int width = inputRoi.cols;

    for (int j = 0; j < height; j++) {
        const uint8_t* __restrict in = (const uint8_t* __restrict) inputRoi.ptr(j);
        uint8_t* __restrict out = (uint8_t* __restrict) outputRoi.ptr(j);
        for (int i = 0; i < width; i++) {
           out[i] = (in[i] < valueTh) ? 255 : 0;
        }
    }
}

唯一的方法是将内部循环放在一个单独的函数中,并在其中将指针定义为__restrict__参数,以使编译器不执行别名检查。如果我将这个内部函数声明为内联的,那么别名检查又会被激活。

您还可以通过这个例子看到效果,我认为它是一致的:http://goo.gl/7HK5p7

(注意:我知道可能有更好的编写相同函数的方法,但在这种情况下,我只是试图了解如何避免别名检查)

编辑:
问题已经解决!(请参见下面的答案
使用gcc 4.9.2,这里是完整的示例。请注意,在超出使用-ftree-vectorizer-verbose=N之后,使用编译器标志-fopt-info-vec-optimized
因此,对于gcc,请使用#pragma GCC ivdep并享受! :)


1
请注意,gcc-5版本可能会修复内联问题:https://gcc.gnu.org/ml/gcc-patches/2014-09/msg00606.html - Marc Glisse
感谢您展示C++ Web编译器。 - StarShine
我没有准备好测试的openCV副本,但也许你可以通过利用“__assume(in!= out)”语句来说服编译器,使得“inputRoi”和“outputRoi”引用不同的缓冲区?在使用“__assume”时有很多事情可以做,但这取决于编译器是否足够聪明以理解它。 - Stefan
2
@Stefan 假设 in != out 对编译器来说绝对不足够的信息:缓冲区可能会_部分_重叠。 - Antonio
3个回答

5

如果您使用的是英特尔编译器,可以尝试添加以下行:

#pragma ivdep 

以下段落摘自Intel编译器用户手册:
ivdep pragma指示编译器忽略假定的向量依赖项。为了确保正确的代码,编译器将假定的依赖性视为已证明的依赖性,这会防止向量化。此pragma覆盖了该决定。只有在您知道可以安全地忽略假定的循环依赖关系时才使用此pragma。
在gcc中,应添加以下行:
#pragma GCC ivdep

在函数内部,在你想要向量化的循环之前(请参见文档),需要插入代码。这仅在gcc 4.9及以上版本中受支持,并且顺便说一下,这使得使用__restrict__变得多余。

这似乎是一个非常好的提示!我无法在http://gcc.godbolt.org上快速测试,因为缺少gcc 4.9编译器,而且我了解到这个功能在以前的编译器版本中不存在... - Antonio
请告诉我它是否有效。对于简单循环,这个技巧大约有三分之二的时间是有效的。如果您仍然难以自动向量化您的代码,在函数内将输入指针分配给虚拟指针可能会欺骗编译器。 - PhD AP EcE
1
请将#pragma放在嵌套循环内。#pragma ivdep大多数情况下(intel编译器)不能在嵌套循环外部工作。 - PhD AP EcE
我在使用gcc 4.9.2的MinGw上进行了测试,它可以正常工作!注意:在我的情况下,我将#pragma指令放在循环之前;而__restrict__变得多余了,我会相应地更新答案。 - Antonio

2

针对这个特定问题的另一种方法是使用OpenMP simd指令,该指令已成为自版本4.0以来的标准,并且在(相当现代的)编译器中完全可移植。代码如下:

void threshold(const unsigned char* inputRoi, const unsigned char valueTh,
               unsigned char* outputRoi, const int width,
               const int stride, const int height) {
    #pragma omp simd
    for (int i = 0; i < width; i++) {
        outputRoi[i] = (inputRoi[i] < valueTh) ? 255 : 0;
    }
}

如果启用了OpenMP支持(使用完全支持或仅支持simd的部分支持,例如使用英特尔编译器的-qopenmp-simd),则代码将完全向量化。

此外,这使您有机会指示向量的可能对齐方式,在某些情况下非常方便。例如,如果您的输入和输出数组是使用对齐感知内存分配器(例如posix_memalign(),对齐要求为256b)分配的,则代码可以变成:

void threshold(const unsigned char* inputRoi, const unsigned char valueTh,
               unsigned char* outputRoi, const int width,
               const int stride, const int height) {
    #pragma omp simd aligned(inputRoi, outputRoi : 32)
    for (int i = 0; i < width; i++) {
        outputRoi[i] = (inputRoi[i] < valueTh) ? 255 : 0;
    }
}

这将允许生成更快的二进制文件。而且,使用 ivdep 指令并不容易实现此功能。更多理由使用 OpenMP simd 指令。


1
至少在版本14中,英特尔编译器未针对您提供的代码中的threshold2生成别名检查,这意味着您的方法应该有效。但是,gcc自动向量化程序错过了这个优化机会,但生成矢量化代码,测试适当的对齐方式,测试别名和非矢量化回退/清理代码。

问题已经足够明确,像影响可能是什么或者内存对齐之类的事情并不相关。我认为你回答中相关的部分是英特尔编译器的结果,这可以在一个简单的注释中说明。 - Antonio
@Antonio:为什么gcc是否生成别名检查对您很重要? - user1225999
因为在我的使用情况下(图像处理),这个检查会发生多次(在每个图像行中,对于可能具有少量列的图像或子图像)。 - Antonio
@Antonio:好的,没问题。 - user1225999

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