Deinterleave并有效地将float转换为uint16_t

时间:2018-01-08 14:00:45

标签: x86 sse intrinsics avx yuv

我需要将浮动的打包图像缓冲区(YUVA)解交织到平面缓冲区。我还希望将这些float转换为uint16_t,但这确实很慢。我的问题是:如何通过使用内在函数加快速度?

void deinterleave(char* pixels, int rowBytes, char *bufferY, char *bufferU, char *bufferV, char *bufferA)
{
    // Scaling factors (note min. values are actually negative) (limited range)
    const float yuva_factors[4][2] = {
        { 0.07306f, 1.09132f }, // Y
        { 0.57143f, 0.57143f }, // U
        { 0.57143f, 0.57143f }, // V
        { 0.00000f, 1.00000f }  // A
    };

    float *frameBuffer = (float*)pixels;

    // De-Interleave and convert source buffer / bottom first
    for (int r = height - 1, p = 0; r >= 0; r--)
    {
        for (int c = 0; c < width; c++)
        {
            // Get beginning of next block
            const int pos = r * width * 4 + c * 4;

            // VUYA -> YUVA
            ((uint16_t*)bufferY)[p] = (uint16_t)((frameBuffer[pos + 2] + yuva_factors[0][0]) / (yuva_factors[0][0] + yuva_factors[0][1]) * 65535.0f);
            ((uint16_t*)bufferU)[p] = (uint16_t)((frameBuffer[pos + 1] + yuva_factors[1][0]) / (yuva_factors[1][0] + yuva_factors[1][1]) * 65535.0f);
            ((uint16_t*)bufferV)[p] = (uint16_t)((frameBuffer[pos + 0] + yuva_factors[2][0]) / (yuva_factors[2][0] + yuva_factors[2][1]) * 65535.0f);
            ((uint16_t*)bufferA)[p] = (uint16_t)((frameBuffer[pos + 3] + yuva_factors[3][0]) / (yuva_factors[3][0] + yuva_factors[3][1]) * 65535.0f);

            p++;
        }
    }
}

只是澄清一下:我得到了#34;像素&#34;来自此API函数的缓冲区......

// prSuiteError (*GetPixels)(PPixHand inPPixHand, PrPPixBufferAccess inRequestedAccess, char** outPixelAddress);
char *pixels;
ppixSuite->GetPixels(inRenderedFrame, PrPPixBufferAccess_ReadOnly, &pixels);

...并且根据所选的像素格式,它可以是从uint8_tfloat的任何内容。在这个用例中,它肯定是float s。

我的简化代码如下所示:

#include <stdint.h>

static const int width = 1920;
static const int height = 1080;

void unpackFloatToUint16(float* pixels, uint16_t *bufferY, uint16_t *bufferU, uint16_t *bufferV, uint16_t *bufferA)
{
    for (int r = height - 1; r >= 0; r--)
    {
        for (int c = 0; c < (int)width * 4; c += 4)
        {
            const int pos = r * width * 4 + c;

            *bufferV++ = (uint16_t)((pixels[pos] + 0.57143f) * 57342.98164f);
            *bufferU++ = (uint16_t)((pixels[pos + 1] + 0.57143f) * 57342.98164f);
            *bufferY++ = (uint16_t)((pixels[pos + 2] + 0.07306f) * 56283.17216f);
            *bufferA++ = (uint16_t)(pixels[pos + 3] * 65535.0f);
        }
    }
}

1 个答案:

答案 0 :(得分:0)

