Я использую int
тип для хранения значения. В соответствии с семантикой программы значение всегда изменяется в очень небольшом диапазоне (0 - 36), и int
(не a char
) используется только из-за эффективности процессора.
Кажется, что многие специальные арифметические оптимизации могут быть выполнены для такого небольшого диапазона целых чисел. Многие вызовы функций для этих целых чисел могут быть оптимизированы в небольшой набор «магических» операций, а некоторые функции могут быть даже оптимизированы для поиска в таблице.
Итак, можно ли сказать компилятору, что int
он всегда находится в этом небольшом диапазоне, и возможно ли, чтобы компилятор выполнял эти оптимизации?
unsigned
типов, поскольку компилятору их легче рассуждать.var value: 0..36;
.int
иunsigned int
нужно расширять знак или ноль с 32 до 64 бит, также на большинстве систем с 64-битными указателями. Обратите внимание, что на x86-64 операции с 32-битными регистрами бесплатно расширяются с нуля до 64-битного (не расширение знака, но переполнение со знаком - неопределенное поведение, поэтому компилятор может просто использовать 64-битную математику со знаком, если он хочет). Таким образом, вы видите только дополнительные инструкции для расширенных нулями аргументов 32-битных функций, а не результаты вычислений. Вы бы для более узких типов без знака.Ответы:
Да, это возможно. Например,
gcc
вы можете__builtin_unreachable
сообщить компилятору о невозможных условиях, например:Мы можем обернуть условие выше в макрос:
И используйте это так:
Как видите ,
gcc
выполняет оптимизацию на основе этой информации:Производит:
Однако есть один недостаток: если ваш код нарушает такие предположения, вы получаете неопределенное поведение .
Он не уведомляет вас, когда это происходит, даже в отладочных сборках. Для более легкой отладки / тестирования / выявления ошибок с допущениями вы можете использовать гибридный макрос предположения / утверждения (кредиты @David Z), например, такой:
В отладочных сборках (с
NDEBUG
не определенным) он работает как обычноеassert
печатное сообщение об ошибке иabort
программа, а в сборочных выпусках он использует допущение, создающее оптимизированный код.Обратите внимание, однако, что он не заменяет обычный
assert
-cond
остается в сборках релиза, поэтому вы не должны делать что-то подобноеassume(VeryExpensiveComputation())
.источник
return 2
компилятор исключил переход из кода.__builtin_expect
это не строгая подсказка.__builtin_expect(e, c)
должен читаться как «e
скорее всего, оцениватьc
» и может быть полезен для оптимизации прогнозирования ветвлений, но это неe
всегдаc
так, поэтому оптимизатор не может отбрасывать другие случаи. Посмотрите, как организованы ветки в сборке .__builtin_unreachable()
.assert
, например, определить,assume
как,assert
когдаNDEBUG
не определено, и как,__builtin_unreachable()
когдаNDEBUG
определено. Таким образом, вы получаете преимущество предположения в производственном коде, но в отладочной сборке у вас все еще есть явная проверка. Конечно, тогда вы должны сделать достаточно тестов, чтобы убедиться, что это предположение будет выполнено в условиях дикой природы.Существует стандартная поддержка для этого. Что вы должны сделать, это включить
stdint.h
(cstdint
), а затем использовать типuint_fast8_t
.Это говорит компилятору, что вы используете только числа от 0 до 255, но что он может использовать больший тип, если это дает более быстрый код. Точно так же компилятор может предположить, что переменная никогда не будет иметь значения выше 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, но это встроенный тип, не определяемый заголовочными файлами в терминахchar
gcc / clang. Как я уже сказал, я не думаю, что большинство компиляторов имеют механизм это сделало бы это возможным.)uint_fast8_t
это хорошая рекомендация, поскольку она будет использовать 8-битный тип на платформах, где это так же эффективно, как иunsigned int
. (Я на самом деле не уверен, чтоfast
типы должны быть быстрыми для , и будет ли кэш след Компромисс должен быть частью этого.). x86 имеет обширную поддержку байтовых операций, даже для добавления байтов с источником памяти, так что вам даже не нужно делать отдельную загрузку с нулевым расширением (что также очень дешево). gcc делаетuint_fast16_t
64-битный тип на x86, что безумно для большинства применений (по сравнению с 32-битным). godbolt.org/g/Rmq5bv .Текущий ответ подходит для случая, когда вы точно знаете , что это за диапазон, но если вам все еще нужно правильное поведение, когда значение выходит за пределы ожидаемого диапазона, тогда оно не будет работать.
Для этого случая я обнаружил, что эта техника может работать:
Идея заключается в обмене данными кода: вы перемещаете 1 бит данных (независимо от того
x == c
) ли в управляющую логику .Это намекает оптимизатору, который
x
на самом деле является известной константойc
, побуждая его встроить и оптимизировать первый вызовfoo
отдельно от остальных, возможно, довольно сильно.Удостоверьтесь, что на самом деле код разделен на одну подпрограмму
foo
- не дублируйте код.Пример:
Чтобы эта техника работала, вам нужно немного повезти - в некоторых случаях компилятор решает не оценивать вещи статически, и они являются произвольными. Но когда это работает, это работает хорошо:
Просто используйте
-O3
и обратите внимание на предварительно оцененные константы0x20
и0x30e
в выходе на ассемблере .источник
if (x==c) foo(c) else foo(x)
? Если только пойматьconstexpr
реализацииfoo
?constexpr
было, и никогда не удосужился «обновить» ее позже (хотя я даже не удосужился беспокоиться о нейconstexpr
даже потом), но причина, по которой я не делал этого изначально, заключалась в том, что я хотел упростит компилятору выделение их в общий код и удаление ветви, если он решил оставить их как обычные вызовы методов, а не оптимизировать. Я ожидал, что, если я добавлю,c
компилятору будет очень трудно (извините, плохая шутка), что это один и тот же код, хотя я никогда не проверял это.Я просто хочу сказать, что если вы хотите решение, которое является более стандартным C ++, вы можете использовать этот
[[noreturn]]
атрибут для написания своего собственногоunreachable
.Поэтому я переназначу отличный пример Дениса, чтобы продемонстрировать:
Что, как вы можете видеть , приводит к почти идентичному коду:
Недостатком является, конечно, то, что вы получаете предупреждение о том, что
[[noreturn]]
функция действительно возвращает.источник
clang
, когда мое оригинальное решение не , так хороший трюк и +1. Но все это очень зависит от компилятора (как показал нам Питер Кордес,icc
это может ухудшить производительность), поэтому оно все еще не универсально применимо. Также, небольшое замечание:unreachable
определение должно быть доступно оптимизатору и встроено, чтобы это работало .