我有两个数组:char* c
和float* 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;
}
由于
答案 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。