我对SIMD / SSE很新,我正在尝试做一些简单的图像过滤(模糊)。 下面的代码使用水平方向上的简单[1 2 1]加权来过滤8位灰度位图的每个像素。我一次创建16个像素的总和。
至少对我而言,这段代码看起来非常糟糕的是,其中有很多插入/提取,这不是很优雅,也可能减慢所有内容。在转移时是否有更好的方法将数据从一个reg包装到另一个?
buf是图像数据,16字节对齐。 w / h是宽度和高度,16的倍数。
__m128i *p = (__m128i *) buf;
__m128i cur1, cur2, sum1, sum2, zeros, tmp1, tmp2, saved;
zeros = _mm_setzero_si128();
short shifted, last = 0, next;
// preload first row
cur1 = _mm_load_si128(p);
for (x = 1; x < (w * h) / 16; x++) {
// unpack
sum1 = sum2 = saved = cur1;
sum1 = _mm_unpacklo_epi8(sum1, zeros);
sum2 = _mm_unpackhi_epi8(sum2, zeros);
cur1 = tmp1 = sum1;
cur2 = tmp2 = sum2;
// "middle" pixel
sum1 = _mm_add_epi16(sum1, sum1);
sum2 = _mm_add_epi16(sum2, sum2);
// left pixel
cur2 = _mm_slli_si128(cur2, 2);
shifted = _mm_extract_epi16(cur1, 7);
cur2 = _mm_insert_epi16(cur2, shifted, 0);
cur1 = _mm_slli_si128(cur1, 2);
cur1 = _mm_insert_epi16(cur1, last, 0);
sum1 = _mm_add_epi16(sum1, cur1);
sum2 = _mm_add_epi16(sum2, cur2);
// right pixel
tmp1 = _mm_srli_si128(tmp1, 2);
shifted = _mm_extract_epi16(tmp2, 0);
tmp1 = _mm_insert_epi16(tmp1, shifted, 7);
tmp2 = _mm_srli_si128(tmp2, 2);
// preload next row
cur1 = _mm_load_si128(p + x);
// we need the first pixel of the next row for the "right" pixel
next = _mm_extract_epi16(cur1, 0) & 0xff;
tmp2 = _mm_insert_epi16(tmp2, next, 7);
// and the last pixel of last row for the next "left" pixel
last = ((uint16_t) _mm_extract_epi16(saved, 7)) >> 8;
sum1 = _mm_add_epi16(sum1, tmp1);
sum2 = _mm_add_epi16(sum2, tmp2);
// divide
sum1 = _mm_srli_epi16(sum1, 2);
sum2 = _mm_srli_epi16(sum2, 2);
sum1 = _mm_packus_epi16(sum1, sum2);
mm_store_si128(p + x - 1, sum1);
}
答案 0 :(得分:2)
这种邻里操作一直是SSE的痛苦,直到SSE3.5(又名SSSE3)出现,而PALIGNR(_mm_alignr_epi8)被引入。
如果你需要向后兼容SSE2 / SSE3,你可以编写一个等效的宏或内联函数,它模拟SSE2 / SSE3的_mm_alignr_epi8,并在目标SSE3.5 / SSE4时下降到_mm_alignr_epi8。
另一种方法是使用未对齐的负载来获取移位的数据 - 这在较旧的CPU上相对昂贵(大约是延迟的两倍,对齐负载的吞吐量的一半),但这可能是可以接受的,这取决于您正在进行的大量计算每个负载。它还具有以下优点:在当前的Intel CPU(Core i7)上,未对齐的负载与对齐的负载相比没有任何损失,因此您的代码在Core i7上非常有效等。
答案 1 :(得分:2)
我建议保留SSE寄存器上的相邻像素。也就是说,将_mm_slli_si128 / _mm_srli_si128的结果保存在SSE变量中,并消除所有插入和提取。我的理由是,在较旧的CPU中,插入/提取指令需要SSE单元和通用单元之间的通信,这比将计算保持在SSE内要慢得多,即使它溢出到L1缓存。
完成后,应该只有四个16位移位(_mm_slli_si128,_mm_srli_si128,不计算divison shift )。我的建议是对你的代码进行基准测试,因为到那时你的代码可能已经达到了内存带宽限制......这意味着你不能再进行优化了。
如果图像很大(大于L2尺寸)且输出不会很快回读,请尝试使用MOVNTDQ(_mm_stream_si128)进行回写。根据几个网站,它在SSE2中,尽管您可能需要仔细检查。
SIMD教程:
一些SIMD大师网站: