使用SSE2(作为浮点数)缩放字节像素值(y = ax + b)?

时间:2015-08-29 08:26:22

标签: c++ visual-studio x86 simd sse2

我想计算y = ax + b,其中x和y是像素值[即,值范围为0~255的字节],而ab是浮点数< / p>

由于我需要对图像中的每个像素应用此公式,此外,a和b对于不同的像素是不同的。在C ++中直接计算很慢,所以我很有兴趣知道c ++中的sse2指令。

搜索之后,我发现浮点数与sse2的乘法和加法就像_mm_mul_ps_mm_add_ps一样。但首先,我需要将字节中的x转换为float(4字节)。

问题是,在我从字节数据源(_mm_load_si128)加载数据后,如何将数据从byte转换为float?

2 个答案:

答案 0 :(得分:5)

每个像素的

ab是不同的?除非有一个模式或者你可以生成它们,否则这会使矢量化变得困难

有没有什么方法可以在向量中有效地生成ab,无论是定点还是浮点?如果没有,插入4个FP值或8个16位整数可能比标量操作更差。

固定点

如果ab可以完全重用,或者首先使用定点生成,这可能是定点数学的一个很好的用例。 (即表示值* 2 ^标度的整数)。 SSE / AVX没有8b * 8b-> 16b乘法;最小的元素是单词,所以你必须将字节解包为单词,但不是一直到32位。这意味着每条指令可以处理两倍的数据。

如果_mm_maddubs_epi16b不经常更改,或者您可以轻松生成一个交替a * 2 ^ 4和b的向量,那么a指令可能很有用* 2 ^ 1个字节。显然它真的handy for bilinear interpolation,但如果我们可以准备a和b矢量,它仍然可以通过最小的改组为我们完成工作。

float a, b;
const int logascale = 4, logbscale=1;
const int ascale = 1<<logascale;  // fixed point scale for a: 2^4
const int bscale = 1<<logbscale;  // fixed point scale for b: 2^1

const __m128i brescale = _mm_set1_epi8(1<<(logascale-logbscale));  // re-scale b to match a in the 16bit temporary result

for (i=0 ; i<n; i+=16) {
    //__m128i avec = get_scaled_a(i);  
    //__m128i bvec = get_scaled_b(i);
    //__m128i ab_lo = _mm_unpacklo_epi8(avec, bvec);
    //__m128i ab_hi = _mm_unpackhi_epi8(avec, bvec);

    __m128i abvec = _mm_set1_epi16( ((int8_t)(bscale*b) << 8) | (int8_t)(ascale*a) );  // integer promotion rules might do sign-extension in the wrong place here, so check this if you actually write it this way.

    __m128i block = _mm_load_si128(&buf[i]);  // call this { v[0] .. v[15] }

    __m128i lo = _mm_unpacklo_epi8(block, brescale);  // {v[0], 8, v[1], 8, ...}
    __m128i hi = _mm_unpackhi_epi8(block, brescale);  // {v[8], 8, v[9], 8, ...
    lo = _mm_maddubs_epi16(lo, abvec);  // first arg is unsigned bytes, 2nd arg is signed bytes
    hi = _mm_maddubs_epi16(hi, abvec);
    // lo = { v[0]*(2^4*a) + 8*(2^1*b), ... }

    lo = _mm_srli_epi16(lo, logascale);  // truncate from scaled fixed-point to integer
    hi = _mm_srli_epi16(hi, logascale);

    // and re-pack.  Logical, not arithmetic right shift means sign bits can't be set
    block = _mm_packuswb(lo, hi);  
    _mm_store_si128(&buf[i], block);
}
// then a scalar cleanup loop

2 ^ 4是任意选择。它为a的整数部分和4个分数位留下3个非符号位。因此它有效地将a舍入到最接近的第16个,如果它的幅度大于8和15/16,则溢出。 2 ^ 6会给出更多的小数位,并允许a从-2到+1和63 / 64th。

由于b被添加而不是相乘,因此它的有用范围要大得多,而小数部分则不那么有用。为了用8位表示它,将它四舍五入到最近的一半仍保留一些小数信息,但允许它为[-64:63.5]而不会溢出。

为了获得更高的精度,16b定点是一个不错的选择。你可以将ab缩放2 ^ 7或者其他东西,得到7b的小数精度,并且仍然允许整数部分为[-256 ... 255]。对于这种情况,没有乘法和加法指令,因此您必须单独执行此操作。进行乘法的好选择包括:

  • _mm_mulhi_epu16:无符号16b * 16b-&gt; high16(位[31:16])。如果a不能为负,则很有用
  • _mm_mulhi_epi16:签名16b * 16b-&gt; high16(位[31:16])。
  • _mm_mulhrs_epi16:签署了32b临时的16b * 16b->位[30:15],并进行了舍入。通过选择a的缩放因子,这应该更好。据我了解,SSSE3引入了这条指令,正是为了这种用途。
  • _mm_mullo_epi16:签名16b * 16b-&gt; low16(位[15:0])。在low16结果溢出之前,这只允许a的8个有效位,所以我认为你对_mm_maddubs_epi16 8位解决方案的所有收益都更加精确b

