Могу ли я называть оптимизатор, задавая диапазон целого числа?

Я использую тип int для хранения значения. По семантике программы значение всегда изменяется в очень малом диапазоне (0-36), а int (не a char) используется только из-за эффективности ЦП.

Кажется, что многие специальные арифметические оптимизации могут выполняться на таком небольшом диапазоне целых чисел. Многие вызовы функций этих целых чисел могут быть оптимизированы в небольшой набор "магических" операций, а некоторые функции могут даже оптимизироваться в виде таблицы.

Итак, можно ли сообщить компилятору, что этот int всегда находится в этом небольшом диапазоне, и возможно ли, чтобы компилятор выполнил эти оптимизации?

Ответ 1

Да, это возможно. Например, для gcc вы можете использовать __builtin_unreachable, чтобы сообщить компилятору о невозможных условиях, например:

if (value < 0 || value > 36) __builtin_unreachable();

Мы можем обернуть условие выше в макросе:

#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)

И используйте его так:

assume(x >= 0 && x <= 10);

Как вы можете видеть, gcc выполняет оптимизацию на основе этой информации:

#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)

int func(int x){
    assume(x >=0 && x <= 10);

    if (x > 11){
        return 2;
    }
    else{
        return 17;
    }
}

Выдает:

func(int):
    mov     eax, 17
    ret

Один недостаток, однако, что , если ваш код когда-либо нарушает такие предположения, вы получаете undefined поведение.

Он не уведомляет вас, когда это происходит, даже в отладочных сборках. Чтобы легче отлаживать/тестировать/улавливать ошибки с допущениями, вы можете использовать гибридный макрос accept/assert (кредиты для @David Z), как этот:

#if defined(NDEBUG)
#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)
#else
#include <cassert>
#define assume(cond) assert(cond)
#endif

В отладочных сборках (с NDEBUG не), он работает как обычный assert, сообщение об ошибке печати и abort 'ing, а в релиз-сборках он использует предположение, создающее оптимизированный код.

Обратите внимание, однако, что он не является заменой регулярных assert - cond остается в релизах, поэтому вы не должны делать что-то вроде assume(VeryExpensiveComputation()).

Ответ 2

Для этого существует стандартная поддержка. Вам следует включить stdint.h (cstdint), а затем использовать тип uint_fast8_t.

Это говорит компилятору, что вы используете только числа от 0 до 255, но можете использовать более крупный тип, если это дает более быстрый код. Аналогично, компилятор может предположить, что переменная никогда не будет иметь значение выше 255, а затем будет делать оптимизацию соответственно.

Ответ 3

Текущий ответ хорош для случая, когда вы точно знаете, что такое диапазон, но если вы все еще хотите правильное поведение, когда значение выходит за пределы ожидаемого диапазона, то это не сработает.

В этом случае я нашел, что этот метод может работать:

if (x == c)  // assume c is a constant
{
    foo(x);
}
else
{
    foo(x);
}

Идея заключается в компрометации кода: вы перемещаете 1 бит данных (будь то x == c) в логику управления.
Это указывает на оптимизатор, что x фактически является известной константой c, поощряя ее встроить и оптимизировать первый вызов foo отдельно от остальных, возможно, довольно сильно.

Удостоверьтесь, что этот код действительно используется в одной подпрограмме foo, но не дублируйте код.

Пример:

Чтобы этот метод работал, вам нужно немного повезло - бывают случаи, когда компилятор решает не оценивать вещи статически, и они являются произвольными. Но когда он работает, он работает хорошо:

#include <math.h>
#include <stdio.h>

unsigned foo(unsigned x)
{
    return x * (x + 1);
}

unsigned bar(unsigned x) { return foo(x + 1) + foo(2 * x); }

int main()
{
    unsigned x;
    scanf("%u", &x);
    unsigned r;
    if (x == 1)
    {
        r = bar(bar(x));
    }
    else if (x == 0)
    {
        r = bar(bar(x));
    }
    else
    {
        r = bar(x + 1);
    }
    printf("%#x\n", r);
}

Просто используйте -O3 и обратите внимание на предварительно оцененные константы 0x20 и 0x30e в выход ассемблера.

Ответ 4

Я просто пишу, чтобы сказать, что если вам может понадобиться решение, более стандартное С++, вы можете использовать атрибут [[noreturn]], чтобы написать свой собственный unreachable.

Итак, я намерен переименовать deniss отличный пример, чтобы продемонстрировать:

namespace detail {
    [[noreturn]] void unreachable(){}
}

#define assume(cond) do { if (!(cond)) detail::unreachable(); } while (0)

int func(int x){
    assume(x >=0 && x <= 10);

    if (x > 11){
        return 2;
    }
    else{
        return 17;
    }
}

Какой как вы можете видеть, приводит к почти идентичному коду:

detail::unreachable():
        rep ret
func(int):
        movl    $17, %eax
        ret

Недостатком, конечно же, является то, что вы получаете предупреждение о том, что функция [[noreturn]] действительно возвращается.