AVX2 Winner-Take-All Disparity Search

时间:2015-05-21 03:34:35

标签: c++ sse avx disparity-mapping avx2

我正在优化"赢家通吃"使用AVX2的视差估计算法的一部分。我的标量例程是准确的,但在QVGA分辨率和48个差异时,我的笔记本电脑上的运行时间在~14 ms时令人失望。我创建了LR和RL视差图像,但为了简单起见,我将只包含RL搜索的代码。

我的标量例程:

int MAXCOST = 32000;
for (int i = maskRadius; i < rstep-maskRadius; i++) {

    // WTA "RL" Search:
    for (int j = maskRadius; j+maskRadius < cstep; j++) {
        int minCost = MAXCOST;
        int minDisp = 0;
        for (int d = 0; d < numDisp && j+d < cstep; d++) {
            if (asPtr[(i*numDisp*cstep)+(d*cstep)+j] < minCost) {
                minCost = asPtr[(i*numDisp*cstep)+(d*cstep)+j];
                minDisp = d;
            }
        }
        dRPtr[(i*cstep)+j] = minDisp;
    }
}

我尝试使用AVX2:

int MAXCOST = 32000;
int* dispVals = (int*) _mm_malloc( sizeof(int32_t)*16, 32 );

for (int i = maskRadius; i < rstep-maskRadius; i++) {

    // WTA "RL" Search AVX2:
    for( int j = 0; j < cstep-16; j+=16) {

        __m256i minCosts = _mm256_set1_epi16( MAXCOST );
        __m128i loMask   = _mm_setzero_si128();
        __m128i hiMask   = _mm_setzero_si128();

        for (int d = 0; d < numDisp && j+d < cstep; d++) {
            // Grab 16 costs to compare
            __m256i costs = _mm256_loadu_si256((__m256i*) (asPtr[(i*numDisp*cstep)+(d*cstep)+j]));

            // Get the new minimums
            __m256i newMinCosts = _mm256_min_epu16( minCosts, costs );

            // Compare new mins to old to build mask to store minDisps
            __m256i mask   = _mm256_cmpgt_epi16( minCosts, newMinCosts );
            __m128i loMask = _mm256_extracti128_si256( mask, 0 );
            __m128i hiMask = _mm256_extracti128_si256( mask, 1 );
            // Sign extend to 32bits
            __m256i loMask32 = _mm256_cvtepi16_epi32( loMask );
            __m256i hiMask32 = _mm256_cvtepi16_epi32( hiMask );

            __m256i currentDisp = _mm256_set1_epi32( d );
            // store min disps with mask
            _mm256_maskstore_epi32( dispVals, loMask32, currentDisp );    // RT error, why?
            _mm256_maskstore_epi32( dispVals+8, hiMask32, currentDisp );  // RT error, why?

            // Set minCosts to newMinCosts
            minCosts = newMinCosts;
        }

        // Write the WTA minimums one-by-one to the RL disparity image
        int index = (i*cstep)+j;
        for( int k = 0; k < 16; k++ ) {
            dRPtr[index+k] = dispVals[k];
        }
    }
}
_mm_free( dispVals );

Disparity Space Image(DSI)的大小为HxWxD(320x240x48),我将其水平放置以便更好地进行内存访问,这样每行的大小为WxD。

Disparity Space Image具有每像素匹配成本。这个汇总了 使用简单的盒式过滤器制作完全相同尺寸的另一个图像, 但是,成本总计超过3x3或5x5的窗口。这种平滑使 结果更加强大&#39;当我使用asPtr访问时,我正在编制索引 到这个聚合成本图像。

此外,为了节省不必要的计算,我一直在开始 并以掩码半径偏移的行结束。该掩模半径是半径 我的人口普查面具。我可以做一些奇特的边框反射,但确实如此 更简单,更快,只是为了不打扰这个边界的差异。 这当然也适用于开始和结束cols,但是搞乱了 当我强制我的整个算法只运行时,这里的索引并不好 在列为16的倍数的图像上(例如QVGA:320x240),以便我 可以简单地索引并用SIMD命中所有内容(没有残余标量处理)。

另外,如果您认为我的代码很乱,我建议您查看 高度优化的OpenCV立体算法。我发现它们是不可能的,并且几乎没有使用它们。

我的代码编译但在运行时失败。我正在使用VS 2012 Express Update 4.当我使用调试器运行时,我无法获得任何见解。我对使用内在函数相对较新,所以我不确定调试时应该看到什么信息,寄存器数量,__m256i变量是否应该可见等等。

在下面注意评论建议,我通过使用更智能的索引将标量时间从~14提高到~8。我的CPU是i7-4980HQ,我在同一个文件的其他地方成功使用了AVX2内在函数。

Info Image

2 个答案:

答案 0 :(得分:2)

我仍然没有发现问题,但我确实看到了一些你可能想要改变的事情。但是,您并未检查_mm_malloc的返回值。如果它失败了,那就解释了它。 (也许它不喜欢分配32字节对齐的内存?)

如果您在内存检查程序或某些内容下运行代码,那么它可能不喜欢从dispVals的未初始化内存中读取内容。 (_mm256_maskstore_epi32即使掩码是全1,也可以算作读 - 修改 - 写。)

在调试器下运行代码,找出问题所在。 &#34;运行时错误&#34;没有多大意义。

_mm_set1*函数很慢。 VPBROADCASTD需要其内存或向量寄存器,而不是GP寄存器,因此编译器可以movd从GP寄存器到矢量寄存器然后广播,或者存储到存储器然后广播。无论如何,做起来会更快

