SSE代码根据比较将float变量设置为0.0f或1.0f

时间:2013-10-30 14:56:55

标签: c performance optimization sse simd

我有两个数组:char* cfloat* f我需要执行此操作:

// Compute float mask
float* f;
char* c;
char c_thresh;
int n;

for ( int i = 0; i < n; ++i )
{
    if ( c[i] < c_thresh ) f[i] = 0.0f;
    else                   f[i] = 1.0f;
}

我正在寻找一种快速的方法:无条件并尽可能使用SSE(4.2或AVX)。

如果使用float代替char可以带来更快的代码,我可以将代码更改为仅使用浮点数:

// Compute float mask
float* f;
float* c;
float c_thresh;
int n;

for ( int i = 0; i < n; ++i )
{
    if ( c[i] < c_thresh ) f[i] = 0.0f;
    else                   f[i] = 1.0f;
}

由于

6 个答案:

答案 0 :(得分:5)

非常简单,只需进行比较,将字节转换为dword,将AND转换为1.0f :(未经过测试,无论如何这都不是复制和粘贴代码,它的目的是展示你是如何做到的)

movd xmm0, [c]          ; read 4 bytes from c
pcmpgtb xmm0, threshold ; compare (note: comparison is >, not >=, so adjust threshold)
pmovzxbd xmm0, xmm0     ; convert bytes to dwords
pand xmm0, one          ; AND all four elements with 1.0f
movdqa [f], xmm0        ; save result

转换为内在函数应该很容易。

答案 1 :(得分:5)

以下代码使用SSE2(我认为)。

它在一条指令(_mm_cmpgt_epi8)中执行16次字节比较。它假定char已签名;如果您的char未签名,则需要额外的摆弄(翻转每个char的最高位)。

它唯一不标准的事情是使用幻数3f80来表示浮点常量1.0。幻数实际上是0x3f800000,但是16 LSB为零的事实使得可以更有效地进行比特摆放(使用16位掩码而不是32位掩码)。

// load (assuming the pointer is aligned)
__m128i input = *(const __m128i*)c;
// compare
__m128i cmp = _mm_cmpgt_epi8(input, _mm_set1_epi8(c_thresh - 1));
// convert to 16-bit
__m128i c0 = _mm_unpacklo_epi8(cmp, cmp);
__m128i c1 = _mm_unpackhi_epi8(cmp, cmp);
// convert ffff to 3f80
c0 = _mm_and_si128(c0, _mm_set1_epi16(0x3f80));
c1 = _mm_and_si128(c1, _mm_set1_epi16(0x3f80));
// convert to 32-bit and write (assuming the pointer is aligned)
__m128i* result = (__m128i*)f;
result[0] = _mm_unpacklo_epi16(_mm_setzero_si128(), c0);
result[1] = _mm_unpackhi_epi16(_mm_setzero_si128(), c0);
result[2] = _mm_unpacklo_epi16(_mm_setzero_si128(), c1);
result[3] = _mm_unpackhi_epi16(_mm_setzero_si128(), c1);

答案 2 :(得分:3)

通过切换到浮点数,您可以在GCC中自动向量化循环,而不用担心内在函数。以下代码将执行您想要的操作并自动进行矢量化。

void foo(float *f, float*c, float c_thresh, const int n) {
    for (int i = 0; i < n; ++i) {
        f[i] = (float)(c[i] >= c_thresh);
    }
}

编译

g++  -O3 -Wall  -pedantic -march=native main.cpp -ftree-vectorizer-verbose=1 

您可以在coliru自己查看结果并编辑/编译代码。但是,MSVC2013没有对循环进行矢量化。

答案 3 :(得分:2)

AVX版本:

void floatSelect(float* f, const char* c, size_t n, char c_thresh) {
    for (size_t i = 0; i < n; ++i) {
        if (c[i] < c_thresh) f[i] = 0.0f;
        else f[i] = 1.0f;
    }
}

