Какие оптимизации обеспечивает __builtin_unreachable? - PullRequest
0 голосов
/ 19 февраля 2019

Судя по документации gcc

Если поток управления достигает точки __builtin_unreachable, программа не определена.

Я думал, что можно использовать __builtin_unreachableкак подсказка оптимизатору во всех видах творческих подходов.Итак, я провел небольшой эксперимент

void stdswap(int& x, int& y)
{
    std::swap(x, y);
}

void brswap(int& x, int& y)
{
    if(&x == &y)
        __builtin_unreachable();
    x ^= y;
    y ^= x;
    x ^= y;
}

void rswap(int& __restrict x, int& __restrict y)
{
    x ^= y;
    y ^= x;
    x ^= y;
}

скомпилирован в (g ++ -O2)

stdswap(int&, int&):
        mov     eax, DWORD PTR [rdi]
        mov     edx, DWORD PTR [rsi]
        mov     DWORD PTR [rdi], edx
        mov     DWORD PTR [rsi], eax
        ret
brswap(int&, int&):
        mov     eax, DWORD PTR [rdi]
        xor     eax, DWORD PTR [rsi]
        mov     DWORD PTR [rdi], eax
        xor     eax, DWORD PTR [rsi]
        mov     DWORD PTR [rsi], eax
        xor     DWORD PTR [rdi], eax
        ret
rswap(int&, int&):
        mov     eax, DWORD PTR [rsi]
        mov     edx, DWORD PTR [rdi]
        mov     DWORD PTR [rdi], eax
        mov     DWORD PTR [rsi], edx
        ret

Я предполагаю, что stdswap и rswap оптимальныс точки зрения оптимизатора.Почему brswap не скомпилируется с одним и тем же?Могу ли я заставить его скомпилировать то же самое с __builtin_unreachable?

Ответы [ 2 ]

0 голосов
/ 19 февраля 2019

Цель __builtin_unreachable - помочь компилятору удалить мертвый код (который, как знает программист, никогда не будет выполнен) и линеаризовать код, сообщив компилятору, что путь «холодный».Рассмотрим следующее:

void exit_if_true(bool x);

int foo1(bool x)
{
    if (x) {
        exit_if_true(true);
        //__builtin_unreachable(); // we do not enable it here
    } else {
        std::puts("reachable");
    }

    return 0;
}
int foo2(bool x)
{
    if (x) {
        exit_if_true(true);
        __builtin_unreachable();  // now compiler knows exit_if_true
                                  // will not return as we are passing true to it
    } else {
        std::puts("reachable");
    }

    return 0;
}

Сгенерированный код:

foo1(bool):
        sub     rsp, 8
        test    dil, dil
        je      .L2              ; that jump is going to change
        mov     edi, 1
        call    exit_if_true(bool)
        xor     eax, eax         ; that tail is going to be removed
        add     rsp, 8
        ret
.L2:
        mov     edi, OFFSET FLAT:.LC0
        call    puts
        xor     eax, eax
        add     rsp, 8
        ret
foo2(bool):
        sub     rsp, 8
        test    dil, dil
        jne     .L9              ; changed jump
        mov     edi, OFFSET FLAT:.LC0
        call    puts
        xor     eax, eax
        add     rsp, 8
        ret
.L9:
        mov     edi, 1
        call    exit_if_true(bool)

Обратите внимание на различия:

  • xor eax, eax и ret были удалены, как теперь компиляторзнает, что это мертвый код.
  • Компилятор поменял местами порядок ветвлений: сначала идет ветвь с вызовом puts, так что условный переход может быть быстрее (прямые ветвления, которые не были приняты, быстрее как при предсказании, так и при прогнозировании).когда нет информации о предсказании).

Здесь предполагается, что ветвь, которая заканчивается вызовом функции noreturn или __builtin_unreachable, будет выполнена только один раз или приведет к вызову или исключению longjmpбросить оба из них редко, и не нужно расставлять приоритеты во время оптимизации.

Вы пытаетесь использовать его для другой цели - предоставляя компилятору информацию о псевдонимах (и вы можете попробовать сделать то же самое для выравнивания),К сожалению, GCC не поддерживает такие проверки адресов.

