【问题标题】:Blit faster than conditional + pointer increment?Blit 比条件 + 指针增量快吗?
【发布时间】:2018-10-07 12:14:05
【问题描述】:

这是我的简单 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 似乎可以提供 very 次要的加速(~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 相同)提供了

memcpy 快了约 25%。我希望尽可能接近原始memcpy 的速度,同时保持颜色0 透明。

问题是,据我所知,没有向量指令支持条件,但我需要保留dest,其中src0。有没有什么 [fast] 可以像 OR 一样但在字节级别上运行?

我之前读过有一个扩展或其他东西告诉 CPU 不要缓存一些数据,但我再也找不到它了。我的想法是不直接从src 读取,只从它写入dest,并确保它没有被缓存。然后只需从位掩码中读取以检查透明度。 我只是不知道如何真正做到这一点。这甚至可能吗?更不用说快速了?我也不知道,所以我问这个问题。

我更喜欢仅使用 C 来提高速度的技巧,也许是一些 gcc 扩展,但如果 x86 汇编是唯一的方法,那就这样吧。帮助我了解我的实际瓶颈(因为我对我的结果感到困惑)也会有所帮助。

【问题讨论】:

  • 为什么不使用 memcpy?它是超级优化的,如果在您的架构上可用,它会被一些低级组装操作所取代。
  • @sturcotte06 此代码不会用零覆盖,因此与 memcpy 不同。
  • @sturcotte06 OP 已经提到了memcpy
  • 那么我不认为你可以匹配 memcpy 的性能,因为你不能利用 x86 指令。
  • 与遮罩非常兼容。例如,请参阅似乎适合此处的 pblendvb 指令。

标签: c performance optimization memcpy blit


【解决方案1】:

您没有提到是否使用 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 代码;)

【讨论】:

  • 为了让编译器满意而进行如此多的模糊处理,使用内部函数可能会更好(不那么脆弱,更容易维护)。
  • 如果他使用 16 个字节而不是 8 个字节,那么只需将 if() 替换为 ? :.当手动使用内在函数时,这一步也必须完成(8-> 16),你可以看到这个转换很便宜。不 - 内在函数不容易维护,更不用说代码不再可移植了。 Autovectorizer 运行良好 - 只需遵循文档中列出的限制即可。
  • 您的意思是,对于具有一组特定参数的特定架构上的特定编译器的当前版本,当且仅当您使用一组特定的 args 在一个特定架构上修改代码以适应一个特定编译器的一个版本?
  • 如果您将最流行的编译器称为“特定”、“当前”任何几年前的版本、“修改”和“特定参数”遵循自动矢量化规则,那么我没有更多问题了。
【解决方案2】:

如果您的编译器/架构支持 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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-12-08
    • 1970-01-01
    • 2021-10-28
    • 1970-01-01
    • 1970-01-01
    • 2011-09-06
    • 2012-06-11
    • 2015-06-08
    相关资源
    最近更新 更多