结合 Poseur 和 Jitamaro 的答案,如果您假设输入和输出均为 16 字节对齐,并且每次处理 4 个像素,您可以使用混洗、掩码、与运算和或运算的组合来使用对齐存储器存储。主要思想是生成四个中间数据集,然后使用掩码将它们与相关像素值选择进行或运算,并写出 3 个 16 字节的像素数据集。请注意,我没有编译或尝试运行此代码。
编辑2:有关基础代码结构的更多细节:
使用 SSE2,通过对 16 字节进行 16 字节对齐读取和写入,可以获得更好的性能。由于每 16 个像素才能使您的 3 字节像素对齐到 16 字节,因此我们使用混洗和掩码以及每次处理 16 个输入像素的或运算来一次批量处理 16 个像素。
从最低有效位到最高有效位,忽略特定组件,输入看起来像这样:
s[0]: 0000 0000 0000 0000
s[1]: 1111 1111 1111 1111
s[2]: 2222 2222 2222 2222
s[3]: 3333 3333 3333 3333
输出结果如下:
d[0]: 000 000 000 000 111 1
d[1]: 11 111 111 222 222 22
d[2]: 2 222 333 333 333 333
为了生成这些输出,你需要按照以下步骤进行操作(我稍后会详细说明实际的转换方法):
d[0]= combine_0(f_0_low(s[0]), f_0_high(s[1]))
d[1]= combine_1(f_1_low(s[1]), f_1_high(s[2]))
d[2]= combine_2(f_1_low(s[2]), f_1_high(s[3]))
现在,combine_<x>
应该长什么样呢?如果我们假设 d
只是将 s
压缩在一起,我们可以通过应用掩码和或运算符来连接两个 s
:
combine_x(left, right)= (left & mask(x)) | (right & ~mask(x))
其中,1表示选择左侧像素,0表示选择右侧像素:
mask(0)= 111 111 111 111 000 0
mask(1)= 11 111 111 000 000 00
mask(2)= 1 111 000 000 000 000
但实际的变换(f_<x>_low
, f_<x>_high
)并不那么简单。由于我们是从源像素中反转和移除字节,因此第一个目标的实际变换为:
d[0]=
s[0][0].Blue s[0][0].Green s[0][0].Red
s[0][1].Blue s[0][1].Green s[0][1].Red
s[0][2].Blue s[0][2].Green s[0][2].Red
s[0][3].Blue s[0][3].Green s[0][3].Red
s[1][0].Blue s[1][0].Green s[1][0].Red
s[1][1].Blue
如果你把上面的内容按照从源代码到目标代码的字节偏移量转换,你会得到:
d[0]=
&s[0]+3 &s[0]+2 &s[0]+1
&s[0]+7 &s[0]+6 &s[0]+5
&s[0]+11 &s[0]+10 &s[0]+9
&s[0]+15 &s[0]+14 &s[0]+13
&s[1]+3 &s[1]+2 &s[1]+1
&s[1]+7
(如果你看所有 s[0] 偏移量,它们只是一个骗子的倒置掩码。)
现在,我们可以生成一个掩码来映射每个源字节到目标字节(
X
表示我们不关心那个值):
f_0_low= 3 2 1 7 6 5 11 10 9 15 14 13 X X X X
f_0_high= X X X X X X X X X X X X 3 2 1 7
f_1_low= 6 5 11 10 9 15 14 13 X X X X X X X X
f_1_high= X X X X X X X X 3 2 1 7 6 5 11 10
f_2_low= 9 15 14 13 X X X X X X X X X X X X
f_2_high= X X X X 3 2 1 7 6 5 11 10 9 15 14 13
我们可以通过查看每个源像素使用的掩码来进一步优化此过程。如果您查看我们用于 s[1] 的洗牌掩码:
f_0_high= X X X X X X X X X X X X 3 2 1 7
f_1_low= 6 5 11 10 9 15 14 13 X X X X X X X X
由于两个洗牌掩码不重叠,我们可以将它们合并并简单地在combine_中屏蔽不相关的像素,这一步我们已经完成了!以下代码执行所有这些优化(还假定源地址和目标地址都是16字节对齐的)。此外,掩码以 MSB->LSB 的顺序在代码中输出,以防您对排序产生困惑。
编辑:将存储更改为_mm_stream_si128
,因为您可能会进行大量写操作,而我们不希望必须刷新缓存。此外,它应该是对齐的,所以您可以获得免费的性能提升!
#include <assert.h>
#include <inttypes.h>
#include <tmmintrin.h>
void convert(uint8_t *orig, size_t imagesize, uint8_t *dest) {
assert((uintptr_t)orig % 16 == 0);
assert(imagesize % 16 == 0);
__m128i shuf0 = _mm_set_epi8(
-128, -128, -128, -128,
13, 14, 15, 9, 10, 11, 5, 6, 7, 1, 2, 3);
__m128i shuf1 = _mm_set_epi8(
7, 1, 2, 3,
-128, -128, -128, -128,
13, 14, 15, 9, 10, 11, 5, 6);
__m128i shuf2 = _mm_set_epi8(
10, 11, 5, 6, 7, 1, 2, 3,
-128, -128, -128, -128,
13, 14, 15, 9);
__m128i shuf3 = _mm_set_epi8(
13, 14, 15, 9, 10, 11, 5, 6, 7, 1, 2, 3,
-128, -128, -128, -128);
__m128i mask0 = _mm_set_epi32(0, -1, -1, -1);
__m128i mask1 = _mm_set_epi32(0, 0, -1, -1);
__m128i mask2 = _mm_set_epi32(0, 0, 0, -1);
uint8_t *end = orig + imagesize * 4;
for (; orig != end; orig += 64, dest += 48) {
__m128i a= _mm_shuffle_epi8(_mm_load_si128((__m128i *)orig), shuf0);
__m128i b= _mm_shuffle_epi8(_mm_load_si128((__m128i *)orig + 1), shuf1);
__m128i c= _mm_shuffle_epi8(_mm_load_si128((__m128i *)orig + 2), shuf2);
__m128i d= _mm_shuffle_epi8(_mm_load_si128((__m128i *)orig + 3), shuf3);
_mm_stream_si128((__m128i *)dest, _mm_or_si128(_mm_and_si128(a, mask0), _mm_andnot_si128(b, mask0));
_mm_stream_si128((__m128i *)dest + 1, _mm_or_si128(_mm_and_si128(b, mask1), _mm_andnot_si128(c, mask1));
_mm_stream_si128((__m128i *)dest + 2, _mm_or_si128(_mm_and_si128(c, mask2), _mm_andnot_si128(d, mask2));
}
}
for(x = 0; x < IMAGESIZE; x++) { dest[x].Red = orig[x].Red; } for(x = 0; x < IMAGESIZE; x++) { dest[x].Green = orig[x].Green; } for(x = 0; x < IMAGESIZE; x++) { dest[x].Blue = orig[x].Blue; }
的可能性。在这种情况下,简单的循环是否超过了位操作? - Mark Hurd