复合布尔值与 unlikely/likely 的性能

Performance for compound boolean vs. unlikely/likely

这样做更快吗?:

return m_bVisible && (_colorTransform.alphaTransform > 0 || _colorTransform.colorOffsetTransform.a > 0) && (!_masked || _maskVisitable);

比这样做?:

if (UNLIKELY(_colorTransform.alphaTransform == 0 && _colorTransform.colorOffsetTransform.a == 0))
{
    return false;
}

if (UNLIKELY(_masked && !_maskVisitable))
{
    return false;
}

return m_bVisible;

做了很多小优化,显着提高了我们游戏的帧率性能,这是我不确定的优化。我对获得 0.01% 的性能提升感到满意,因为在进行了 100 次这些优化之后,我已经能够相当显着地提高性能 (30-40%)。询问使用 UNLIKELY 优化与仅使用复合布尔值。在这个特定的表达式中短路不是很容易。

我会说第一个更快!

即使你是 "optimizing" 第二个分支预测,这基本上只是意味着 return m_bVisible; 在 ASM 代码中比你的 return false 更早,你仍然必须在您的 if 语句中最多进行四次比较。

但是,正如您所说,由于它们非常不可能,因此在实际查看 m_bVisible 之前没有必要这样做。第一个示例首先检查 m_bVisible,这似乎并不偏向 FALSE 或 TRUE。

我的预感是,对于 m_bVisible == false 的所有情况,第一个会更快。在所有其他情况下,除了第二个示例中 JMP 的最小开销(需要您跳过 return m_bVisible; 以到达实际的 return false)之外,不会有太大差异。

编辑

让我们更深入地了解两种变体的 x86_64 程序集 (gcc 8):

测试代码:

#include <stdio.h>

bool a=true; int b=0; int c=0; bool d=false; bool e=true;
#define likely(x)       __builtin_expect((x),1)
#define unlikely(x)     __builtin_expect((x),0)

int main()
{
marker1:
    return a && (b > 0 || c > 0) && (!d || e);
}


int alternative()
{
    if (unlikely(b == 0 && c == 0))
    {
        return false;
    }

    if (unlikely(d && !e))
    {
        return false;
    }

    return a;
}

X86_86 为两个函数组装 (GCC 8.2):

主要()

main:
    push    rbp
    mov     rbp, rsp
    movzx   eax, BYTE PTR a[rip]
    test    al, al
    je      .L3
    mov     eax, DWORD PTR b[rip]
    test    eax, eax
    jg      .L4
    mov     eax, DWORD PTR c[rip]
    test    eax, eax
    jle     .L3
.L4:
    movzx   eax, BYTE PTR d[rip]
    xor     eax, 1
    test    al, al
    jne     .L5
    movzx   eax, BYTE PTR e[rip]
    test    al, al
    je      .L3
.L5:
    mov     eax, 1
    jmp     .L6
.L3:
    mov     eax, 0
.L6:
    movzx   eax, al
    pop     rbp
    ret

备选方案()

alternative():
    push    rbp
    mov     rbp, rsp
    mov     eax, DWORD PTR b[rip]
    test    eax, eax
    sete    al
    movzx   eax, al
    test    rax, rax
    je      .L9
    mov     eax, DWORD PTR c[rip]
    test    eax, eax
    sete    al
    movzx   eax, al
    test    rax, rax
    je      .L9
    mov     eax, 0
    jmp     .L10
.L9:
    movzx   eax, BYTE PTR d[rip]
    movzx   eax, al
    test    rax, rax
    je      .L11
    movzx   eax, BYTE PTR e[rip]
    xor     eax, 1
    movzx   eax, al
    test    rax, rax
    je      .L11
    mov     eax, 0
    jmp     .L10
.L11:
    movzx   eax, BYTE PTR a[rip]
    movzx   eax, al
.L10:
    pop     rbp
    ret

在第一个变体中,在 main() 中,最快的退出是在第五行,je .L3。在第二个变体中,最快的退出发生得晚很多......在 .L9 at je .L11

编辑2

只是为了让它完整,这里是 -O3

的汇编
main:
        xor     eax, eax
        cmp     BYTE PTR a[rip], 0
        je      .L1
        cmp     DWORD PTR b[rip], 0
        jle     .L10
.L4:
        cmp     BYTE PTR d[rip], 0
        mov     eax, 1
        je      .L1
        movzx   eax, BYTE PTR e[rip]
.L1:
        ret
.L10:
        cmp     DWORD PTR c[rip], 0
        jg      .L4
        ret

alternative():
        mov     eax, DWORD PTR b[rip]
        or      eax, DWORD PTR c[rip]
        je      .L11
        cmp     BYTE PTR d[rip], 0
        jne     .L18
.L13:
        movzx   eax, BYTE PTR a[rip]
        ret
.L18:
        cmp     BYTE PTR e[rip], 0
        jne     .L13
        xor     eax, eax
.L11:
        ret

即使使用内联,第一个在 O3 上似乎仍然更快!