首页 > 解决方案 > 如何将两个位图与 80-20% 的 AVX2 混合?

问题描述

我有 2 个位图。我想将它们混合成 80:20 的部分,所以我只需将像素值乘以 0,8 和 0,2。用 C 语言编写的代码可以正常工作(作为 for 循环),但使用 AVX2 指令会导致输出图像错误。

#include <stdio.h>
#include <stdlib.h>
#include <immintrin.h>

#define ARRSIZE 5992826

void main(void){

 FILE *bmp    = fopen("filepath1", "rb"),
      *bmpp   = fopen("filepath2", "rb"),
      *write  = fopen("output", "wb");

 unsigned char *a = aligned_alloc(32, ARRSIZE),
               *b = aligned_alloc(32, ARRSIZE),
               *c = aligned_alloc(32, ARRSIZE);

 fread(c, 1, 122, bmp);
 rewind(bmp); 
 fread(a, 1, ARRSIZE, bmp);
 fread(b, 1, ARRSIZE, bmpp);

 __m256i mm_a, mm_b;
 __m256d mm_two   = _mm256_set1_pd(2),
         mm_eight = _mm256_set1_pd(8);

 __m256d mm_c, mm_d,
         mm_ten = _mm256_set1_pd(10.0);

 int i = 122;
 for(; i < ARRSIZE; i+=32){
 // c[i] = ((a[i] * 0.8) + (b[i] * 0.2)); 
  mm_a = _mm256_loadu_si256((__m256i *)&(a[i]));
  mm_b = _mm256_loadu_si256((__m256i *)&(b[i]));

  mm_c = _mm256_div_pd((__m256d)mm_a, mm_ten);
  mm_d = _mm256_div_pd((__m256d)mm_b, mm_ten);

  mm_a = (__m256i)_mm256_floor_pd(_mm256_mul_pd(mm_c, mm_eight));
  mm_b = (__m256i)_mm256_floor_pd(_mm256_mul_pd(mm_d, mm_two));

  mm_a = _mm256_add_epi8(mm_a, mm_b);

  _mm256_storeu_si256((__m256i *)&(c[i]), mm_a);
 }

 fwrite(c, 1, ARRSIZE, write);

 fclose(bmp);
 fclose(bmpp);
 fclose(write);

 free(a);
 free(b);
 free(c);
}

标签: cbitmapintrinsicsavxavx2

解决方案


您所拥有的代码的一个问题是向量类型之间的转换不是保值转换,而是重新解释。所以(__m256d)mm_a实际上意味着“取这 32 个字节并将它们解释为 4 个双精度数”。可以,但是如果数据是 RGB888 打包的,那么将其重新解释为双精度是不好的。

可以使用适当的转换,但为此使用浮点算术(尤其是双精度)是多余的。使用较小的类型会使它们中的更多适合向量,因此通常更快,因为可以使用指令处理更多项目。

此外,不应将 122 字节标头放入对齐的数组中,它的存在会立即使实际像素数据的位置不对齐。它可以单独写入输出文件。

例如,一种策略是扩大到 16 位,使用_mm256_mulhi_epu16大约 80% 和大约 20% 进行缩放,将它们与 相加_mm256_add_epi16,然后再次缩小到 8 位。解包到 16 位然后打包回 8 位与 256 位向量的工作有点奇怪,可以将其视为 2 倍的 128 位操作并排。为防止过早截断,可以通过自由左移 8 位来解压缩 8 位源数据,将数据字节放在相应字的高字节中。这样,乘高将创建 16 位中间结果,而不是立即将它们截断为 8 位,这样我们可以在进行更合适的加法之后进行舍入(这确实需要额外的移位,并且可以选择加法)。例如像这样(未测试):

const uint16_t scale_a = uint16_t(0x10000 * 0.8);
const uint16_t scale_b = uint16_t(0x10000 - scale_a);
__m256i roundoffset = _mm256_set1_epi16(0x80);
__m256i zero = _mm256_setzero_si256();
for(int i = 0; i < ARRSIZE; i += 32) {
    // c[i] = ((a[i] * 0.8) + (b[i] * 0.2));
    // c[i] = ((a[i] << 8) * scale_a) + ((b[i] << 8) * scale_b) >> 7;
    __m256i raw_a = _mm256_loadu_si256((__m256i *)&(a[i]));
    __m256i raw_b = _mm256_loadu_si256((__m256i *)&(b[i]));
    __m256i data_al = _mm256_unpacklo_epi8(zero, raw_a);
    __m256i data_bl = _mm256_unpacklo_epi8(zero, raw_b);
    __m256i data_ah = _mm256_unpackhi_epi8(zero, raw_a);
    __m256i data_bh = _mm256_unpackhi_epi8(zero, raw_b);
    __m256i scaled_al = _mm256_mulhi_epu16(data_al, _mm256_set1_epi16(scale_a));
    __m256i scaled_bl = _mm256_mulhi_epu16(data_bl, _mm256_set1_epi16(scale_b));
    __m256i scaled_ah = _mm256_mulhi_epu16(data_ah, _mm256_set1_epi16(scale_a));
    __m256i scaled_bh = _mm256_mulhi_epu16(data_bh, _mm256_set1_epi16(scale_b));
    __m256i suml = _mm256_add_epi16(scaled_al, scaled_bl);
    __m256i sumh = _mm256_add_epi16(scaled_ah, scaled_bh);
    __m256i roundedl = _mm256_srli_epi16(_mm256_add_epi16(suml, roundoffset), 8);
    __m256i roundedh = _mm256_srli_epi16(_mm256_add_epi16(sumh, roundoffset), 8);
    __m256i packed = _mm256_packus_epi16(roundedl, roundedh);
    _mm256_storeu_si256((__m256i *)&(c[i]), packed);
}

