【发布时间】:2018-11-08 21:36:44
【问题描述】:
我有一个带符号的短数组,我想除以 2048 并得到一个浮点数组。
我发现 SSE: convert short integer to float 允许将 unsigned 短裤转换为浮点数,但我也想处理签名短裤。
下面的代码有效,但仅适用于正面短裤。
// We want to divide some signed short by 2048 and get a float.
const auto floatScale = _mm256_set1_ps(2048);
short* shortsInput = /* values from somewhere */;
float* floatsOutput = /* initialized */;
__m128i* m128iInput = (__m128i*)&shortsInput[0];
// Converts the short vectors to 2 float vectors. This works, but only for positive shorts.
__m128i m128iLow = _mm_unpacklo_epi16(m128iInput[0], _mm_setzero_si128());
__m128i m128iHigh = _mm_unpackhi_epi16(m128iInput[0], _mm_setzero_si128());
__m128 m128Low = _mm_cvtepi32_ps(m128iLow);
__m128 m128High = _mm_cvtepi32_ps(m128iHigh);
// Puts the 2 __m128 vectors into 1 __m256.
__m256 singleComplete = _mm256_castps128_ps256(m128Low);
singleComplete = _mm256_insertf128_ps(singleComplete, m128High, 1);
// Finally do the math
__m256 scaledVect = _mm256_div_ps(singleComplete, floatScale);
// and puts the result where needed.
_mm256_storeu_ps(floatsOutput[0], scaledVect);
如何将我的签名短裤转换为花车?或者也许有更好的方法来解决这个问题?
编辑: 与非 SIMD 算法相比,我尝试了不同的答案,在 ~3.2GHz 的 AMD Ryzen 7 2700 上,在 2048 阵列上执行了 10M 次。我正在使用 Visual 15.7.3,主要是默认配置:
/permissive- /Yu"stdafx.h" /GS /GL /W3 /Gy /Zc:wchar_t /Zi /Gm- /O2 /sdl
/Fd"x64\Release\vc141.pdb" /Zc:inline /fp:precise /D "NDEBUG" /D "_CONSOLE"
/D "_UNICODE" /D "UNICODE" /errorReport:prompt /WX- /Zc:forScope
/arch:AVX2 /Gd /Oi /MD /openmp /FC /Fa"x64\Release\" /EHsc /nologo
/Fo"x64\Release\" /Fp"x64\Release\test.pch" /diagnostics:classic
请注意,我对 SIMD 非常陌生,并且很长时间没有使用 C++。这是我得到的(我分别重新运行每个测试,而不是一个接一个地重新运行,并获得了更好的结果):
- 无 SIMD:7300 毫秒
- wim 的回答:2300 毫秒
- chtz 的 SSE2 答案:1650ms
- chtz 的 AVX2 答案:2100ms
因此,我通过使用 SIMD 获得了很好的加速,而 chtz 的 SSE2 答案虽然更冗长且难以理解,但速度更快。 (至少在启用 AVX 的情况下编译时,它避免了使用 3 操作数 VEX 编码指令来复制寄存器的额外指令。在 Intel CPU 上,AVX2 版本应该比 128 位版本快得多。)
这是我的测试代码:
const int size = 2048;
const int loopSize = (int)1e7;
float* noSimd(short* shortsInput) {
float* floatsOutput = new float[size];
auto startTime = std::chrono::high_resolution_clock::now();
for (int i = 0; i < loopSize; i++) {
for (int j = 0; j < size; j++) {
floatsOutput[j] = shortsInput[j] / 2048.0f;
}
}
auto stopTime = std::chrono::high_resolution_clock::now();
long long totalTime = (stopTime - startTime).count();
printf("%lld noSimd\n", totalTime);
return floatsOutput;
}
float* wimMethod(short* shortsInput) {
const auto floatScale = _mm256_set1_ps(1.0f / 2048.0f);
float* floatsOutput = new float[size];
auto startTime = std::chrono::high_resolution_clock::now();
for (int i = 0; i < loopSize; i++) {
for (int j = 0; j < size; j += 8) {
__m128i short_vec = _mm_loadu_si128((__m128i*)&shortsInput[j]);
__m256i int_vec = _mm256_cvtepi16_epi32(short_vec);
__m256 singleComplete = _mm256_cvtepi32_ps(int_vec);
// Finally do the math
__m256 scaledVect = _mm256_mul_ps(singleComplete, floatScale);
// and puts the result where needed.
_mm256_storeu_ps(&floatsOutput[j], scaledVect);
}
}
auto stopTime = std::chrono::high_resolution_clock::now();
long long totalTime = (stopTime - startTime).count();
printf("%lld wimMethod\n", totalTime);
return floatsOutput;
}
float* chtzMethodSSE2(short* shortsInput) {
float* floatsOutput = new float[size];
auto startTime = std::chrono::high_resolution_clock::now();
for (int i = 0; i < loopSize; i++) {
for (int j = 0; j < size; j += 8) {
// get input:
__m128i val = _mm_loadu_si128((__m128i*)&shortsInput[j]);
// add 0x8000 to wrap to unsigned short domain:
val = _mm_add_epi16(val, const0x8000);
// interleave with upper part of float(1<<23)/2048.f:
__m128i lo = _mm_unpacklo_epi16(val, const0x4580);
__m128i hi = _mm_unpackhi_epi16(val, const0x4580);
// interpret as float and subtract float((1<<23) + (0x8000))/2048.f
__m128 lo_f = _mm_sub_ps(_mm_castsi128_ps(lo), constFloat);
__m128 hi_f = _mm_sub_ps(_mm_castsi128_ps(hi), constFloat);
// store:
_mm_storeu_ps(&floatsOutput[j], lo_f);
_mm_storeu_ps(&floatsOutput[j] + 4, hi_f);
}
}
auto stopTime = std::chrono::high_resolution_clock::now();
long long totalTime = (stopTime - startTime).count();
printf("%lld chtzMethod\n", totalTime);
return floatsOutput;
}
float* chtzMethodAVX2(short* shortsInput) {
const auto floatScale = _mm256_set1_ps(1.0f / 2048.0f);
float* floatsOutput = new float[size];
auto startTime = std::chrono::high_resolution_clock::now();
for (int i = 0; i < loopSize; i++) {
for (int j = 0; j < size; j += 8) {
// get input:
__m128i val = _mm_loadu_si128((__m128i*)&shortsInput[j]);
// interleave with 0x0000
__m256i val_unpacked = _mm256_cvtepu16_epi32(val);
// 0x4580'8000
const __m256 magic = _mm256_set1_ps(float((1 << 23) + (1 << 15)) / 2048.f);
const __m256i magic_i = _mm256_castps_si256(magic);
/// convert by xor-ing and subtracting magic value:
// VPXOR avoids port5 bottlenecks on Intel CPUs before SKL
__m256 val_f = _mm256_castsi256_ps(_mm256_xor_si256(val_unpacked, magic_i));
__m256 converted = _mm256_sub_ps(val_f, magic);
// store:
_mm256_storeu_ps(&floatsOutput[j], converted);
}
}
auto stopTime = std::chrono::high_resolution_clock::now();
long long totalTime = (stopTime - startTime).count();
printf("%lld chtzMethod2\n", totalTime);
return floatsOutput;
}
【问题讨论】:
-
尝试编写一个循环,看看编译器将其向量化为什么。然后你只需要检查与这些指令对应的内在函数。
-
您可以使用 `_mm256_cvtepi16_epi32 (__m128i a)` 内在函数转换为有符号整数。然后
_mm256_cvtepi32_ps (__m256i a)转换为浮点数。 -
我会使用
floatScale = _mm256_set1_ps(1.0f/2048.0f);和scaledVect = _mm256_mul_ps(singleComplete, floatScale);而不是scaledVect = _mm256_div_ps(singleComplete, floatScale);,这样会更快。 -
@wim:啊,所以 ICC 更从字面上理解内在函数,更像是汇编语言。这可能是好是坏。回复:gcc:当然,如果没有
-ffast-math,它不会取2048.1f的倒数,那将是非法的。使用该选项,它确实优化了乘以最接近1/2048.1f的浮点数,即使确切的值不能完全表示。 -
出于好奇:您是否使用 AMD cpu 进行测试?究竟是什么类型的 CPU(品牌+型号)?