Blit 比条件 + 指针增量更快?
Blit faster than conditional + pointer increment?
这是我的简单 blitting 函数:
static void blit8(unsigned char* dest, unsigned char* src)
{
byte i;
for (i = 0; i < 8; ++i) {
if (*src != 0) {
*dest = *src;
}
++dest;
++src;
}
}
我已经在 -O3
,blit8
正在内联。 restrict
(gcc) 在这里没有作用。也不会以任何不同的方式增加指针,或使用另一个数字作为透明度,或 i
的另一种类型......我什至尝试传递一个 1 字节的位掩码并检查它而不是取消引用 src
。将 i
的限制增加到,比如说,16 似乎提供了 非常 较小的加速(~4-6%),但我正在使用 8 字节,而不是16 字节块。
我的瓶颈?实际上没有任何线索,我不认为这是缓存行,因为我的未命中率很低(?)并且 64
(我的缓存行大小)在改变周围时没有特殊意义。但我也不认为它是内存速度(因为 memcpy
更快,稍后会详细介绍)。
cg_annotate
关于 blit8
的说法(没有内联):
Ir I1mr ILmr Dr D1mr DLmr Dw D1mw DLmw file:function
3,747,585,536 62 1 1,252,173,824 2,097,653 0 674,067,968 0 0 ppu.c:blit8.constprop.0
常规 cachegrind
输出(带内联):
I refs: 6,446,979,546
I1 misses: 184,752
LLi misses: 22,549
I1 miss rate: 0.00%
LLi miss rate: 0.00%
D refs: 2,150,502,425 (1,497,875,135 rd + 652,627,290 wr)
D1 misses: 17,121,968 ( 2,761,307 rd + 14,360,661 wr)
LLd misses: 253,685 ( 70,802 rd + 182,883 wr)
D1 miss rate: 0.8% ( 0.2% + 2.2% )
LLd miss rate: 0.0% ( 0.0% + 0.0% )
LL refs: 17,306,720 ( 2,946,059 rd + 14,360,661 wr)
LL misses: 276,234 ( 93,351 rd + 182,883 wr)
LL miss rate: 0.0% ( 0.0% + 0.0% )
0.8%
D1 未命中率?对我来说听起来很低。
对我来说最有趣的事情是,删除 0
-check(在功能上与 memcpy
相同)提供了 <1% 的加速,即使:
memcpy
快约 25%。我想要尽可能接近原始 memcpy
的速度,同时将颜色 0
保持为透明。
问题是,据我所知,没有矢量指令支持条件,但我需要保留 dest
,其中 src
是 0
。有什么[快]可以像OR
但在字节级别上的东西吗?
我之前读过有扩展程序或其他内容告诉 CPU 不要缓存某些数据,但我找不到了。我的想法是不直接从 src
读取,只从它写入 dest
,并确保它不被缓存。然后只需从位掩码中读取以检查透明度。 我只是不知道该怎么做。这有可能吗,更不用说快速了?我也不知道,所以才问这个问题。
我更喜欢有关如何仅用 C 提高速度的提示,也许是一些 gcc 扩展,但如果 x86 汇编是唯一的方法,那就这样吧。帮助我了解我的 实际 瓶颈(因为我对我的结果感到困惑)也会有所帮助。
您没有提到是否使用 GCC,但我们假设是。
如果涉及循环内的条件,GCC 会很挑剔 - 这就是为什么您的示例无法矢量化的原因。
所以这段代码:
void blit8(unsigned char* dest, unsigned char* src)
{
char i;
for (i = 0; i < 8; ++i) {
if (*src != 0) {
*dest = *src;
}
++dest;
++src;
}
}
结果为:
blit8:
movzx eax, BYTE PTR [rsi]
test al, al
je .L5
mov BYTE PTR [rdi], al
.L5:
movzx eax, BYTE PTR [rsi+1]
test al, al
je .L6
mov BYTE PTR [rdi+1], al
.L6:
movzx eax, BYTE PTR [rsi+2]
test al, al
je .L7
mov BYTE PTR [rdi+2], al
.L7:
movzx eax, BYTE PTR [rsi+3]
test al, al
je .L8
mov BYTE PTR [rdi+3], al
.L8:
movzx eax, BYTE PTR [rsi+4]
test al, al
je .L9
mov BYTE PTR [rdi+4], al
.L9:
movzx eax, BYTE PTR [rsi+5]
test al, al
je .L10
mov BYTE PTR [rdi+5], al
.L10:
movzx eax, BYTE PTR [rsi+6]
test al, al
je .L11
mov BYTE PTR [rdi+6], al
.L11:
movzx eax, BYTE PTR [rsi+7]
test al, al
je .L37
mov BYTE PTR [rdi+7], al
.L37:
ret
它由编译器展开,但它仍然适用于单个字节。
但是在这种情况下,有一个技巧经常奏效——使用三元运算符代替 if(cond)。这将解决一个问题。
但是还有另一个 - GCC 拒绝向量化短的小块 - 在这个例子中是 8 个字节。因此,让我们使用另一个技巧 - 在更大的块上进行计算但忽略其中的一部分。
这是我的例子:
void blit8(unsigned char* dest, unsigned char* src)
{
int i;
unsigned char temp_dest[16];
unsigned char temp_src[16];
for (i = 0; i < 8; ++i) temp_dest[i] = dest[i];
for (i = 0; i < 8; ++i) temp_src[i] = src[i];
for (i = 0; i < 16; ++i)
{
temp_dest[i] = (temp_src[i] != 0) ? temp_src[i] : temp_dest[i];
}
for (i = 0; i < 8; ++i) dest[i] = temp_dest[i];
}
和对应的程序集:
blit8:
mov rax, QWORD PTR [rdi]
vpxor xmm0, xmm0, xmm0
mov QWORD PTR [rsp-40], rax
mov rax, QWORD PTR [rsi]
mov QWORD PTR [rsp-24], rax
vmovdqa xmm1, XMMWORD PTR [rsp-24]
vpcmpeqb xmm0, xmm0, XMMWORD PTR [rsp-24]
vpblendvb xmm0, xmm1, XMMWORD PTR [rsp-40], xmm0
vmovq QWORD PTR [rdi], xmm0
ret
注意:
我没有对它进行基准测试 - 它只是证明可以通过使用适当的编码规则和技巧来生成 SIMD 代码 ;)
如果您的 compiler/architecture 支持 vector extensions(如 clang 和 gcc),您可以使用类似的东西:
//This may compile to awful code on x86_64 b/c mmx is slow (its fine on arm64)
void blit8(void* dest, void* src){
typedef __UINT8_TYPE__ u8x8 __attribute__ ((__vector_size__ (8), __may_alias__));
u8x8 *dp = dest, d = *dp, *sp = src, s = *sp, cmp;
cmp = s == (u8x8){0};
d &= cmp;
*dp = s|d;
}
//This may compile to better code on x86_64 - worse on arm64
void blit8v(void* dest, void* src){
typedef __UINT8_TYPE__ u8x16 __attribute__ ((__vector_size__ (16), __may_alias__));
typedef __UINT64_TYPE__ u64, u64x2 __attribute__ ((__vector_size__ (16), __may_alias__));
u8x16 *dp = dest, d = *dp, *sp = src, s = *sp, cmp;
cmp = s == (u8x16){0};
d &= cmp;
d |= s;
*(u64*)dest = ((u64x2)d)[0];
}
//This one is fine on both arm and x86, but 16 bytes vs. 8
void blit16(void* dest, void* src){
typedef __UINT8_TYPE__ u8x16 __attribute__ ((__vector_size__ (16), __may_alias__));
u8x16 *dp = dest, *sp = src, d = *dp, s = *sp, cmp;
cmp = s == (u8x16){0};
*dp = s|(d & cmp);
}
在 arm 上编译为:
blit8:
ldr d1, [x1]
ldr d2, [x0]
cmeq v0.8b, v1.8b, #0
and v0.8b, v0.8b, v2.8b
orr v0.8b, v0.8b, v1.8b
str d0, [x0]
ret
blit16:
ldr q1, [x1]
ldr q2, [x0]
cmeq v0.16b, v1.16b, #0
and v0.16b, v0.16b, v2.16b
orr v0.16b, v0.16b, v1.16b
str q0, [x0]
ret
在 x86_64 上:
blit8v: # @blit8v
movdqa xmm0, xmmword ptr [rsi]
pxor xmm1, xmm1
pcmpeqb xmm1, xmm0
pand xmm1, xmmword ptr [rdi]
por xmm1, xmm0
movq qword ptr [rdi], xmm1
ret
blit16: # @blit16
movdqa xmm0, xmmword ptr [rsi]
pxor xmm1, xmm1
pcmpeqb xmm1, xmm0
pand xmm1, xmmword ptr [rdi]
por xmm1, xmm0
movdqa xmmword ptr [rdi], xmm1
ret
这是我的简单 blitting 函数:
static void blit8(unsigned char* dest, unsigned char* src)
{
byte i;
for (i = 0; i < 8; ++i) {
if (*src != 0) {
*dest = *src;
}
++dest;
++src;
}
}
我已经在 -O3
,blit8
正在内联。 restrict
(gcc) 在这里没有作用。也不会以任何不同的方式增加指针,或使用另一个数字作为透明度,或 i
的另一种类型......我什至尝试传递一个 1 字节的位掩码并检查它而不是取消引用 src
。将 i
的限制增加到,比如说,16 似乎提供了 非常 较小的加速(~4-6%),但我正在使用 8 字节,而不是16 字节块。
我的瓶颈?实际上没有任何线索,我不认为这是缓存行,因为我的未命中率很低(?)并且 64
(我的缓存行大小)在改变周围时没有特殊意义。但我也不认为它是内存速度(因为 memcpy
更快,稍后会详细介绍)。
cg_annotate
关于 blit8
的说法(没有内联):
Ir I1mr ILmr Dr D1mr DLmr Dw D1mw DLmw file:function
3,747,585,536 62 1 1,252,173,824 2,097,653 0 674,067,968 0 0 ppu.c:blit8.constprop.0
常规 cachegrind
输出(带内联):
I refs: 6,446,979,546
I1 misses: 184,752
LLi misses: 22,549
I1 miss rate: 0.00%
LLi miss rate: 0.00%
D refs: 2,150,502,425 (1,497,875,135 rd + 652,627,290 wr)
D1 misses: 17,121,968 ( 2,761,307 rd + 14,360,661 wr)
LLd misses: 253,685 ( 70,802 rd + 182,883 wr)
D1 miss rate: 0.8% ( 0.2% + 2.2% )
LLd miss rate: 0.0% ( 0.0% + 0.0% )
LL refs: 17,306,720 ( 2,946,059 rd + 14,360,661 wr)
LL misses: 276,234 ( 93,351 rd + 182,883 wr)
LL miss rate: 0.0% ( 0.0% + 0.0% )
0.8%
D1 未命中率?对我来说听起来很低。
对我来说最有趣的事情是,删除 0
-check(在功能上与 memcpy
相同)提供了 <1% 的加速,即使:
memcpy
快约 25%。我想要尽可能接近原始 memcpy
的速度,同时将颜色 0
保持为透明。
问题是,据我所知,没有矢量指令支持条件,但我需要保留 dest
,其中 src
是 0
。有什么[快]可以像OR
但在字节级别上的东西吗?
我之前读过有扩展程序或其他内容告诉 CPU 不要缓存某些数据,但我找不到了。我的想法是不直接从 src
读取,只从它写入 dest
,并确保它不被缓存。然后只需从位掩码中读取以检查透明度。 我只是不知道该怎么做。这有可能吗,更不用说快速了?我也不知道,所以才问这个问题。
我更喜欢有关如何仅用 C 提高速度的提示,也许是一些 gcc 扩展,但如果 x86 汇编是唯一的方法,那就这样吧。帮助我了解我的 实际 瓶颈(因为我对我的结果感到困惑)也会有所帮助。
您没有提到是否使用 GCC,但我们假设是。 如果涉及循环内的条件,GCC 会很挑剔 - 这就是为什么您的示例无法矢量化的原因。
所以这段代码:
void blit8(unsigned char* dest, unsigned char* src)
{
char i;
for (i = 0; i < 8; ++i) {
if (*src != 0) {
*dest = *src;
}
++dest;
++src;
}
}
结果为:
blit8:
movzx eax, BYTE PTR [rsi]
test al, al
je .L5
mov BYTE PTR [rdi], al
.L5:
movzx eax, BYTE PTR [rsi+1]
test al, al
je .L6
mov BYTE PTR [rdi+1], al
.L6:
movzx eax, BYTE PTR [rsi+2]
test al, al
je .L7
mov BYTE PTR [rdi+2], al
.L7:
movzx eax, BYTE PTR [rsi+3]
test al, al
je .L8
mov BYTE PTR [rdi+3], al
.L8:
movzx eax, BYTE PTR [rsi+4]
test al, al
je .L9
mov BYTE PTR [rdi+4], al
.L9:
movzx eax, BYTE PTR [rsi+5]
test al, al
je .L10
mov BYTE PTR [rdi+5], al
.L10:
movzx eax, BYTE PTR [rsi+6]
test al, al
je .L11
mov BYTE PTR [rdi+6], al
.L11:
movzx eax, BYTE PTR [rsi+7]
test al, al
je .L37
mov BYTE PTR [rdi+7], al
.L37:
ret
它由编译器展开,但它仍然适用于单个字节。
但是在这种情况下,有一个技巧经常奏效——使用三元运算符代替 if(cond)。这将解决一个问题。 但是还有另一个 - GCC 拒绝向量化短的小块 - 在这个例子中是 8 个字节。因此,让我们使用另一个技巧 - 在更大的块上进行计算但忽略其中的一部分。
这是我的例子:
void blit8(unsigned char* dest, unsigned char* src)
{
int i;
unsigned char temp_dest[16];
unsigned char temp_src[16];
for (i = 0; i < 8; ++i) temp_dest[i] = dest[i];
for (i = 0; i < 8; ++i) temp_src[i] = src[i];
for (i = 0; i < 16; ++i)
{
temp_dest[i] = (temp_src[i] != 0) ? temp_src[i] : temp_dest[i];
}
for (i = 0; i < 8; ++i) dest[i] = temp_dest[i];
}
和对应的程序集:
blit8:
mov rax, QWORD PTR [rdi]
vpxor xmm0, xmm0, xmm0
mov QWORD PTR [rsp-40], rax
mov rax, QWORD PTR [rsi]
mov QWORD PTR [rsp-24], rax
vmovdqa xmm1, XMMWORD PTR [rsp-24]
vpcmpeqb xmm0, xmm0, XMMWORD PTR [rsp-24]
vpblendvb xmm0, xmm1, XMMWORD PTR [rsp-40], xmm0
vmovq QWORD PTR [rdi], xmm0
ret
注意: 我没有对它进行基准测试 - 它只是证明可以通过使用适当的编码规则和技巧来生成 SIMD 代码 ;)
如果您的 compiler/architecture 支持 vector extensions(如 clang 和 gcc),您可以使用类似的东西:
//This may compile to awful code on x86_64 b/c mmx is slow (its fine on arm64)
void blit8(void* dest, void* src){
typedef __UINT8_TYPE__ u8x8 __attribute__ ((__vector_size__ (8), __may_alias__));
u8x8 *dp = dest, d = *dp, *sp = src, s = *sp, cmp;
cmp = s == (u8x8){0};
d &= cmp;
*dp = s|d;
}
//This may compile to better code on x86_64 - worse on arm64
void blit8v(void* dest, void* src){
typedef __UINT8_TYPE__ u8x16 __attribute__ ((__vector_size__ (16), __may_alias__));
typedef __UINT64_TYPE__ u64, u64x2 __attribute__ ((__vector_size__ (16), __may_alias__));
u8x16 *dp = dest, d = *dp, *sp = src, s = *sp, cmp;
cmp = s == (u8x16){0};
d &= cmp;
d |= s;
*(u64*)dest = ((u64x2)d)[0];
}
//This one is fine on both arm and x86, but 16 bytes vs. 8
void blit16(void* dest, void* src){
typedef __UINT8_TYPE__ u8x16 __attribute__ ((__vector_size__ (16), __may_alias__));
u8x16 *dp = dest, *sp = src, d = *dp, s = *sp, cmp;
cmp = s == (u8x16){0};
*dp = s|(d & cmp);
}
在 arm 上编译为:
blit8:
ldr d1, [x1]
ldr d2, [x0]
cmeq v0.8b, v1.8b, #0
and v0.8b, v0.8b, v2.8b
orr v0.8b, v0.8b, v1.8b
str d0, [x0]
ret
blit16:
ldr q1, [x1]
ldr q2, [x0]
cmeq v0.16b, v1.16b, #0
and v0.16b, v0.16b, v2.16b
orr v0.16b, v0.16b, v1.16b
str q0, [x0]
ret
在 x86_64 上:
blit8v: # @blit8v
movdqa xmm0, xmmword ptr [rsi]
pxor xmm1, xmm1
pcmpeqb xmm1, xmm0
pand xmm1, xmmword ptr [rdi]
por xmm1, xmm0
movq qword ptr [rdi], xmm1
ret
blit16: # @blit16
movdqa xmm0, xmmword ptr [rsi]
pxor xmm1, xmm1
pcmpeqb xmm1, xmm0
pand xmm1, xmmword ptr [rdi]
por xmm1, xmm0
movdqa xmmword ptr [rdi], xmm1
ret