void vecFloatSelect(float* f, const char* c, size_t n, char c_thresh) {
    const auto thresh = _mm_set1_epi8(c_thresh);
    const auto zeros = _mm256_setzero_ps();
    const auto ones = _mm256_set1_ps(1.0f);
    const auto shuffle0 = _mm_set_epi8(3, -1, -1, -1, 2, -1, -1, -1, 1, -1, -1, -1, 0, -1, -1, -1);
    const auto shuffle1 = _mm_set_epi8(7, -1, -1, -1, 6, -1, -1, -1, 5, -1, -1, -1, 4, -1, -1, -1);
    const auto shuffle2 = _mm_set_epi8(11, -1, -1, -1, 10, -1, -1, -1, 9, -1, -1, -1, 8, -1, -1, -1);
    const auto shuffle3 = _mm_set_epi8(15, -1, -1, -1, 14, -1, -1, -1, 13, -1, -1, -1, 12, -1, -1, -1);

    const size_t nVec = (n / 16) * 16;
    for (size_t i = 0; i < nVec; i += 16) {
        const auto chars = _mm_loadu_si128(reinterpret_cast<const __m128i*>(c + i));
        const auto mask = _mm_cmplt_epi8(chars, thresh);
        const auto floatMask0 = _mm_shuffle_epi8(mask, shuffle0);
        const auto floatMask1 = _mm_shuffle_epi8(mask, shuffle1);
        const auto floatMask2 = _mm_shuffle_epi8(mask, shuffle2);
        const auto floatMask3 = _mm_shuffle_epi8(mask, shuffle3);
        const auto floatMask01 = _mm256_set_m128i(floatMask1, floatMask0);
        const auto floatMask23 = _mm256_set_m128i(floatMask3, floatMask2);
        const auto floats0 = _mm256_blendv_ps(ones, zeros, _mm256_castsi256_ps(floatMask01));
        const auto floats1 = _mm256_blendv_ps(ones, zeros, _mm256_castsi256_ps(floatMask23));
        _mm256_storeu_ps(f + i, floats0);
        _mm256_storeu_ps(f + i + 8, floats1);
    }
    floatSelect(f + nVec, c + nVec, n % 16, c_thresh);
}

答案 4 :(得分:1)

怎么样:

f[i] = (c[i] >= c_thresh);

至少这会删除条件。

答案 5 :(得分:1)

转换为

f[i] = (float)(c[i] >= c_thresh);

- 也可以使用英特尔编译器进行自动矢量化(其他人也提到gcc也是如此)

如果您需要自动矢量化某些分支循环, - 您还可以尝试 #pragma ivdep pragma simd (最后一个是Intel Cilk Plus和OpenMP 4.0标准的一部分)。这些编译指示自动向量化以便携方式为SSE,AVX和未来的向量扩展(如AVX512)提供代码。这些编译指示由英特尔编译器(所有已知版本),Cray和PGI编译器(仅限ivdep),可能即将推出的GCC4.9版本支持,并且从VS2012开始部分支持MSVC(仅限ivdep)。

对于给定的例子,我没有改变任何东西(保留if和char *),只是添加了pragma ivdep:

void foo(float *f, char*c, char c_thresh, const int n) {
    #pragma ivdep
    for ( int i = 0; i < n; ++i )
    {
        if ( c[i] < c_thresh ) f[i] = 0.0f;
        else                   f[i] = 1.0f;
    }
}

在我的Core i5上没有AVX支持(仅限SSE3),对于n = 32K(32000000),c [i]随机生成并使用c_thresh等于0(我们使用signed char),给定代码提供约~5x由于ICL矢量化导致的加速。

完整测试(附加测试用例正确性检查)可用here(它是coliru,即仅限gcc4.8,没有ICL / Cray;这就是为什么它没有在coliru env中进行矢量化)。

应该可以通过处理更多的预取,对齐和类型转换pragma / optimizations来进一步进行性能优化。在给定的简单情况下,也可以使用添加restrict关键字(或 restrict ,具体取决于所使用的编译器)而不是ivdep / simd,而对于更一般的情况 - 编译指示simd / ivdep是最强大的。

注意:实际上#pragma ivdep“指示编译器忽略假定的跨迭代依赖性”(粗略地说明如果并行化同一个循环导致数据竞争的那些)。由于众所周知的原因,编译器在这些假设中非常保守。在给定的情况下,显然没有写后读或写后读依赖。如果需要,可以通过动态工具(如Advisor XE正确性分析,至少在给定工作负载上验证此类依赖关系的存在,如下面的评论中所示的btw。