Подтвердить что ты не робот

Могу ли я использовать GCC __builtin_expect() с тройным оператором в C

Руководство пользователя GCC показывает только примеры, где __builtin_expect() помещается вокруг всего условия оператора if.

Я также заметил, что GCC не жалуется, если я его использую, например, с тернарным оператором или любым произвольным интегральным выражением, даже если оно не используется в контексте ветвления.

Итак, интересно, каковы основные ограничения его использования на самом деле.

Сохраняет ли это эффект при использовании в тройной операции, например:

int foo(int i)
{
  return __builtin_expect(i == 7, 1) ? 100 : 200;
}

А как насчет этого случая:

int foo(int i)
{
  return __builtin_expect(i, 7) == 7 ? 100 : 200;
}

И этот:

int foo(int i)
{
  int j = __builtin_expect(i, 7);
  return j == 7 ? 100 : 200;
}
4b9b3361

Ответ 1

Он, по-видимому, работает как для тернарных, так и для регулярных операторов if.

Во-первых, рассмотрим следующие три примера кода, два из которых используют __builtin_expect в стилях обычного-if и trernary-if, а третий, который не использует его вообще.

builtin.c:

int main()
{
    char c = getchar();
    const char *printVal;
    if (__builtin_expect(c == 'c', 1))
    {
        printVal = "Took expected branch!\n";
    }
    else
    {
        printVal = "Boo!\n";
    }

    printf(printVal);
}

ternary.c:

int main()
{
    char c = getchar();
    const char *printVal = __builtin_expect(c == 'c', 1) 
        ? "Took expected branch!\n"
        : "Boo!\n";

    printf(printVal);
}

nobuiltin.c:

int main()
{
    char c = getchar();
    const char *printVal;
    if (c == 'c')
    {
        printVal = "Took expected branch!\n";
    }
    else
    {
        printVal = "Boo!\n";
    }

    printf(printVal);
}

При компиляции с -O3 все три результата приводятся к одной и той же сборке. Однако, когда значение -O отсутствует (в GCC 4.7.2), оба ternary.c и builtin.c имеют один и тот же список сборок (где это важно):

builtin.s:

    .file   "builtin.c"
    .section    .rodata
.LC0:
    .string "Took expected branch!\n"
.LC1:
    .string "Boo!\n"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    andl    $-16, %esp
    subl    $32, %esp
    call    getchar
    movb    %al, 27(%esp)
    cmpb    $99, 27(%esp)
    sete    %al
    movzbl  %al, %eax
    testl   %eax, %eax
    je  .L2
    movl    $.LC0, 28(%esp)
    jmp .L3
.L2:
    movl    $.LC1, 28(%esp)
.L3:
    movl    28(%esp), %eax
    movl    %eax, (%esp)
    call    printf
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Debian 4.7.2-4) 4.7.2"
    .section    .note.GNU-stack,"",@progbits

ternary.s:

    .file   "ternary.c"
    .section    .rodata
.LC0:
    .string "Took expected branch!\n"
.LC1:
    .string "Boo!\n"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    andl    $-16, %esp
    subl    $32, %esp
    call    getchar
    movb    %al, 31(%esp)
    cmpb    $99, 31(%esp)
    sete    %al
    movzbl  %al, %eax
    testl   %eax, %eax
    je  .L2
    movl    $.LC0, %eax
    jmp .L3
.L2:
    movl    $.LC1, %eax
.L3:
    movl    %eax, 24(%esp)
    movl    24(%esp), %eax
    movl    %eax, (%esp)
    call    printf
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Debian 4.7.2-4) 4.7.2"
    .section    .note.GNU-stack,"",@progbits

В то время как nobuiltin.c не делает:

    .file   "nobuiltin.c"
    .section    .rodata
.LC0:
    .string "Took expected branch!\n"
.LC1:
    .string "Boo!\n"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    andl    $-16, %esp
    subl    $32, %esp
    call    getchar
    movb    %al, 27(%esp)
    cmpb    $99, 27(%esp)
    jne .L2
    movl    $.LC0, 28(%esp)
    jmp .L3
.L2:
    movl    $.LC1, 28(%esp)
.L3:
    movl    28(%esp), %eax
    movl    %eax, (%esp)
    call    printf
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Debian 4.7.2-4) 4.7.2"
    .section    .note.GNU-stack,"",@progbits

Соответствующая часть:

diff

В основном, __builtin_expect вызывает дополнительный код (sete %al...), который должен быть выполнен до je .L2 на основе результата testl %eax, %eax, который процессор с большей вероятностью прогнозирует как 1 (наивное предположение, здесь) вместо того, чтобы основываться на прямом сравнении ввода char с 'c'. В то время как в случае nobuiltin.c такой код не существует, а je/jne непосредственно следует за сравнением с 'c' (cmp $99). Помните, что предсказание ветвлений в основном выполняется в CPU, и здесь GCC просто "кладет ловушку" для предсказателя ветвления процессора, чтобы предположить, какой путь будет выполнен (через дополнительный код и переключение je и jne, хотя у меня нет источника для этого, так как официальное руководство по оптимизации Intel не упоминает о лечении первых встречей с помощью je vs jne по-разному для предсказания ветки! Я могу только предположить, что команда GCC прибыла на это через пробную версию и ошибку).

Я уверен, что есть лучшие тестовые примеры, когда предсказание ветвей GCC можно увидеть более непосредственно (вместо того, чтобы наблюдать намеки на CPU), хотя я не знаю, как эмулировать такой случай лаконично/кратко. (Угадайте: скорее всего, он включит цикл во время компиляции.)