const __m256i add1 = _mm256_set1_epi32( 1 );
__m256i dvec = _mm256_setzero_si256();
for (d;d...;d++) {
    dvec = _mm256_add_epi32(dvec, add1);
}

其他东西:
 如果您不在内循环的每次迭代中存储到内存中,这可能会运行得更快。使用混合指令(_mm256_blendv_epi8)或类似的东西来更新与最小成本一致的位移矢量。 Blend =使用注册目的地进行蒙版移动。

此外,您的位移值应该适合16b整数,所以不要签名 - 将它们扩展到32b,直到您找到它们为止。英特尔CPU可以在运行中将16b内存位置签名扩展到gp寄存器,而不会降低速度(movszmov一样快),所以问题。只需将dRPtr数组声明为uint16_t即可。那么你根本不需要你的矢量代码中的符号扩展内容(更不用说你的内循环了!)。希望_mm256_extracti128_si256( mask, 0 )编译为空,因为你想要的128已经是低128,所以只需使用reg作为vmovsx的src,但仍然。

您还可以通过先加载来保存指令(和融合域uop)。 (除非编译器足够聪明,不要忽略vmovdqu并使用vpminuw和内存操作数,即使你使用了内在的加载)。

所以我在想这样的事情:

// totally untested, didn't even check that this compiles.
for(i) { for(j) {
// inner loop, compiler can hoist these constants.
const __m256i add1 = _mm256_set1_epi16( 1 );
__m256i dvec = _mm256_setzero_si256();
__m256i minCosts = _mm256_set1_epi16( MAXCOST );
__m256i minDisps = _mm256_setzero_si256();

for (int d=0 ; d < numDisp && j+d < cstep ;
     d++, dvec = _mm256_add_epi16(dvec, add1))
{
    __m256i newMinCosts = _mm256_min_epu16( minCosts, asPtr[(i*numDisp*cstep)+(d*cstep)+j]) );
    __m256i mask   = _mm256_cmpgt_epi16( minCosts, newMinCosts );
    minDisps = _mm256_blendv_epi8(minDisps, dvec, mask); // 2 uops, latency=2
    minCosts = newMinCosts;
}

// put sign extension here if making dRPtr uint16_t isn't an option.
int index = (i*cstep)+j;
_mm256_storeu_si256 (dRPtr + index, __m256i minDisps);
}}

使用两个并行依赖关系链可能会获得更好的性能:minCosts0 / minDisps0minCosts1 / minDisps1,然后在最后组合它们。 minDisps是循环携带的依赖项,但循环只有5条指令(包括vpadd,它看起来像循环开销,但不能通过展开来减少)。它们解码为6 uops(blendv为2),加上循环开销。它应该在haswell上以1.5周/迭代(不计算循环开销)运行,但是dep链将它限制为每2个循环一次迭代。 (假设展开以摆脱循环开销)。并行执行两个dep链会修复此问题,并且与展开循环具有相同的效果:减少循环开销。

嗯,实际上是在Haswell,

  • pminuw可以在p1 / p5上运行。 (以及p2 / p3上的载荷部分)
  • pcmpgtw可以在p1 / p5上运行
  • 对于p5,
  • vpblendvb是2 uops。
  • padduw可以在p1 / p5上运行
  • movdqa reg,reg可以在p0 / p1 / p5上运行(可能根本不需要执行单元)。展开应该消除minCosts = newMinCosts的任何开销,因为编译器最终可以从右侧寄存器中的最后一个展开的循环体中获得newMinCosts,用于下一次迭代的第一个循环体。
  • fused sub / jge(循环计数器)可以在p6上运行。 (在dvec上使用PTEST + jcc会更慢)。未与add融合时,sub / jcc可以在p0 / p1 / p5 / p6上运行。

好的,实际上循环每次迭代需要2.5个周期,受到只能在p1 / p5上运行的指令的限制。展开2或4将减少循环/ movdqa开销。由于Haswell每个时钟可以发出4个uop,因此它可以更有效地排队uops以进行无序执行,因为循环不会有超高的迭代次数。 (48是你的例子。)排队的很多uops会在离开循环后给CPU做一些事情,并隐藏缓存未命中的任何延迟等。

_mm256_min_epu16PMINUW)是另一个循环携带的依赖链。将它与内存操作数一起使用会使其成为3或4个周期的延迟。但是,指令的加载部分可以在地址已知时立即开始,因此将负载折叠到修改操作中以利用微融合并不会使得分支链比使用单独的负载更长或更短

有时您需要使用单独的加载,用于未对齐的数据(AVX删除了内存操作数的对齐要求)。我们的执行单元比4 uop / clock发布限制更多,因此使用专用加载指令可能很好。

source for insn ports / latencies

答案 1 :(得分:2)

在您进行平台特定优化之前,可以执行大量可移植优化。提取循环不变量,将索引乘法转换为增量加法等等......

这可能不完全正确,但可以得到一般性的想法:

int MAXCOST = 32000, numDispXcstep = numDisp*cstep;
for (int i = maskRadius; i < rstep - maskRadius; i+=numDispXcstep) {
    for (int j = maskRadius; j < cstep - maskRadius; j++) {
        int minCost = MAXCOST, minDisp = 0;
        for (int d = 0; d < numDispXcstep - j; d+=cstep) {
            if (asPtr[i+j+d] < minCost) {
                minCost = asPtr[i+j+d];
                minDisp = d;
            }
        }
        dRPtr[i/numDisp+j] = minDisp;
    }
}

一旦你完成了这一点,很明显实际发生了什么。看起来“i”是最大的一步,其次是“d”,“j”实际上是对顺序数据进行操作的变量。 ...下一步是相应地重新排序循环,如果仍需要进一步优化,请应用特定于平台的内在函数。