我正在尝试通过使用适用于 AVX2 的英特尔内部函数将浮点值转换为整数值。我的简单代码如下:
void convert_f2i(float *fin, int *iout, int iLen)
{
int i, index, iDiv8, iLeft;
int *iin1;
__m256 v0;
__m256i vi0;
iDiv8 = iLen/8;
for(i=0; i<iDiv8; i++)
{
v0 = _mm256_load_ps(fin+i*8);
vi0 = _mm256_cvttps_epi32(v0);
_mm256_store_si256((__m256i *)(iout+i*8), vi0);
}
iLeft = iLen%8;
/* iLeft = iLen&7;*/
if (iLeft)
{
v0 = _mm256_load_ps(fin+i*8);
vi0 = _mm256_cvttps_epi32(v0);
iin1 = (int *)&vi0;
for(i=0; i<iLeft; i++)
{
index = iLen-iLeft+i;
printf("iLeft:%d i:%d %d %d index:%dn", iLeft, i, iin1[i], ((int *)&vi0)[i], index);
iout[index] = iin1[i];
}
}
}
我正在运行 iLen = 28671 的代码。前 28664 个结果是正确的。但最后 7 个结果是有问题的。 如果我在打开"iLeft = iLen%8"行的情况下编译代码,我会得到以下结果:
/*compiled with iLeft = iLen%8 */
iLeft:7 i:0 9 9 index:28664
iLeft:7 i:1 4 4 index:28665
iLeft:7 i:2 9 9 index:28666
iLeft:7 i:3 6 6 index:28667
iLeft:7 i:4 4 4 index:28668
iLeft:7 i:5 2 2 index:28669
iLeft:7 i:6 1 1 index:28670
这是正确的。
另一方面,如果我在打开"iLeft = iLen&7"行的情况下编译代码,我会得到以下结果:/*compiled with iLeft = iLen&7 */
iLeft:7 i:0 3 3 index:28664
iLeft:7 i:1 6 6 index:28665
iLeft:7 i:2 3 3 index:28666
iLeft:7 i:3 8 8 index:28667
iLeft:7 i:4 0 0 index:28668
iLeft:7 i:5 3 3 index:28669
iLeft:7 i:6 5 5 index:28670
这是不正确的。
9-4-9-6-4-2-1 是预期结果,3-6-3-8-0-3-5 是 28656-28662 之间的索引的结果。我不明白当我以不同的方式计算 iLeft 时会发生什么变化。在这两种方式中,iLeft=7,但结果并不相同。
有人可以告诉我可能有什么问题吗?
因为iLeft
在这两种情况下具有相同的值,所以 OP 看到的差异的真正原因一定是在其他地方。
与其开始盲目寻找实际原因,我个人为了简单起见,只会重写函数:
#include <stdlib.h>
#include <immintrin.h>
#include <string.h>
void truncate_floats_to_ints(int *dst, const float *src, size_t count)
{
const size_t nvecs = count / 8;
const size_t nfloats = count & 7;
const float *end = src + 8 * nvecs;
while (src < end) {
const __m256 fvec = _mm256_load_ps(src);
const __m256i ivec = _mm256_cvttps_epi32(fvec);
_mm256_store_si256((__m256i *)dst, ivec);
src += 8;
dst += 8;
}
if (nfloats) {
__v8sf fvec;
__v8si ivec;
memcpy(&fvec, src, nfloats * 4);
ivec = (__v8si)_mm256_cvttps_epi32(fvec);
memcpy(dst, &ivec, nfloats * 4);
}
}
请注意,此版本不会访问数组,即使在count
不是 8 的倍数的情况下也是如此。在这种情况下,fvec
中未使用的条目会获取垃圾值(从堆栈),但也会忽略ivec
中相应的截断值。如果您不喜欢这样做,可以将fvec
初始化为零。
另请注意,src
和dst
都必须对齐为 32 个字节。标准 Cmalloc()
不能保证这种对齐,尽管某些实现(也许是 Windows?)可能会。GNU C 库malloc()
没有,您应该使用 C11aligned_alloc(32, size)
其中size
是 32 的倍数来为此类矢量数组分配内存。
如果您不喜欢memcpy()
,则可以将它们替换为您自己的自定义函数,例如
#include <stdlib.h>
#include <inttypes.h>
#include <immintrin.h>
static inline void vector_copy_part(void *dst, const void *src, const size_t count)
{
switch (count & 7) {
case 7: ((uint32_t *)dst)[6] = ((uint32_t *)src)[6];
case 6: ((uint64_t *)dst)[2] = ((uint64_t *)src)[2];
((uint64_t *)dst)[1] = ((uint64_t *)src)[1];
((uint64_t *)dst)[0] = ((uint64_t *)src)[0];
break;
case 5: ((uint32_t *)dst)[4] = ((uint32_t *)src)[4];
case 4: ((uint64_t *)dst)[1] = ((uint64_t *)src)[1];
((uint64_t *)dst)[0] = ((uint64_t *)src)[0];
break;
case 3: ((uint32_t *)dst)[2] = ((uint32_t *)src)[2];
case 2: ((uint64_t *)dst)[0] = ((uint64_t *)src)[0];
break;
case 1: ((uint32_t *)dst)[0] = ((uint32_t *)src)[0];
}
}
void truncate_floats_to_ints(int *dst, const float *src, size_t count)
{
const size_t nvecs = count / 8;
const size_t nfloats = count & 7;
const float *end = src + 8 * nvecs;
while (src < end) {
const __m256 fvec = _mm256_load_ps(src);
const __m256i ivec = _mm256_cvttps_epi32(fvec);
_mm256_store_si256((__m256i *)dst, ivec);
src += 8;
dst += 8;
}
if (nfloats) {
__v8sf fvec;
__v8si ivec;
vector_copy_part(&fvec, src, nfloats);
ivec = (__v8si)_mm256_cvttps_epi32(fvec);
vector_copy_part(dst, &ivec, nfloats);
}
}
此代码使用 64 位整数寄存器(支持 AVX 的体系结构上的本机寄存器大小)和可能用于奇数元素的一个 32 位整数寄存器复制数据。(OP 的代码使用循环来复制 32 位整数,这很好 - 编译器更难优化,但由于它最多是 7 个整数副本,因此无论如何都不会花费任何大量时间。
一般来说,应该避免在循环中复制float
(或double
)值,因为这总是涉及浮点单元(这些架构上的AVX寄存器),而GCC至少不太擅长矢量化。在支持 AVX 的体系结构上使用普通通用寄存器(相同大小的整数)复制浮点数据会产生完全相同的寄存器,而根本不涉及 AVX 寄存器。
GCC-5.4将上述代码编译为一个简单的跳转表,其中包含八个条目,并使用rax
或eax
寄存器进行简单的移动;总体上完全可以接受。truncate_floats_to_ints()
中的循环只有六个指令(vcvttps2dq
、addq
、vmovdqa
、addq
、cmpq
、ja
)。
上述实现都通过了我的快速测试,但总是有可能潜伏着一个错误之类的。我不这么认为,但如果你找到了一个,请在评论中告诉我,这样我就可以修复它。