【问题标题】:Separable gaussian blur - optimize vertical pass可分离高斯模糊 - 优化垂直通道
【发布时间】:2020-10-18 14:36:18
【问题描述】:

我已经实现了可分离的高斯模糊。使用 SIMD 处理来优化水平通道相对容易。但是,我不确定如何优化垂直传递。

访问元素对缓存不是很友好,填充 SIMD 通道意味着读取许多不同的像素。我正在考虑转置图像并运行水平传递然后将图像转回,但是,由于两次转置操作,我不确定它是否会获得任何改进。

我有相当大的图像,分辨率为 16k,内核大小为 19,因此垂直通过增益的矢量化约为 15%。

我的 Vertical pass 如下(它是类型为 T 的泛型类,可以是 uint8_t 或 float):

int yStart = kernelHalfSize;
int xStart = kernelHalfSize;
int yEnd = input.GetWidth() - kernelHalfSize;
int xEnd = input.GetHeigh() - kernelHalfSize;

const T * inData = input.GetData().data();
V * outData = output.GetData().data();

int kn = kernelHalfSize * 2 + 1;
int kn4 = kn - kn % 4;

for (int y = yStart; y < yEnd; y++)
{
    size_t yW = size_t(y) * output.GetWidth();
    size_t outX = size_t(xStart) + yW;

    size_t xEndSimd = xStart;

    int len = xEnd - xStart;
    len = len - len % 4;
    xEndSimd = xStart + len;

    for (int x = xStart; x < xEndSimd; x += 4)
    {
        size_t inYW = size_t(y) * input.GetWidth();
        size_t x0 = ((x + 0) - kernelHalfSize) + inYW;
        size_t x1 = x0 + 1;
        size_t x2 = x0 + 2;
        size_t x3 = x0 + 3;

        __m128 sumDot = _mm_setzero_ps();

        int i = 0;
        for (; i < kn4; i += 4)
        {               
            __m128 kx = _mm_set_ps1(kernelDataX[i + 0]);
            __m128 ky = _mm_set_ps1(kernelDataX[i + 1]);
            __m128 kz = _mm_set_ps1(kernelDataX[i + 2]);
            __m128 kw = _mm_set_ps1(kernelDataX[i + 3]);

            
            __m128 dx, dy, dz, dw;

            if constexpr (std::is_same<T, uint8_t>::value)
            {
                //we need co convert uint8_t inputs to float
                __m128i u8_0 = _mm_loadu_si128((const __m128i*)(inData + x0));
                __m128i u8_1 = _mm_loadu_si128((const __m128i*)(inData + x1));
                __m128i u8_2 = _mm_loadu_si128((const __m128i*)(inData + x2));
                __m128i u8_3 = _mm_loadu_si128((const __m128i*)(inData + x3));

                __m128i u32_0 = _mm_unpacklo_epi16(
                    _mm_unpacklo_epi8(u8_0, _mm_setzero_si128()),
                    _mm_setzero_si128());
                __m128i u32_1 = _mm_unpacklo_epi16(
                    _mm_unpacklo_epi8(u8_1, _mm_setzero_si128()),
                    _mm_setzero_si128());
                __m128i u32_2 = _mm_unpacklo_epi16(
                    _mm_unpacklo_epi8(u8_2, _mm_setzero_si128()),
                    _mm_setzero_si128());
                __m128i u32_3 = _mm_unpacklo_epi16(
                    _mm_unpacklo_epi8(u8_3, _mm_setzero_si128()),
                    _mm_setzero_si128());

                dx = _mm_cvtepi32_ps(u32_0);
                dy = _mm_cvtepi32_ps(u32_1);
                dz = _mm_cvtepi32_ps(u32_2);
                dw = _mm_cvtepi32_ps(u32_3);

            }
            else
            {
                /*
                //load 8 consecutive values
                auto dd = _mm256_loadu_ps(inData + x0);

                //extract parts by shifting and casting to 4 values float
                dx = _mm256_castps256_ps128(dd);
                dy = _mm256_castps256_ps128(_mm256_permutevar8x32_ps(dd, _mm256_set_epi32(0, 0, 0, 0, 4, 3, 2, 1)));
                dz = _mm256_castps256_ps128(_mm256_permutevar8x32_ps(dd, _mm256_set_epi32(0, 0, 0, 0, 5, 4, 3, 2)));
                dw = _mm256_castps256_ps128(_mm256_permutevar8x32_ps(dd, _mm256_set_epi32(0, 0, 0, 0, 6, 5, 4, 3)));
                */

                dx = _mm_loadu_ps(inData + x0);
                dy = _mm_loadu_ps(inData + x1);
                dz = _mm_loadu_ps(inData + x2);
                dw = _mm_loadu_ps(inData + x3);
            }

            //calculate 4 dots at once
            //[dx, dy, dz, dw] <dot> [kx, ky, kz, kw]

            auto mx = _mm_mul_ps(dx, kx); //dx * kx
            auto my = _mm_fmadd_ps(dy, ky, mx); //mx + dy * ky
            auto mz = _mm_fmadd_ps(dz, kz, my); //my + dz * kz
            auto res = _mm_fmadd_ps(dw, kw, mz); //mz + dw * kw

            sumDot = _mm_add_ps(sumDot, res);

            x0 += 4;
            x1 += 4;
            x2 += 4;
            x3 += 4;
        }

        for (; i < kn; i++)
        {               
            auto v = _mm_set_ps1(kernelDataX[i]);
            auto v2 = _mm_set_ps(
                *(inData + x3), *(inData + x2), 
                *(inData + x1), *(inData + x0)
            );
            
            sumDot = _mm_add_ps(sumDot, _mm_mul_ps(v, v2));

            x0++;
            x1++;
            x2++;
            x3++;
        }

        sumDot = _mm_mul_ps(sumDot, _mm_set_ps1(weightX));

        if constexpr (std::is_same<V, uint8_t>::value)
        {
            __m128i asInt = _mm_cvtps_epi32(sumDot);
            
            asInt = _mm_packus_epi32(asInt, asInt);
            asInt = _mm_packus_epi16(asInt, asInt);

            uint32_t res = _mm_cvtsi128_si32(asInt);

            ((uint32_t *)(outData + outX))[0] = res;                
            outX += 4;
        }
        else 
        {
            float tmpRes[4];
            _mm_store_ps(tmpRes, sumDot);

            outData[outX + 0] = tmpRes[0];
            outData[outX + 1] = tmpRes[1];
            outData[outX + 2] = tmpRes[2];
            outData[outX + 3] = tmpRes[3];
            outX += 4;
        }
        
    }   

    for (int x = xEndSimd; x < xEnd; x++)
    {
        int kn = kernelHalfSize * 2 + 1;
        const T * v = input.GetPixelStart(x - kernelHalfSize, y);
        float tmp = 0;
        for (int i = 0; i < kn; i++)
        {
            tmp += kernelDataX[i] * v[i];
        }
        tmp *= weightX;
        outData[outX] = ImageUtils::clamp_cast<V>(tmp);
        outX++;
    }
}