Как вы заметили, добавление __restrict__ помогает.Так что __restrict__ работает для псевдонимов, __builtin_unreachable - нет.

Посмотрите на следующий пример, в котором используется __builtin_assume_aligned:

void copy1(int *__restrict__ dst, const int *__restrict__ src)
{
    if (reinterpret_cast<uintptr_t>(dst) % 16 == 0) __builtin_unreachable();
    if (reinterpret_cast<uintptr_t>(src) % 16 == 0) __builtin_unreachable();

    dst[0] = src[0];
    dst[1] = src[1];
    dst[2] = src[2];
    dst[3] = src[3];
}

void copy2(int *__restrict__ dst, const int *__restrict__ src)
{
    dst = static_cast<int *>(__builtin_assume_aligned(dst, 16));
    src = static_cast<const int *>(__builtin_assume_aligned(src, 16));

    dst[0] = src[0];
    dst[1] = src[1];
    dst[2] = src[2];
    dst[3] = src[3];
}

Сгенерированный код:

copy1(int*, int const*):
        movdqu  xmm0, XMMWORD PTR [rsi]
        movups  XMMWORD PTR [rdi], xmm0
        ret
copy2(int*, int const*):
        movdqa  xmm0, XMMWORD PTR [rsi]
        movaps  XMMWORD PTR [rdi], xmm0
        ret

Можно предположить, что компилятор может понять, что dst % 16 == 0 означает, что указатель выровнен на 16 байтов, но это не так.Таким образом, используются невыровненные хранилища и нагрузки, а вторая версия генерирует более быстрые инструкции, требующие выравнивания адреса.

0 голосов
/ 19 февраля 2019

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

__buildin_unreachable, а также __builtin_expect делают то, что ожидали - в вашем случае удалите ненужные cmp и jnz из неиспользованногоесли оператор.

Компилятор должен сгенерировать машинный код, используя код C, который вы пишете, для создания предсказуемой программы.И во время оптимизации он может находить и оптимизировать (т.е. заменять лучшей версией машинного кода) некоторые паттерны, когда он известен алгоритму оптимизации - такая оптимизация не нарушит поведение программы.

Например, что-то вроде

char a[100];
for(int i=0; i < 100; i++)
   a[i]  = 0;

будет заменен одним вызовом библиотеки std :: memset (a, 0,100), который реализован с использованием ассемблера и оптимален для текущей архитектуры ЦП.

А также компилятор, способный обнаруживать

x ^= y;
y ^= x;
x ^= y;

и замените его простейшим кодом.

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

В случае замены двух целых чисел 3-я временная переменная обмена может быть удаленакомпилятор сам, то есть это будет что-то вроде

movl    $2, %ebx
movl    $1, %eax
xchg    %eax,%ebx  

, где значения регистров ebx и eax на самом деле являются вашими x и y.Вы можете реализовать это самостоятельно, например

void swap_x86(int& x, int& y)
{
    __asm__ __volatile__( "xchg %%rax, %%rbx": "=a"(x), "=b"(y) : "a"(x), "b"(y) : );
}
...
int a = 1;
int b = 2;
swap_x86(a,b);

Когда использовать __builtin_unreachable?Вероятно, когда вы знаете, что некоторые ситуации практически невозможны, но логически это может произойти.Т.е. у вас есть какая-то функция типа

void foo(int v) {

    switch( v ) {
        case 0:
            break;
        case 1:
            break;
        case 2:
            break;
        case 3:
            break;
        default:
            __builtin_unreachable();
    }
}

И вы знаете, что значение аргумента v всегда находится в диапазоне от 0 до 3. Тем не менее, диапазон int составляет от -2147483648 до 2147483647 (когда тип int 32-битный), компилятор не имеет представления о диапазоне реальных значений и не может удалить блок по умолчанию (а также некоторые инструкции cmp и т. д.), но он предупредит вас, если вы не добавите этот блок в switch.Так что в этом случае __builtin_unreachable может помочь.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...