是否可以使用SIMD指令进行替换?

时间:2018-01-14 22:51:53

标签: c++ algorithm replace simd

我有 int 的向量,我需要找到替换一些具有特定值的元素。它们都是相同的 例如:对所有元素替换4到8。

我正在c ++中尝试循环内存访问。但它对我来说仍然很慢。

更新
我正在使用Mat上的OpenCV x86对象:

for (int i = 0; i < labels.rows; ++i) {
    for (int j = 0; j < labels.cols; ++j) {
        int& label = labels.at<int>(i, j);
        if (label == oldValue) {
            label = newValue;
        }
    }
}

Mat.at()函数只是在发布模式下通过指针返回值

template<typename _Tp> inline
_Tp& Mat::at(int i0, int i1)
{
    CV_DbgAssert(dims <= 2);
    CV_DbgAssert(data);
    CV_DbgAssert((unsigned)i0 < (unsigned)size.p[0]);
    CV_DbgAssert((unsigned)(i1 * DataType<_Tp>::channels) < (unsigned)(size.p[1] * channels()));
    CV_DbgAssert(CV_ELEM_SIZE1(traits::Depth<_Tp>::value) == elemSize1());
    return ((_Tp*)(data + step.p[0] * i0))[i1];
}

2 个答案:

答案 0 :(得分:5)

您没有提及您正在开发的架构,因此无法告诉您使用哪些内在函数。幸运的是,您的编译器应该能够自动向量化像

这样的东西
for (int i = 0 ; i < N ; i++)
  foo[i] = (foo[i] == 4) ? 8 : foo[i];

假设您的数据已充分对齐,-mavx2 -O3 GCC将使用vpcmpeqd和vpblendvb。

答案 1 :(得分:2)

让编译器自动向量化的关键是始终分配给元素,即使您将其分配给自身也是如此。 (三元运算符在这里很好,请参阅@nemequ的答案)。这使编译器可以读取/重写未更改的值,因此可以使用load + compare和blend + store进行矢量化。

编译器无法向C ++源代码不写入的内存位置发明写入,因为这可能会踩到另一个线程的写入。它不是用于读取/写入相邻数组元素的不同线程的数据竞争。如果编译器不知道的另一个函数也使用具有不同搜索/替换值的向量加载/混合/存储循环,则它们的存储将相互衔接。所以这个矢量化策略只有在源写入所有元素时才有效。编译器可以自由地优化它(例如,如果它没有矢量化)。

对另一个答案的评论指出了无条件存储的缺点:it dirties the cache even if the data doesn't change。如果搜索命中率很少,则可能值得分支跳过存储并节省内存带宽,特别是如果多个线程将在大块内存上运行。包括在同一台机器上运行的程序的多个实例,尤其是在共享内存情况下。

AVX引入了解决此问题的掩码存储指令AVX2 vpmaskmovd和AVX1 vmaskmovps都具有32位粒度,因此您可以直接将它们用于int数据。对于较窄的元素,您可以将+ blend与字节或单词粒度进行比较,然后使用dword粒度检查更改以生成掩码。

我认为vpmaskmovd(至少在Skylake中)的实现确实可以避免在掩码为全0时弄脏缓存行。根据{{​​3}},使用蒙面商店 - &gt;任何重新加载:如果掩码全为0,则加载不依赖于被屏蔽的存储。因此存储队列知道全零掩码使存储成为无操作。

我还没有测试过,但我希望在这种情况下避免弄脏缓存线,至少在Skylake上(包括不支持AVX512的Skylake客户端;但它确实具有AVX512所需的微架构特性,如高效的蒙面商店)。甚至允许屏蔽元素在没有故障的情况下触摸非法地址,并且一些CPU可以这样做(至少对于全零屏蔽情况)而不捕获微码辅助。这意味着他们有办法完全压制商店。

所以asm你想要编译器(通过内在函数或自动向量化)是:

 ;; outside the loop:  ymm4 = set1_epi32(4);  ymm5 = set1_epi32(8);


vpcmpeqd    ymm0, [rdi], ymm4     ; ymm0 = _mm256_cmpeq_epi32
vpmaskmovd  [rdi], ymm0, ymm5     ; store 8 in elements where ymm0 is -1
add         rdi, 32

我没有对此进行基准测试,看它是否实际上更快(或者至少在内存带宽不是瓶颈时相等,这将是一个更容易设计的微基准测试)。

Skylake上的vpmaskmovd商店只有3 uops(p0 +商店地址+商店数据)。哈斯韦尔是4 uops。

根据Intel's optimization manual: 11.9 CONDITIONAL SIMD PACKED LOADS AND STORESvmaskmovps - 商店在Skylake上是4 uops。很奇怪它与行为相同的整数指令不匹配。

使用条件掩码存储意味着您不需要原始数据,因此它允许将负载折叠到vpcmpeqd。 load + cmp + blend + store将需要1 + 2 + 1个指令,而vpblendvb是2个uop。 (所以vblendps)。因此理论上掩盖的商店更快。

Haswell上的

vpblendvb只能在端口5上运行,因此这会限制您每隔一个时钟处理32个字节,而不是每1.25个时钟处理一个向量(具有无限展开)。大多数情况下,每2个时钟32个字节是可以的,但如果您的数据在L1D缓存中很热,那么它就是瓶颈。

使用AVX512,您可能以相同的方式实现它,但使用AVX512BW,您可以使用相同的屏蔽存储策略,以比32位更小的粒度。比较k1vmovdqu8 [mem]{k1}, zmm8

没有AVX:请勿使用SSE maskmovdqu ;它很慢,而且是隐式NT,所以它会刷新缓存行,以及所有这些。使用load + blend + store。