有一点是显而易见的:用乘法代替除法。如果你在FP部门遇到瓶颈,吞吐量应该增加~7到15左右。 (Haswell&#39; divss每7个时钟吞吐量有一个,但mulss每0.5个时钟就有一个。)

(假设你还没有使用-ffast-math让编译器用一个常数替换除法,用你的倒数乘以)。

GCC和clang已经自动向量化你的函数(至少是编译时常量heightwidth。问题中的代码没有编译,因为它们不是定义,所以我不知道该怎么做)See it on the Godbolt compiler explorer。如果没有-ffast-math,它会使用divps进行除法,但它会使用SIMD进行数学计算(包括转换为32位整数),并使用shuffle将第4组Y值组合在一起对于64位商店。我认为它不是一份非常有效的工作,但如果你在div吞吐量上遇到瓶颈,那么它可能比gcc好得多。

但是对于int height = rowBytes >> 1;,clang不会自动向量化,而gcc仍会设法进行自动向量化。

但是,看起来还有改进编译器工作的空间。

无论如何,我们想要为AVX + FMA手动矢量化(例如Haswell或Steamroller / Ryzen)。您也可以创建其他版本,但由于您没有指定任何关于您想要定位的微体系结构(甚至是x86),我只是将其作为一个有趣的例子。< / p>

首先,我们可以将(Y + c0) / (c0 + c1) * 65535.0f转换为单个FMA 。在添加内部分发* (1.0f/(c0+c1)) * 65535.0f以获取(Y * mul_const + add_const),可以使用单个FMA进行评估。我们可以同时对一个像素的所有4个分量执行此操作,使用128位SIMD FMA,其中两个矢量常量按照与内存中float的布局匹配的顺序保存系数。 (或者使用256位FMA同时为所有2个像素)。

不幸的是,gcc和clang没有使用-ffast-math进行优化。

保存所有shuffling直到转换为整数后可能效果最好。它的优点是您只需要两个FP常量向量,而不是每个组件的系数广播到所有元素的单独向量。好吧,我猜你可以在转换为整数之前对FMA的结果使用FP shuffle。 (例如,FMA,然后随机播放,然后将4个Y值的矢量转换为整数)。

FP数学不是严格关联的或分布式的(因为舍入误差),因此这可能会改变边缘情况的结果。但不一定会让它变得更糟,只是与旧的舍入方式不同。如果您使用FMA进行转换,(Y + const1) * const2转换为Y * altconst1 + altconst2并不会失去任何精确度,因为在进行添加之前,FMA不会对内部临时产品进行舍入。

所以我们知道如何有效地进行数学运算并转换为整数(2个CPU指令用于8个float的矢量,保持2个像素)。这使得将Ys与其他Ys组合在一起,并从32位有符号整数打包到16位无符号整数。 (x86只能在FP和有符号整数之间进行转换,直到AVX512F引入直接FP&lt; - &gt; unsigned(和64位整数&gt; FP的SIMD而不是64位模式中的标量)。无论如何,我们无法直接从float转换为16位无符号整数的向量。)

因此,给定VUYA 32位整数元素的128位向量,我们的第一步可能是缩小到16位整数。 x86有一条指令(SSE4.1 packusdw,内在_mm_packus_epi32)用于打包无符号饱和(因此负输入饱和为0,大正输入饱和为65535)。大概这是你想要的,而不是截断将使溢出环绕的整数。这需要2个SIMD向量作为输入,并产生一个输出向量,因此我们得到VYUAVYUA(对于2个不同的像素)。

即使您不需要饱和行为(例如,如果不可能超出范围输入),packusdw可能仍然是缩小整数的最有效选择。其他shuffle只有一个输入向量,或者一个固定的shuffle模式,它不会丢弃每个32位元素的上半部分,所以你在{}之后的结果中只有64位有用的数据。 {1}}或pshufb随机播放。

立即从punpck开始很容易将事情减少到只有2个向量。我查看了其他shuffle命令,但是如果你从32位shuffle开始而不是打包到16位元素,它总是花费更多的总洗牌次数。 (参见下面godbolt链接中的评论)

pack的256位AVX2版本分别对256位向量的两个128位通道进行操作,因此对于vpackusdw,您得到pack(ABCD EFGH, IJKL MNOP)。通常你需要另一个洗牌来按正确的顺序排列。无论如何我们还需要进一步洗牌,但它仍然很麻烦。尽管如此,我认为你可以在每次循环迭代中处理两倍的数据,而循环中只需要再进行几次shuffle。

这是我使用128位向量

得出的结果

源+编译器输出on the Godbolt compiler explorer

请注意,它不能处理像素数不是4的倍数的情况。您可以使用清理循环(加载,缩放和饱和包装,然后提取四个16位组件)。或者你可以做一个部分重叠的最后4个像素。 (如果像素数确实是4的倍数,则不重叠,否则将部分重叠存储到Y,U,V和A阵列中。)这很容易,因为它没有就地操作,所以你可以重新 - 存储输出后读取相同的输入。

