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

173

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

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

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

rolevax
источник
4
оптимизация диапазона значений существует во многих компиляторах, например. LLVM , но я не знаю ни одного языка намека , чтобы объявить его.
Ремус Русану
2
Обратите внимание, что если у вас никогда не бывает отрицательных чисел, вы можете получить небольшую выгоду от использования unsignedтипов, поскольку компилятору их легче рассуждать.
user694733
4
@RemusRusanu: Pascal позволяет вам определять типы поддиапазонов , например var value: 0..36;.
Эдгар Бонет
7
« int (не char) используется только потому, что эффективность процессора. » Этот старый кусок общепринятого мнения обычно не очень верно. Узкие типы иногда должны быть расширены от нуля до знака до полной ширины регистра, особенно при использовании в качестве индексов массивов, но иногда это происходит бесплатно. Если у вас есть массив этого типа, уменьшение объема кэша обычно перевешивает все остальное.
Питер Кордес
1
Забыл сказать: intи unsigned intнужно расширять знак или ноль с 32 до 64 бит, также на большинстве систем с 64-битными указателями. Обратите внимание, что на x86-64 операции с 32-битными регистрами бесплатно расширяются с нуля до 64-битного (не расширение знака, но переполнение со знаком - неопределенное поведение, поэтому компилятор может просто использовать 64-битную математику со знаком, если он хочет). Таким образом, вы видите только дополнительные инструкции для расширенных нулями аргументов 32-битных функций, а не результаты вычислений. Вы бы для более узких типов без знака.
Питер Кордес

Ответы:

230

Да, это возможно. Например, 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

Однако есть один недостаток: если ваш код нарушает такие предположения, вы получаете неопределенное поведение .

Он не уведомляет вас, когда это происходит, даже в отладочных сборках. Для более легкой отладки / тестирования / выявления ошибок с допущениями вы можете использовать гибридный макрос предположения / утверждения (кредиты @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программа, а в сборочных выпусках он использует допущение, создающее оптимизированный код.

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

Питер Мортенсен
источник
5
@Xofo, не понял, в моем примере это уже происходит, поскольку return 2компилятор исключил переход из кода.
6
Однако кажется, что gcc не может оптимизировать функции для магических операций или поиска в таблице, как ожидалось в OP.
jingyu9575
19
@ user3528438, __builtin_expectэто не строгая подсказка. __builtin_expect(e, c)должен читаться как « eскорее всего, оценивать c» и может быть полезен для оптимизации прогнозирования ветвлений, но это не eвсегда cтак, поэтому оптимизатор не может отбрасывать другие случаи. Посмотрите, как организованы ветки в сборке .
6
Теоретически любой код, который безусловно вызывает неопределенное поведение, может быть использован вместо __builtin_unreachable().
CodesInChaos
14
Если нет какой-то причуды, о которой я не знаю, которая делает это плохой идеей, возможно, имеет смысл объединить это с assert, например, определить, assumeкак, assertкогда NDEBUGне определено, и как, __builtin_unreachable()когда NDEBUGопределено. Таким образом, вы получаете преимущество предположения в производственном коде, но в отладочной сборке у вас все еще есть явная проверка. Конечно, тогда вы должны сделать достаточно тестов, чтобы убедиться, что это предположение будет выполнено в условиях дикой природы.
Дэвид З
61

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

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

Лундин
источник
2
Эти типы используются не так часто, как следовало бы (лично я склонен забывать, что они существуют). Они дают код, который является одновременно быстрым и переносимым, довольно блестящим. И они были вокруг с 1999 года.
Лундин
Это хорошее предложение для общего случая. Ответ Дениса показывает более гибкое решение для конкретных сценариев.
Гонки легкости на орбите
1
Компилятор получает информацию о диапазоне 0-255 только в тех системах, где uint_fast8_tфактически используется 8-битный тип (например,unsigned char ), как в x86 / ARM / MIPS / PPC ( godbolt.org/g/KNyc31 ). В ранних версиях DEC Alpha до 21164A загрузка / сохранение байтов не поддерживалась, поэтому любая разумная реализация использовалась бы typedef uint32_t uint_fast8_t. AFAIK, у типа нет механизма для того, чтобы иметь дополнительные ограничения диапазона с большинством компиляторов (таких как gcc), поэтому я почти уверен, uint_fast8_tчто будет вести себя точно так же, как unsigned intи в этом случае.
Питер Кордес
( boolявляется специальным и ограничен диапазоном 0 или 1, но это встроенный тип, не определяемый заголовочными файлами в терминах chargcc / clang. Как я уже сказал, я не думаю, что большинство компиляторов имеют механизм это сделало бы это возможным.)
Питер Кордес
1
В любом случае, uint_fast8_tэто хорошая рекомендация, поскольку она будет использовать 8-битный тип на платформах, где это так же эффективно, как и unsigned int. (Я на самом деле не уверен, чтоfast типы должны быть быстрыми для , и будет ли кэш след Компромисс должен быть частью этого.). x86 имеет обширную поддержку байтовых операций, даже для добавления байтов с источником памяти, так что вам даже не нужно делать отдельную загрузку с нулевым расширением (что также очень дешево). gcc делает uint_fast16_t64-битный тип на x86, что безумно для большинства применений (по сравнению с 32-битным). godbolt.org/g/Rmq5bv .
Питер Кордес
8

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

Для этого случая я обнаружил, что эта техника может работать:

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в выходе на ассемблере .

user541686
источник
Разве вы не хотите if (x==c) foo(c) else foo(x)? Если только поймать constexprреализации foo?
MSalters
@MSalters: я знал, что кто-то спросит об этом !! Я придумал эту технику, прежде чем это constexprбыло, и никогда не удосужился «обновить» ее позже (хотя я даже не удосужился беспокоиться о ней constexprдаже потом), но причина, по которой я не делал этого изначально, заключалась в том, что я хотел упростит компилятору выделение их в общий код и удаление ветви, если он решил оставить их как обычные вызовы методов, а не оптимизировать. Я ожидал, что, если я добавлю, cкомпилятору будет очень трудно (извините, плохая шутка), что это один и тот же код, хотя я никогда не проверял это.
user541686 10.11.16
4

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

Поэтому я переназначу отличный пример Дениса, чтобы продемонстрировать:

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]]функция действительно возвращает.

Рассказчик - Unslander Monica
источник
Это работает с clang, когда мое оригинальное решение не , так хороший трюк и +1. Но все это очень зависит от компилятора (как показал нам Питер Кордес, iccэто может ухудшить производительность), поэтому оно все еще не универсально применимо. Также, небольшое замечание: unreachableопределение должно быть доступно оптимизатору и встроено, чтобы это работало .