【问题标题】:Using AVX to improve performance of float subtract, divide, truncate to int32使用 AVX 提高浮点减法、除法、截断到 int32 的性能
【发布时间】:2021-01-03 11:19:50
【问题描述】:

尝试使用 AVX 来提高以下性能

__declspec(dllexport) void __cdecl calculate_quantized_vertical_values(long length, float min, float step, float* source, unsigned long* destination)
{           
    for (long i = 0; i < length; i++)
    {
        destination[i] = (source[i] - min) / step;          
    }       
}

将其替换为

__declspec(dllexport) void __cdecl calculate_quantized_vertical_values_avx(long length, float min, float step, float* source, unsigned long* destination)
{       
    long multiple8end = ((long)(length / 8)) * 8;

    __m256 min256 = _mm256_broadcast_ss((const float*)&min);
    __m256 step256 = _mm256_broadcast_ss((const float*)&step);

    for (long i = 0; i < multiple8end; i+=8)
    {
        __m256 value256 = _mm256_load_ps((const float*)(source + i));
        __m256 offset256 = _mm256_sub_ps(value256, min256);
        __m256 floatres256 = _mm256_div_ps(offset256, step256);
        __m256i long256 = _mm256_cvttps_epi32(floatres256);
        _mm256_store_si256((__m256i*)(destination + i), long256);           
    }

    for (long i = multiple8end; i < length; i ++)
    {
        destination[i] = (source[i] - min) / step;
    }
}

原始循环大约需要 330 毫秒,我的 55M 元素源数组和循环的内容编译为

loc_180001050:
movss   xmm0, dword ptr [r10+rcx-4]
subss   xmm0, xmm3
divss   xmm0, xmm2
cvttss2si rax, xmm0
mov     [rcx-4], eax
movss   xmm1, dword ptr [r10+rcx]
subss   xmm1, xmm3
divss   xmm1, xmm2
cvttss2si rax, xmm1
mov     [rcx], eax
movss   xmm0, dword ptr [r10+rcx+4]
subss   xmm0, xmm3
divss   xmm0, xmm2
cvttss2si rax, xmm0
mov     [rcx+4], eax
movss   xmm1, dword ptr [r10+rcx+8]
subss   xmm1, xmm3
divss   xmm1, xmm2
cvttss2si rax, xmm1
mov     [rcx+8], eax
add     rcx, 10h
sub     r8, 1
jnz     short loc_180001050

AVX 循环在相同的 55M 元素源数组上花费大约 170ms,并且(主)循环的内容编译为:

loc_180001160:
vmovups ymm0, ymmword ptr [r8+rdx]
lea     rdx, [rdx+20h]
vsubps  ymm1, ymm0, ymm6
vdivps  ymm2, ymm1, ymm7
vcvttps2dq ymm3, ymm2
vmovdqu ymmword ptr [rdx-20h], ymm3
sub     rax, 1
jnz     short loc_180001160

所以 AVX 有性能改进,但我想知道是否有可能获得更显着的性能改进,或者这是关于此特定计算的限制

编辑:我还应该提到,如果有任何不同,我将从 .NET 应用程序调用这些 DLL 函数。

编辑: 理想情况下,我希望 unsigned char 数组用于 destination,但现在坚持使用 int32,因为我还没有找到实现 float 的方法 -> @987654329 @ AVX 转换

如果可以提高性能,那么乘以 1.f/step 而不是除以 step 对我来说应该没问题

【问题讨论】:

  • _mm256_cvttps_epi32 将转换为签名的int32,您的签名表明您希望unsigned long 作为输出,这是有意的吗? (无论如何,我都会在此处避免使用long——在 64 位宽的 linux 64 位系统上,以防您想移植它)。你真的需要使用除法,还是可以乘以1.f/step
  • @chtz,我已经更新了这个问题。明白了 long vs int32 的观点,我不太可能移植到 Linux,但我会更新

标签: c optimization avx


【解决方案1】:

如果您按1/step 缩放而不是除以step,您应该会明显更快,除非您受到内存吞吐量的限制。如果您考虑到 min 的减法,您还可以使用 FMA 指令(如果可用):

void calculate_quantized_vertical_values_avx(size_t length, float min, float step, float* source, uint32_t* destination)
{       
    size_t multiple8end = ((length / 8)) * 8;
    const float scale = 1.f/step;
    const float offset = -min * scale;
    const __m256 scale256 = _mm256_set1_ps(scale);
    const __m256 offset256 = _mm256_set1_ps(offset);

    for (size_t i = 0; i < multiple8end; i+=8)
    {
        __m256 value256 = _mm256_load_ps((const float*)(source + i));
#ifdef __FMA__
        __m256 floatres256 = _mm256_fmadd_ps(value256, scale256, offset256);
#else
        __m256 floatres256 = _mm256_add_ps(_mm256_mul_ps(value256, scale256), offset256); 
#endif
        __m256i long256 = _mm256_cvttps_epi32(floatres256);
        _mm256_store_si256((__m256i*)(destination + i), long256);           
    }

    for (size_t i = multiple8end; i < length; i ++)
    {
        destination[i] = (source[i] * scale) + offset;
    }
}

如果您想将结果转换为uint8,请查看_mm256_packus_epi32_mm256_packus_epi16(或者_mm_packus_epi32_mm_packus_epi16,如果您没有AVX2)。

【讨论】:

  • @axk:为了在将 4 个向量压缩成 4 个 8 字节的块后以正确的顺序获取数据,您需要vpermq。 AVX2 pack 指令真的很不方便“in-lane”随机播放。
  • 不幸的是,用乘法代替除法根本没有帮助,我想在我的情况下它是内存受限的,我尝试只复制数据并且它需要相同的时间。现在,800MHz DDR3 的峰值吞吐量为 6.4GB/s,在 120ms(多线程)内读取/写入 220Mb 的速度为 3.7GB/s。
  • 我想如果我“交错”源和目标可能会带来轻微的改进,因为它不必在相距很远的内存块之间如此频繁地来回走动?
  • 我怀疑交错源和目标会有所帮助。用目标覆盖源可能会有所帮助。此外,即时将4x8 int32 压缩为32 uint8 可能会有所帮助。理想情况下,您能够将生成源值的任何代码流水线到同一个循环中(从而完全避免存储该源)。或者只生成适合(一半)L1 缓存的块,转换它们然后生成下一个块。不过,您应该为此发布另一个问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-11-07
  • 2021-07-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多