此外,它假设行跨度与宽度匹配,因为问题中的代码做了同样的事情。因此,宽度是4像素的倍数并不重要。但是如果你确实有一个与宽度分开的可变行间距,那么你必须担心每行末尾的清理。 (或使用填充,所以你不必)。

ABCD IJKL   EFGH MNOP

希望shuffle的变量名+注释应该是人类可读的。这是未经测试的;最可能的错误是将一些向量排序错误作为shuffle的参数。但修复它应该只是扭转arg命令或其他东西,不需要额外的洗牌来减慢它。

看起来包括包装的6次洗牌是我能做的最好的。它基本上包括从vuya x4到vvvv uuuu yyyy aaaa的4x4转置,而#include <stdint.h> #include <immintrin.h> static const int height = 1024; static const int width = 1024; // helper function for unrolling static inline __m128i load_and_scale(const float *src) { // and convert to 32-bit integer with truncation towards zero. // Scaling factors (note min. values are actually negative) (limited range) const float yuvaf[4][2] = { { 0.07306f, 1.09132f }, // Y { 0.57143f, 0.57143f }, // U { 0.57143f, 0.57143f }, // V { 0.00000f, 1.00000f } // A }; // (Y + yuvaf[n][0]) / (yuvaf[n][0] + yuvaf[n][1]) -> // Y * 1.0f/(yuvaf[n][0] + yuvaf[n][1]) + yuvaf[n][0]/(yuvaf[n][0] + yuvaf[n][1]) // Pixels are in VUYA order in memory, from low to high address const __m128 scale_mul = _mm_setr_ps( 65535.0f / (yuvaf[2][0] + yuvaf[2][1]), // V 65535.0f / (yuvaf[1][0] + yuvaf[1][1]), // U 65535.0f / (yuvaf[0][0] + yuvaf[0][1]), // Y 65535.0f / (yuvaf[3][0] + yuvaf[3][1]) // A ); const __m128 scale_add = _mm_setr_ps( 65535.0f * yuvaf[2][0] / (yuvaf[2][0] + yuvaf[2][1]), // V 65535.0f * yuvaf[1][0] / (yuvaf[1][0] + yuvaf[1][1]), // U 65535.0f * yuvaf[0][0] / (yuvaf[0][0] + yuvaf[0][1]), // Y 65535.0f * yuvaf[3][0] / (yuvaf[3][0] + yuvaf[3][1]) // A ); // prefer having src aligned for performance, but with AVX it won't help the compiler much to know it's aligned. // So just use an unaligned load intrinsic __m128 srcv = _mm_loadu_ps(src); __m128 scaled = _mm_fmadd_ps(srcv, scale_mul, scale_add); __m128i vuya = _mm_cvttps_epi32(scaled); // truncate toward zero // for round-to-nearest, use cvtps_epi32 instead return vuya; } void deinterleave_avx_fma(char* __restrict pixels, int rowBytes, char *__restrict bufferY, char *__restrict bufferU, char *__restrict bufferV, char *__restrict bufferA) { const float *src = (float*)pixels; uint16_t *__restrict Y = (uint16_t*)bufferY; uint16_t *__restrict U = (uint16_t*)bufferU; uint16_t *__restrict V = (uint16_t*)bufferV; uint16_t *__restrict A = (uint16_t*)bufferA; // 4 pixels per loop iteration, loading 4x 16 bytes of floats // and storing 4x 8 bytes of uint16_t. for (unsigned pos = 0 ; pos < width*height * 4; pos += 4) { // pos*4 because each pixel is 4 floats long __m128i vuya0 = load_and_scale(src+pos*4); __m128i vuya1 = load_and_scale(src+pos*4 + 1); __m128i vuya2 = load_and_scale(src+pos*4 + 2); __m128i vuya3 = load_and_scale(src+pos*4 + 3); __m128i vuya02 = _mm_packus_epi32(vuya0, vuya2); // vuya0 | vuya2 __m128i vuya13 = _mm_packus_epi32(vuya1, vuya3); // vuya1 | vuya3 __m128i vvuuyyaa01 = _mm_unpacklo_epi16(vuya02, vuya13); // V0V1 U0U1 | Y0Y1 A0A1 __m128i vvuuyyaa23 = _mm_unpackhi_epi16(vuya02, vuya13); // V2V3 U2U3 | Y2Y3 A2A3 __m128i vvvvuuuu = _mm_unpacklo_epi32(vvuuyyaa01, vvuuyyaa23); // v0v1v2v3 | u0u1u2u3 __m128i yyyyaaaa = _mm_unpackhi_epi32(vvuuyyaa01, vvuuyyaa23); // we have 2 vectors holding our four 64-bit results (or four 16-bit elements each) // We can most efficiently store with VMOVQ and VMOVHPS, even though MOVHPS is "for" FP vectors // Further shuffling of another 4 pixels to get 128b vectors wouldn't be a win: // MOVHPS is a pure store on Intel CPUs, no shuffle uops. // And we have more shuffles than stores already. //_mm_storeu_si64(V+pos, vvvvuuuu); // clang doesn't have this (AVX512?) intrinsic _mm_storel_epi64((__m128i*)(V+pos), vvvvuuuu); // MOVQ _mm_storeh_pi((__m64*)(U+pos), _mm_castsi128_ps(vvvvuuuu)); // MOVHPS _mm_storel_epi64((__m128i*)(Y+pos), yyyyaaaa); _mm_storeh_pi((__m64*)(A+pos), _mm_castsi128_ps(yyyyaaaa)); } } 有一个固定的shuffle模式,它对解交织没有帮助,所以我不认为我们可以做用128位向量比这更好。当然,我总是可以忽视某些事情。