其中有相当多的 shuffle 操作,将吞吐量限制为每 5 个周期进行一次迭代(在没有其他限制器的情况下),每个周期大约 1 个像素(作为输出)。

可以使用不同的策略,使用_mm256_maddubs_epi16混合因子的较低精度近似值。它将第二个操作数视为有符号字节并进行有符号饱和,因此这次只有 7 位近似值适合。由于它对 8 位数据进行操作,因此解包较少,但仍有一些解包,因为它需要对来自两个图像的数据进行交错。也许像这样(也未测试):

const uint8_t scale_a = uint8_t(0x80 * 0.8);
const uint8_t scale_b = uint8_t(0x80 - scale_a);
__m256i scale = _mm256_set1_epi16((scale_b << 8) | scale_a);
__m256i roundoffset = _mm256_set1_epi16(0x80);
//__m256i scale = _mm256_set1_epi16();
for(int i = 0; i < ARRSIZE; i += 32) {
    // c[i] = ((a[i] * 0.8) + (b[i] * 0.2));
    // c[i] = (a[i] * scale_a) + (b[i] * scale_b) >> 7;
    __m256i raw_a = _mm256_loadu_si256((__m256i *)&(a[i]));
    __m256i raw_b = _mm256_loadu_si256((__m256i *)&(b[i]));
    __m256i data_l = _mm256_unpacklo_epi8(raw_a, raw_b);
    __m256i data_h = _mm256_unpackhi_epi8(raw_a, raw_b);
    __m256i blended_l = _mm256_maddubs_epi16(data_l, scale);
    __m256i blended_h = _mm256_maddubs_epi16(data_h, scale);
    __m256i roundedl = _mm256_srli_epi16(_mm256_add_epi16(blended_l, roundoffset), 7);
    __m256i roundedh = _mm256_srli_epi16(_mm256_add_epi16(blended_h, roundoffset), 7);
    __m256i packed = _mm256_packus_epi16(roundedl, roundedh);
    _mm256_storeu_si256((__m256i *)&(c[i]), packed);
}

只有 3 次 shuffle,吞吐量可能达到每 3 个周期 1 次迭代,即每个周期几乎 1.8 个像素。

希望有更好的方法来做到这一点。这些方法都没有接近乘法的最大值,这似乎应该是目标。我不知道怎么去那里。


另一种策略是使用几轮平均来接近所需的比率,但接近并不是那么接近。也许是这样的(未经测试):

for(int i = 0; i < ARRSIZE; i += 32) {
    // c[i] = round_somehow((a[i] * 0.8125) + (b[i] * 0.1875));
    __m256i raw_a = _mm256_loadu_si256((__m256i *)&(a[i]));
    __m256i raw_b = _mm256_loadu_si256((__m256i *)&(b[i]));
    __m256i mixed_8_8 = _mm256_avg_epu8(raw_a, raw_b);
    __m256i mixed_12_4 = _mm256_avg_epu8(raw_a, mixed_8_8);
    __m256i mixed_14_2 = _mm256_avg_epu8(raw_a, mixed_12_4);
    __m256i mixed_13_3 = _mm256_avg_epu8(mixed_12_4, mixed_14_2);
    _mm256_storeu_si256((__m256i *)&(c[i]), mixed_13_3);
}

但是_mm256_avg_epu8四舍五入,也许堆叠这么多次是不好的。没有“平均向下舍入”指令,但是avg_down(a, b) == ~avg_up(~a, ~b). 这不会导致互补的巨大混乱,因为它们中的大多数会相互抵消。如果仍有四舍五入,则将其留给最后一次操作是有意义的。不过,总是向下舍入可以节省 XOR。也许是这样的(未经测试)

__m256i ones = _mm256_set1_epi8(-1);
for(int i = 0; i < ARRSIZE; i += 32) {
    // c[i] = round_somehow((a[i] * 0.8125) + (b[i] * 0.1875));
    __m256i raw_a = _mm256_loadu_si256((__m256i *)&(a[i]));
    __m256i raw_b = _mm256_loadu_si256((__m256i *)&(b[i]));
    __m256i inv_a = _mm256_xor_si256(ones, raw_a);
    __m256i inv_b = _mm256_xor_si256(ones, raw_b);
    __m256i mixed_8_8 = _mm256_avg_epu8(inv_a, inv_b);
    __m256i mixed_12_4 = _mm256_avg_epu8(inv_a, mixed_8_8);
    __m256i mixed_14_2 = _mm256_avg_epu8(inv_a, mixed_12_4);
    __m256i mixed_13_3 = _mm256_avg_epu8(_mm256_xor_si256(mixed_12_4, ones), 
                                         _mm256_xor_si256(mixed_14_2, ones));
    _mm256_storeu_si256((__m256i *)&(c[i]), mixed_13_3);
}

推荐阅读