c - 如何将两个位图与 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);
}
解决方案
您所拥有的代码的一个问题是向量类型之间的转换不是保值转换,而是重新解释。所以(__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);
}
推荐阅读
- junit - 如何在 Junit 4 中的 @before 方法中知道当前的 @Test 方法
- mysql - 如何使用sql查询删除相同的表字段
- shell - 在 ssh 上使用交互式 shell 似乎可以删除/阻止某些控制字符 - 粗体、下划线或反转文本是错误的
- python - 如何遍历考拉 groupby 的元素?
- c - 变量的内存地址与 gdb 的“信息帧”输出不匹配
- python - 如何更新我的模型 acc 和 val_acc?
- c++ - 如何在 C++ 中将 OpenCV 2D 矩阵转换为 1D 数组?
- python - 当出现 FileNotFoundError 异常时,我应该使用 file.close() 关闭文件吗?
- python - discord.py 问题:如何在不和谐中检查第一个加入语音聊天的用户
- java - Android-Java无法写入firebase实时数据库