要使用这些,您需要获得ab值的16b向量,然后:

  • 使用零(或pmovzx byte-&gt;字)解压缩字节,以使签名字仍然在[0..255]范围内
  • 将单词改为7。
  • 乘以16b字的a向量,取每个16 * 16-> 32结果的上半部分。 (例如mul
  • 如果您想要ab的不同比例,请转到此处,以获得a
  • 的更高分数精度
  • 添加b
  • 右移以从固定点到[0..255]进行最终截断。

通过良好的定点刻度选择,这应该能够处理更宽范围的ab,以及比8位定点更高的分数精度。

如果你在将它们解包为单词后没有左移你的字节,a必须是全范围的,只是为了在结果的高16中设置8位。这意味着您可以支持非常有限的a范围,而不会在乘法期间将临时值截断为小于8位。即使_mm_mulhrs_epi16也没有留下太多空间,因为它从第30位开始。

将字节扩展为浮点数

如果您无法为每个像素有效地生成定点ab值,则最好将像素转换为浮点数。这需要更多的解包/重新打包,因此延迟和吞吐量更差。值得研究用固定点生成a和b。

要使pack-float工作,您仍然必须为4个相邻像素有效地构建a值的向量。

这是pmovzx(SSE4.1)的一个很好的用例,因为它可以直接从8b元素转到32b。其他选项包括具有多个步骤的SSE2 punpck[l/h]bw/punpck[l/h]wd或用于模拟pshufb的SSSE3 pmovzx。 (您可以执行一次16B加载并以4种不同的方式将其解压缩到四个32b整数的向量。)

char *buf;

// const __m128i zero = _mm_setzero_si128();
for (i=0 ; i<n; i+=16) {
    __m128 a = get_a(i);
    __m128 b = get_b(i);

    // IDK why there isn't an intrinsic for using `pmovzx` as a load, because it takes a m32 or m64 operand, not m128.  (unlike punpck*)
    __m128i unsigned_dwords = _mm_cvtepu8_epi32((__m128i)(buf+i));  // load 4B at once.
    __m128 floats = _mm_cvtepi32_ps(unsigned_dwords);

    floats = _mm_fmadd_ps(floats, a, b);  // with FMA available, this might as well be 256b vectors, even with the inconvenience of the different lane-crossing semantics of pmovzx vs. punpck
    // or without FMA, do this with _mm_mul_ps and _mm_add_ps
    unsigned_dwords = _mm_cvtps_epi32(floats);

    // repeat 3 more times for buf+4, buf+8, and buf+12, then:

    __m128i packed01 = _mm_packss_epi32(dwords0, dwords1);  // SSE2
    __m128i packed23 = _mm_packss_epi32(dwords2, dwords3);
    // packuswb wants SIGNED input, so do signed saturation on the first step

    // saturate into [0..255] range
    __m12i8 packedbytes=_mm_packus_epi16(packed01, packed23);  // SSE2
    _mm_store_si128(buf+i, packedbytes);  // or storeu if buf isn't aligned.
}

// cleanup code to handle the odd up-to-15 leftover bytes, if n%16 != 0

此答案的先前版本来自带有packusdw / packuswb的float-&gt; uint8向量,并且在没有SSE4.1的情况下有一个关于变通方法的整个部分。如果只是stay in the signed integer domain until the last pack,则无需在无符号包之后屏蔽该符号位。我假设这是SSE2仅包含从dword到word的signed pack的原因,但是从word到byte都有signed和unsigned pack。 packuswd仅在您的最终目标是uint16_t时才有用,而不是进一步打包。

的最后一个CPU有SSE4.1是Intel Conroe / merom(第一代Core2,从2007年底开始)和AMD pre Barcelona(2007年底之前)。如果这些CPU可以接受慢速工作,那么只需为AVX2编写一个版本,为SSE4.1编写一个版本。或者SSSE3(使用4x pshufb来模拟寄存器的四个32b元素的pmovzxbd)pshufb在Conroe上很慢,所以如果你关心没有SSE4.1的CPU,那就写一个特定的版本。实际上,Conroe / merom也有慢xmm punpcklbw等等(q-> dq除外)。 4x慢pshufb应该仍然比6x慢解压缩。由于拆包和重新包装的缓慢洗牌,矢量化在前Wolfdale上的胜利要少得多。定点版本的解包/重新打包要少得多,在那里会有更大的优势。

在我意识到需要多少额外指令之前,请参阅使用punpck的未完成尝试的编辑历史记录。删除它是因为这个答案已经很久了,另一个代码块会让人感到困惑。

答案 1 :(得分:1)

我猜你正在寻找__m128 _mm_cvtpi8_ps(__m64 a )复合内在。

这是一个最小的例子:

#include <xmmintrin.h>
#include <stdio.h>

int main() {
  unsigned char  a[4] __attribute__((aligned(32)))= {1,2,3,4};
  float b[4] __attribute__((aligned(32)));
  _mm_store_ps(b, _mm_cvtpi8_ps(*(__m64*)a));
  printf("%f %f, %f, %f\n", b[0], b[1], b[2], b[3]);
  return 0;
}