如何使用 avx2 将 24 位 rgb 转换为 32 位?
How to transform 24bit rgb to 32bit using avx2?
我已经使用 SSSE3 完成了此操作,现在我想知道是否可以使用 AVX2 完成此操作以获得更好的性能?
我使用 Fast 24-bit array -> 32-bit array conversion?.
中的代码用一个零字节填充 24 位 rgb
static const __m128i mask = _mm_setr_epi8(0, 1, 2, -1, 3, 4, 5, -1, 6, 7, 8, -1, 9, 10, 11, -1);
for (size_t row = 0; row < height; ++row)
{
for (size_t column = 0; column < width; column += 16)
{
const __m128i *src = reinterpret_cast<const __m128i *>(in + row * in_pitch + column + (column << 1));
__m128i *dst = reinterpret_cast<__m128i *>(out + row * out_pitch + (column << 2));
__m128i v[4];
v[0] = _mm_load_si128(src);
v[1] = _mm_load_si128(src + 1);
v[2] = _mm_load_si128(src + 2);
v[3] = _mm_shuffle_epi8(v[0], mask);
_mm_store_si128(dst, v[3]);
v[3] = _mm_shuffle_epi8(_mm_alignr_epi8(v[1], v[0], 12), mask);
_mm_store_si128(dst + 1, v[3]);
v[3] = _mm_shuffle_epi8(_mm_alignr_epi8(v[2], v[1], 8), mask);
_mm_store_si128(dst + 2, v[3]);
v[3] = _mm_shuffle_epi8(_mm_alignr_epi8(v[2], v[2], 4), mask);
_mm_store_si128(dst + 3, v[3]);
}
}
问题是 _mm256_shuffle_epi8 分别洗牌高 128 位和低 128 位,所以掩码不能只替换为
_mm256_setr_epi8(0, 1, 2, -1, 3, 4, 5, -1, 6, 7, 8, -1, 9, 10, 11, -1, 12, 13, 14, -1, 15, 16, 17, -1, 18, 19, 20, -1, 21, 22, 23, -1);
另外 _mm_alignr_epi8
需要替换为 _mm256_permute2x128_si256
和 _mm256_alignr_epi8
您可以使用 AVX2 相当高效地一次处理 8 个像素(24 个输入字节和 32 个输出字节)。
您只需对齐负载,使您要处理的 24 字节像素块 居中 在 32 字节负载的中间,而不是通常的将负载对齐到像素块开始的方法2。这意味着 车道边界 将落在像素 4 和 5 之间,并且您将在每个车道中拥有恰好 4 个像素的字节。结合适当的洗牌掩码,这应该是 SSE 的两倍效率。
例如:
给定一个输入指针 uint8_t input[]
你用非 SIMD 代码处理前四个像素 1 然后在 [=11= 处加载你的第一个 32 字节] 以便低位通道(字节 0-15)在其高位字节中获取像素 4、5、6、7 的 12 个有效载荷字节,紧接着是高位通道中接下来的 4 个像素的下 12 个有效载荷字节.然后使用 pshufb
将像素扩展到正确的位置(每个通道需要不同的掩码,因为您将低通道中的像素移向较低位置,而高通道中的像素移至较高位置,但这不会造成问题)。然后下一次加载将在 input[26]
(24 个字节后),依此类推。
使用这种方法,您应该在每个循环吞吐量中获得大约 8 个像素,以实现完美缓存 input/output - 限制为 1/循环存储吞吐量和 1/循环随机播放吞吐量。幸运的是,这种方法与始终对齐的存储兼容(因为存储增量是 32 字节)。您将有一些未对齐的负载,但这些负载仍可能以 1/周期发生,因此不应该成为瓶颈。
值得注意的是,这种方法 "only works once" 在 SIMD 指令集扩展方面:当您有 2 条通道时它会起作用,但不会更多(因此相同的想法不适用于 512-具有 4 个 128 位通道的 AVX512 位)。
1这避免了在输入数组之前越界读取:如果你知道这是安全的,你可以避免这一步。
2也就是说,如果你从addr
加载它是addr + 16
应该在像素边界((addr + 16) % 12 == 0
),不是 addr
.
这是原始的 SSSE3 代码,其中包含一些我自己的调度。
void DspConvertPcm(f32* pOutBuffer, const s24* pInBuffer, size_t totalSampleCount)
{
constexpr f32 fScale = static_cast<f32>(1.0 / (1<<23));
size_t i = 0;
size_t vecSampleCount = 0;
#if defined(SFTL_SSE2)
if (CpuInfo::GetSupports_SIMD_I32x8())
{
vecSampleCount = DspConvertPcm_AVX2(pOutBuffer, pInBuffer, totalSampleCount);
}
else
if (CpuInfo::GetSupports_SSE3())
{
const auto vScale = _mm_set1_ps(fScale);
const auto mask = _mm_setr_epi8(-1, 0, 1, 2, -1, 3, 4, 5, -1, 6, 7, 8, -1, 9, 10, 11);
constexpr size_t step = 16;
vecSampleCount = (totalSampleCount / step) * step;
for (; i < vecSampleCount; i += step)
{
const auto* pSrc = reinterpret_cast<const __m128i*>(pInBuffer + i);
auto* pDst = pOutBuffer + i;
const auto sa = _mm_loadu_si128(pSrc + 0);
const auto sb = _mm_loadu_si128(pSrc + 1);
const auto sc = _mm_loadu_si128(pSrc + 2);
const auto da = _mm_srai_epi32(_mm_shuffle_epi8(sa, mask), 8);
const auto db = _mm_srai_epi32(_mm_shuffle_epi8(_mm_alignr_epi8(sb, sa, 12), mask), 8);
const auto dc = _mm_srai_epi32(_mm_shuffle_epi8(_mm_alignr_epi8(sc, sb, 8), mask), 8);
const auto dd = _mm_srai_epi32(_mm_shuffle_epi8(_mm_alignr_epi8(sc, sc, 4), mask), 8);
// Convert to float and store
_mm_storeu_ps(pDst + 0, _mm_mul_ps(_mm_cvtepi32_ps(da), vScale));
_mm_storeu_ps(pDst + 4, _mm_mul_ps(_mm_cvtepi32_ps(db), vScale));
_mm_storeu_ps(pDst + 8, _mm_mul_ps(_mm_cvtepi32_ps(dc), vScale));
_mm_storeu_ps(pDst + 12, _mm_mul_ps(_mm_cvtepi32_ps(dd), vScale));
}
}
#endif
for (; i < totalSampleCount; i += 1)
{
pOutBuffer[i] = (static_cast<s32>(pInBuffer[i])) * fScale;
}
}
如果存在 AVX2,它将调用 DspConvertPcm_AVX2,如下所示:
size_t DspConvertPcm_AVX2(f32* pOutBuffer, const s24* pInBuffer, size_t totalSampleCount)
{
SFTL_ASSERT(CpuInfo::GetSupports_SIMD_I32x8());
constexpr f32 fScale = static_cast<f32>(1.0 / (1 << 23));
const auto vScale = _mm256_set1_ps(fScale);
auto fnDo16Samples = [vScale](f32* pOutBuffer, const s24* pInBuffer)
{
const auto vScaleSSE = _mm256_castps256_ps128(vScale);
const auto mask = _mm_setr_epi8(-1, 0, 1, 2, -1, 3, 4, 5, -1, 6, 7, 8, -1, 9, 10, 11);
const auto* pSrc = reinterpret_cast<const __m128i*>(pInBuffer);
auto* pDst = pOutBuffer;
const auto sa = _mm_loadu_si128(pSrc + 0);
const auto sb = _mm_loadu_si128(pSrc + 1);
const auto sc = _mm_loadu_si128(pSrc + 2);
const auto da = _mm_srai_epi32(_mm_shuffle_epi8(sa, mask), 8);
const auto db = _mm_srai_epi32(_mm_shuffle_epi8(_mm_alignr_epi8(sb, sa, 12), mask), 8);
const auto dc = _mm_srai_epi32(_mm_shuffle_epi8(_mm_alignr_epi8(sc, sb, 8), mask), 8);
const auto dd = _mm_srai_epi32(_mm_shuffle_epi8(_mm_alignr_epi8(sc, sc, 4), mask), 8);
// Convert to float and store
_mm_storeu_ps(pDst + 0, _mm_mul_ps(_mm_cvtepi32_ps(da), vScaleSSE));
_mm_storeu_ps(pDst + 4, _mm_mul_ps(_mm_cvtepi32_ps(db), vScaleSSE));
_mm_storeu_ps(pDst + 8, _mm_mul_ps(_mm_cvtepi32_ps(dc), vScaleSSE));
_mm_storeu_ps(pDst + 12, _mm_mul_ps(_mm_cvtepi32_ps(dd), vScaleSSE));
};
// First 16 samples SSE style
fnDo16Samples(pOutBuffer, pInBuffer);
// Next samples do AVX, where each load will discard 4 bytes at the start and end of each load
constexpr size_t step = 16;
const size_t vecSampleCount = ((totalSampleCount / step) * step) - 16;
{
const auto mask = _mm256_setr_epi8(-1, 4, 5, 6, -1, 7, 8, 9, -1, 10, 11, 12, -1, 13, 14, 15, -1, 16, 17, 18, -1, 19, 20, 21, -1, 22, 23, 24, -1, 25, 26, 27);
for (size_t i = 16; i < vecSampleCount; i += step)
{
const byte* pByteBuffer = reinterpret_cast<const byte*>(pInBuffer + i);
auto* pDst = pOutBuffer + i;
const auto vs24_00_07 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(pByteBuffer - 4));
const auto vs24_07_15 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(pByteBuffer - 24));
const auto vf32_00_07 = _mm256_srai_epi32(_mm256_shuffle_epi8(vs24_00_07, mask), 8);
const auto vf32_07_15 = _mm256_srai_epi32(_mm256_shuffle_epi8(vs24_07_15, mask), 8);
// Convert to float and store
_mm256_storeu_ps(pDst + 0, _mm256_mul_ps(_mm256_cvtepi32_ps(vf32_00_07), vScale));
_mm256_storeu_ps(pDst + 8, _mm256_mul_ps(_mm256_cvtepi32_ps(vf32_00_07), vScale));
}
}
// Last 16 samples SSE style
fnDo16Samples(pOutBuffer + vecSampleCount, pInBuffer + vecSampleCount);
return vecSampleCount;
}
请注意,我手动展开了 AVX2 主循环以尝试加快速度,但这并没有太大关系。
在调用 DspConvertPcm 之前绑定一个定时器,一次处理 1024 个样本,此处启用 AVX2 代码路径的平均处理时间将在 2.6 到 3.0 微秒之间变化。另一方面,如果我禁用 AVX2 代码路径,平均时间徘徊在 2.0 微秒左右。
另一方面,使用 /arch:AVX2 启用 VEX 编码并没有给我之前声称的一致的性能提升,所以这一定是侥幸。
此测试是在 Haswell 核心 i7-6700HQ @ 2.6 GHz 上使用 Visual Studio 15.9.5 上的默认 MSVC 编译器执行的,启用了速度优化并使用 /fp:fast。
我已经使用 SSSE3 完成了此操作,现在我想知道是否可以使用 AVX2 完成此操作以获得更好的性能?
我使用 Fast 24-bit array -> 32-bit array conversion?.
中的代码用一个零字节填充 24 位 rgb static const __m128i mask = _mm_setr_epi8(0, 1, 2, -1, 3, 4, 5, -1, 6, 7, 8, -1, 9, 10, 11, -1);
for (size_t row = 0; row < height; ++row)
{
for (size_t column = 0; column < width; column += 16)
{
const __m128i *src = reinterpret_cast<const __m128i *>(in + row * in_pitch + column + (column << 1));
__m128i *dst = reinterpret_cast<__m128i *>(out + row * out_pitch + (column << 2));
__m128i v[4];
v[0] = _mm_load_si128(src);
v[1] = _mm_load_si128(src + 1);
v[2] = _mm_load_si128(src + 2);
v[3] = _mm_shuffle_epi8(v[0], mask);
_mm_store_si128(dst, v[3]);
v[3] = _mm_shuffle_epi8(_mm_alignr_epi8(v[1], v[0], 12), mask);
_mm_store_si128(dst + 1, v[3]);
v[3] = _mm_shuffle_epi8(_mm_alignr_epi8(v[2], v[1], 8), mask);
_mm_store_si128(dst + 2, v[3]);
v[3] = _mm_shuffle_epi8(_mm_alignr_epi8(v[2], v[2], 4), mask);
_mm_store_si128(dst + 3, v[3]);
}
}
问题是 _mm256_shuffle_epi8 分别洗牌高 128 位和低 128 位,所以掩码不能只替换为
_mm256_setr_epi8(0, 1, 2, -1, 3, 4, 5, -1, 6, 7, 8, -1, 9, 10, 11, -1, 12, 13, 14, -1, 15, 16, 17, -1, 18, 19, 20, -1, 21, 22, 23, -1);
另外 _mm_alignr_epi8
需要替换为 _mm256_permute2x128_si256
和 _mm256_alignr_epi8
您可以使用 AVX2 相当高效地一次处理 8 个像素(24 个输入字节和 32 个输出字节)。
您只需对齐负载,使您要处理的 24 字节像素块 居中 在 32 字节负载的中间,而不是通常的将负载对齐到像素块开始的方法2。这意味着 车道边界 将落在像素 4 和 5 之间,并且您将在每个车道中拥有恰好 4 个像素的字节。结合适当的洗牌掩码,这应该是 SSE 的两倍效率。
例如:
给定一个输入指针 uint8_t input[]
你用非 SIMD 代码处理前四个像素 1 然后在 [=11= 处加载你的第一个 32 字节] 以便低位通道(字节 0-15)在其高位字节中获取像素 4、5、6、7 的 12 个有效载荷字节,紧接着是高位通道中接下来的 4 个像素的下 12 个有效载荷字节.然后使用 pshufb
将像素扩展到正确的位置(每个通道需要不同的掩码,因为您将低通道中的像素移向较低位置,而高通道中的像素移至较高位置,但这不会造成问题)。然后下一次加载将在 input[26]
(24 个字节后),依此类推。
使用这种方法,您应该在每个循环吞吐量中获得大约 8 个像素,以实现完美缓存 input/output - 限制为 1/循环存储吞吐量和 1/循环随机播放吞吐量。幸运的是,这种方法与始终对齐的存储兼容(因为存储增量是 32 字节)。您将有一些未对齐的负载,但这些负载仍可能以 1/周期发生,因此不应该成为瓶颈。
值得注意的是,这种方法 "only works once" 在 SIMD 指令集扩展方面:当您有 2 条通道时它会起作用,但不会更多(因此相同的想法不适用于 512-具有 4 个 128 位通道的 AVX512 位)。
1这避免了在输入数组之前越界读取:如果你知道这是安全的,你可以避免这一步。
2也就是说,如果你从addr
加载它是addr + 16
应该在像素边界((addr + 16) % 12 == 0
),不是 addr
.
这是原始的 SSSE3 代码,其中包含一些我自己的调度。
void DspConvertPcm(f32* pOutBuffer, const s24* pInBuffer, size_t totalSampleCount)
{
constexpr f32 fScale = static_cast<f32>(1.0 / (1<<23));
size_t i = 0;
size_t vecSampleCount = 0;
#if defined(SFTL_SSE2)
if (CpuInfo::GetSupports_SIMD_I32x8())
{
vecSampleCount = DspConvertPcm_AVX2(pOutBuffer, pInBuffer, totalSampleCount);
}
else
if (CpuInfo::GetSupports_SSE3())
{
const auto vScale = _mm_set1_ps(fScale);
const auto mask = _mm_setr_epi8(-1, 0, 1, 2, -1, 3, 4, 5, -1, 6, 7, 8, -1, 9, 10, 11);
constexpr size_t step = 16;
vecSampleCount = (totalSampleCount / step) * step;
for (; i < vecSampleCount; i += step)
{
const auto* pSrc = reinterpret_cast<const __m128i*>(pInBuffer + i);
auto* pDst = pOutBuffer + i;
const auto sa = _mm_loadu_si128(pSrc + 0);
const auto sb = _mm_loadu_si128(pSrc + 1);
const auto sc = _mm_loadu_si128(pSrc + 2);
const auto da = _mm_srai_epi32(_mm_shuffle_epi8(sa, mask), 8);
const auto db = _mm_srai_epi32(_mm_shuffle_epi8(_mm_alignr_epi8(sb, sa, 12), mask), 8);
const auto dc = _mm_srai_epi32(_mm_shuffle_epi8(_mm_alignr_epi8(sc, sb, 8), mask), 8);
const auto dd = _mm_srai_epi32(_mm_shuffle_epi8(_mm_alignr_epi8(sc, sc, 4), mask), 8);
// Convert to float and store
_mm_storeu_ps(pDst + 0, _mm_mul_ps(_mm_cvtepi32_ps(da), vScale));
_mm_storeu_ps(pDst + 4, _mm_mul_ps(_mm_cvtepi32_ps(db), vScale));
_mm_storeu_ps(pDst + 8, _mm_mul_ps(_mm_cvtepi32_ps(dc), vScale));
_mm_storeu_ps(pDst + 12, _mm_mul_ps(_mm_cvtepi32_ps(dd), vScale));
}
}
#endif
for (; i < totalSampleCount; i += 1)
{
pOutBuffer[i] = (static_cast<s32>(pInBuffer[i])) * fScale;
}
}
如果存在 AVX2,它将调用 DspConvertPcm_AVX2,如下所示:
size_t DspConvertPcm_AVX2(f32* pOutBuffer, const s24* pInBuffer, size_t totalSampleCount)
{
SFTL_ASSERT(CpuInfo::GetSupports_SIMD_I32x8());
constexpr f32 fScale = static_cast<f32>(1.0 / (1 << 23));
const auto vScale = _mm256_set1_ps(fScale);
auto fnDo16Samples = [vScale](f32* pOutBuffer, const s24* pInBuffer)
{
const auto vScaleSSE = _mm256_castps256_ps128(vScale);
const auto mask = _mm_setr_epi8(-1, 0, 1, 2, -1, 3, 4, 5, -1, 6, 7, 8, -1, 9, 10, 11);
const auto* pSrc = reinterpret_cast<const __m128i*>(pInBuffer);
auto* pDst = pOutBuffer;
const auto sa = _mm_loadu_si128(pSrc + 0);
const auto sb = _mm_loadu_si128(pSrc + 1);
const auto sc = _mm_loadu_si128(pSrc + 2);
const auto da = _mm_srai_epi32(_mm_shuffle_epi8(sa, mask), 8);
const auto db = _mm_srai_epi32(_mm_shuffle_epi8(_mm_alignr_epi8(sb, sa, 12), mask), 8);
const auto dc = _mm_srai_epi32(_mm_shuffle_epi8(_mm_alignr_epi8(sc, sb, 8), mask), 8);
const auto dd = _mm_srai_epi32(_mm_shuffle_epi8(_mm_alignr_epi8(sc, sc, 4), mask), 8);
// Convert to float and store
_mm_storeu_ps(pDst + 0, _mm_mul_ps(_mm_cvtepi32_ps(da), vScaleSSE));
_mm_storeu_ps(pDst + 4, _mm_mul_ps(_mm_cvtepi32_ps(db), vScaleSSE));
_mm_storeu_ps(pDst + 8, _mm_mul_ps(_mm_cvtepi32_ps(dc), vScaleSSE));
_mm_storeu_ps(pDst + 12, _mm_mul_ps(_mm_cvtepi32_ps(dd), vScaleSSE));
};
// First 16 samples SSE style
fnDo16Samples(pOutBuffer, pInBuffer);
// Next samples do AVX, where each load will discard 4 bytes at the start and end of each load
constexpr size_t step = 16;
const size_t vecSampleCount = ((totalSampleCount / step) * step) - 16;
{
const auto mask = _mm256_setr_epi8(-1, 4, 5, 6, -1, 7, 8, 9, -1, 10, 11, 12, -1, 13, 14, 15, -1, 16, 17, 18, -1, 19, 20, 21, -1, 22, 23, 24, -1, 25, 26, 27);
for (size_t i = 16; i < vecSampleCount; i += step)
{
const byte* pByteBuffer = reinterpret_cast<const byte*>(pInBuffer + i);
auto* pDst = pOutBuffer + i;
const auto vs24_00_07 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(pByteBuffer - 4));
const auto vs24_07_15 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(pByteBuffer - 24));
const auto vf32_00_07 = _mm256_srai_epi32(_mm256_shuffle_epi8(vs24_00_07, mask), 8);
const auto vf32_07_15 = _mm256_srai_epi32(_mm256_shuffle_epi8(vs24_07_15, mask), 8);
// Convert to float and store
_mm256_storeu_ps(pDst + 0, _mm256_mul_ps(_mm256_cvtepi32_ps(vf32_00_07), vScale));
_mm256_storeu_ps(pDst + 8, _mm256_mul_ps(_mm256_cvtepi32_ps(vf32_00_07), vScale));
}
}
// Last 16 samples SSE style
fnDo16Samples(pOutBuffer + vecSampleCount, pInBuffer + vecSampleCount);
return vecSampleCount;
}
请注意,我手动展开了 AVX2 主循环以尝试加快速度,但这并没有太大关系。
在调用 DspConvertPcm 之前绑定一个定时器,一次处理 1024 个样本,此处启用 AVX2 代码路径的平均处理时间将在 2.6 到 3.0 微秒之间变化。另一方面,如果我禁用 AVX2 代码路径,平均时间徘徊在 2.0 微秒左右。
另一方面,使用 /arch:AVX2 启用 VEX 编码并没有给我之前声称的一致的性能提升,所以这一定是侥幸。
此测试是在 Haswell 核心 i7-6700HQ @ 2.6 GHz 上使用 Visual Studio 15.9.5 上的默认 MSVC 编译器执行的,启用了速度优化并使用 /fp:fast。