gcc和clang都略微次优编译

Clang使用pack而不是vpextrq(在每次循环迭代中,在Intel CPU上花费额外的2个shuffle uops)。并且还使用2个单独的循环计数器,而不是将相同的计数器缩放1或8,因此需要花费1个额外的整数vmovhps指令,这没有任何好处。 (如果只有gcc选择这样做而不是使用折叠到FMA中的索引载荷......愚蠢的编译器。)

gcc,而不是使用add加载,处理FMA3,如果它的输入通过复制向量常量然后使用带有索引寻址模式的内存操作数来销毁它。 This doesn't stay micro-fused,因此前端有4个额外的uop。

如果编译完美,只有一个循环计数器用作vmovups源的数组索引(由float缩放)和整数目标数组(未缩放),如gcc,以及gcc按照clang的方式进行加载,然后整个循环将是Haswell / Skylake上的24个融合域uops。

因此它可以每6个时钟在一次迭代中从前端发出,正好是每个时钟4个融合域uop的限制。它包含6个shuffle uops,因此它也可能正好抵御port5吞吐量瓶颈。 (HSW / SKL只有1个shuffle执行单元)。理论上 ,每6个时钟可以运行4个像素(16个浮点数),或者在Intel CPU上每1.5个时钟周期运行一个像素。对于Ryzen来说可能稍微多一点,虽然MOVHPS在那里需要多次uops,但管道更广泛。 (见http://agner.org/optimize/

每6个时钟4个负载和4个存储是很好的,远离任何瓶颈,除非你的src和dst在高速缓存中不热,可能的内存带宽除外。商店只有一半的宽度,一切都是连续的。来自一个循环的4个单独的输出流很少,因此它通常不是问题。 (如果你有超过4个输出流,你可能会考虑分裂循环,只在一次传递中存储U和V值,然后只有另一个传递的Y和A值通过相同的源数据,或者其他东西。但是喜欢我说过,4个输出流很好,不能保证循环裂变。)

这样的256位版本最后可能只需要额外增加2 *8,因为我不认为您可以轻松解决在{{ {1}}被卡在同一vpermq向量的高低通道中。因此,您可能需要在此过程的早期进行额外的4 bufferV__m256i次洗牌,因为最小的粒度交叉shuffle是32位粒度。这可能会伤害Ryzen很多。

在将4个或8个vpermd组合在一起后,使用vperm2i128重新排列向量之间的单词元素可能会做一些事情,但错误的vpblendw s。

AVX512 doesn't have the same in-lane design for most shuffles,所以我认为AVX512版本会更便宜。 AVX512具有缩小饱和/截断指令,但这些指令比Skylake-X上的v慢,所以可能没有。