【问题讨论】:

  • 向我们展示您目前的成果!知道你有什么输入/输出类型,你想要支持什么目标架构,你是否需要完全相同的输出或一个近似值就足够了等等,这也是非常重要的。15% 的增益是总的,或者只是在垂直传球?例如,您是否检查过 OpenCV 是否已经对其高斯模糊实现进行了一些 SIMD 优化?
  • 您可以在垂直通道上使用许多优化 - 特别是两个:(a) 每行处理超过向量以获得更好的缓存占用空间和 (b) 利用所有重叠的连续行的冗余负载,例如同时对 22 行进行操作以生成 4 个输出行(将负载减少约 4 倍)。
  • @chtz 我已经为我的垂直传球添加了代码。
  • //we need co convert uint8_t inputs to float:您可以尝试使用基于PMADDUBSW 的解决方案(即,使用 8+8 位定点数进行所有操作)。与基于 float 的解决方案相比,您会有所偏差。您是否仅限于 SSE2?
  • @chtz 如果需要我可以使用 avx2

标签: image-processing x86 sse simd gaussianblur


【解决方案1】:

有一个众所周知的技巧。

当您计算两个通道时,按顺序读取它们,使用 SIMD 进行计算,但将结果写入另一个缓冲区,转置,使用标量存储。 Protip:SSE 4.1 有_mm_extract_ps,只是不要忘记将目标图像指针从float* 转换为int*。关于这些商店的另一件事,我建议您使用_mm_stream_si32,因为您希望输入数据使用最大的缓存空间。当您计算第二遍时,您将再次读取顺序内存地址,预取器硬件将处理延迟。

这样两次传递都是相同的,我通常调用相同的函数两次,使用不同的缓冲区。

由您的 2 次传递引起的两个转置相互抵消。这是HLSL version,顺便说一句。

还有更多。如果您的内核大小只有 19,则适合 3 个 AVX 寄存器。我认为 shuffle/permute/blend 指令仍然比 L1 缓存加载更快,即在循环外加载内核可能更好。

【讨论】:

  • 小心_mm_extract_ps - 它可以编译到内存目标存储,但请注意,内在返回值的类型为int,因此您需要键入双关它返回到float 以实际存储一个 FP 位模式。否则将int 分配给float 会将FP 位模式作为int,将其转换为float,然后存储。喜欢extractps eax, xmm0, 1 / cvtsi2ss eax, xmm1 / movss [mem], xmm0 /facepalm
  • 不确定它是否与 OP 相关,无论如何他们都需要它在内存中,转换指针很容易。对于那个用例,我什至会推荐 _mm_stream_si32 用于该商店,你只会在稍后阅读,但你想要源图像的所有可用缓存空间,尤其是卷积内核。
  • 我只是说_mm_extract_ps很难用;只是顺便提及它而不提及陷阱可能不是很有帮助。此外,_mm_stream_si32 会导致提取到一个整数 reg,然后是 movnti 。如果你想要 NT 商店,你会想要做 FP shuffle 来组合 movntps 的 4 个元素。或者,如果您不做全线商店,那么您实际上根本不想要 NT;让 L1d 吸收你零散的写作。
  • @PeterCordes 提到了。写转置输出的时候做全行存储是非常复杂的,你必须在内循环中同时做4-8行。可行,但是内核输出/广播的这些洗牌的开销会消耗掉大部分获得的性能。根据我的经验,只要写入的内存不会过早读回,硬件就非常擅长随机写入。
  • 那你不需要 NT 商店;只有大约十几个缓存行大小的缓冲区(并且常规加载/存储也竞争它们,以从 L2 缓存传输数据)。如果需要一个新的,无论它们是否已满,它们都必须一直驱逐到 RAM。全行存储可以避免 RFO(并且只是无效),但部分行 NT 存储效率较低;我不知道多核 CPU 如何处理它的细节,但这是你想要避免的。
猜你喜欢
  • 1970-01-01
  • 2014-01-07
  • 1970-01-01
  • 2017-08-27
  • 1970-01-01
  • 2018-05-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多