Ах, старый трюк с непосредственным растровым изображением.
GCC делает это тоже, по крайней мере, для коммутатора. x86 asm casetable реализация .К сожалению, в некоторых случаях GCC9 имеет регрессию: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=91026#c3;GCC8 и более ранние версии работают лучше.
Еще один пример его использования, на этот раз для code-golf (наименьшее количество байтов кода, в данном случае машинный код x86) для обнаружения определенных букв: Испытание признательности пользователя# 1: Деннис ♦
Основная идея состоит в том, чтобы использовать входные данные в качестве индекса в растровом изображении результатов true / false.
FirstВы должны проверить диапазон, потому что растровое изображение имеет фиксированную ширину, а сдвиги x86 оборачивают счетчик сдвигов.Мы не хотим, чтобы высокие значения входили в диапазон, где есть некоторые, которые должны возвращать true.cmp edi, 24
/ ja
выполняет.
(Если диапазон между самым низким и самым высоким значениями true
был, например, от 120 до 140, он мог бы начинаться с sub edi,120
для изменения диапазонавсе до cmp
.)
Затем вы используете bitmap & (1<<e)
( bt
инструкция ) или (bitmap >> e) & 1
(shr
/ and
) для проверкибит в битовой карте, который сообщает вам, должно ли значение e
возвращать значение true или false.
Существует много способов реализовать эту проверку, логически эквивалентные, но с различиями в производительности.
Если бы диапазон был шире 32, ему пришлось бы использовать 64-битный размер операнда.Если он шире 64, компилятор может вообще не пытаться выполнить эту оптимизацию.Или все еще может сделать это для некоторых условий, которые находятся в узком диапазоне.
Использование еще большего растрового изображения (в памяти .rodata) было бы возможно, но, вероятно, не то, что большинство компиляторов придумает для вас.Либо с bt [mem],reg
(неэффективно), либо вручную индексируем слово и проверяем, что таким же образом этот код проверяет непосредственную битовую карту.Если у вас было много диапазонов с высокой энтропией, возможно, стоит проверить 2x 64-битную непосредственную битовую карту, ветвящуюся или без ветвлений ...
Clang / LLVM предлагает другие приемы для эффективного сравнения снесколько значений (когда не имеет значения, какое из них ударили), например, передать значение в регистр SIMD и использовать упакованное сравнение.Это не зависит от значений, находящихся в плотном диапазоне.( Clang генерирует худший код для 7 сравнений, чем для 8 сравнений )
, который оптимизирует код намного больше, чем я думаю, возможно.
Эти виды оптимизации происходят от умных разработчиков-компиляторов, которые замечают общие шаблоны в исходном коде и думают о разумных способах их реализации.Затем заставьте компиляторы распознавать эти шаблоны и преобразовать их внутреннее представление логики программы, чтобы использовать хитрость.
Оказывается, что операторы switch
и switch-like if()
являются общими, а агрессивные оптимизации - обычными.
Компиляторы далеки от совершенства, но иногда они действительно приближаются к тому, что люди часто утверждают;Эти компиляторы оптимизируют ваш код для вас, чтобы вы могли писать его понятным для человека способом и при этом выполнять его почти оптимально.Иногда это справедливо для малого масштаба.
Поскольку я не понимаю, что происходит в первом случае, я не могу судить, какой из них более эффективен в моем случае.
Непосредственное растровое изображение значительно более эффективно .Нет доступа к памяти данных ни в одном из них, поэтому нет ошибок загрузки кэша.Единственная «дорогая» инструкция - это смещение с переменным счетом (3 моп на основной платформе Intel, из-за надоедливой семантики установки FLAGS в x86; BMI2 shrx
- только 1 моп и избегать mov
числа до ecx
.) https://agner.org/optimize. И другие ссылки на анализ производительности см. В https://stackoverflow.com/tags/x86/info.
* 1.083 * Каждая инструкция в цепочке cmp / cmov не менее 1 моп, и через каждую
cmov
имеется довольно длинная цепочка зависимостей, потому что MSVC не удосужился разбить ее на 2 или более параллельных цепочек.Но, несмотря на то, что это просто много мопов, гораздо больше, чем у растровой версии, тем хуже для пропускной способности (способность exec-of-order exec перекрывать работу с окружающим кодом), а также задержки.
bt
также дешево: 1 моп на современных AMD и Intel.(bts
, btr
, btc
- 2 на AMD, еще 1 на Intel).
Ветвь в версии с непосредственным растровым изображением могла быть setna
/ and
, чтобы сделатьон без ветвей, но специально для этого определения перечисления компилятор ожидал, что он будет в диапазоне.Это могло бы повысить предсказуемость ветвления, потребовав только e <= 31
, а не e <= 24
.
Так как enum
поднимается только до 29, а IIRC его UB имеет значения enum вне диапазона, онможет на самом деле полностью его оптимизировать.
Даже если ветвь e>24
не очень хорошо предсказывает, она все же, вероятно, в целом лучше.Учитывая текущие компиляторы, мы получаем выбор только между цепями cmp / cmov или branch + bitmap.Если не превратить логику asm обратно в C, чтобы ручные компиляторы превратили asm, который мы хотим, тогда мы можем получить без ветвления с помощью AND или CMOV, чтобы сделать его всегда нулевым для вне диапазона e
.
Но если нам повезет, оптимизация на основе профиля может позволить некоторым компиляторам выполнять проверку диапазона растровых изображений без ветвей.(В asm поведение shl reg, cl
с cl> 31 или 63 четко определено: на x86 он просто маскирует счет. В эквиваленте C вы можете использовать bitmap >> (e&31)
, который все еще может оптимизироваться до shr
; компиляторыЗнайте, что x86 shr маскирует счет, чтобы они могли его оптимизировать. Но не для других ISA, которые насыщают счетчик сдвига ...)
Есть много способов реализовать проверку битовых карт, которые довольномного эквивалента.Например, вы могли бы даже использовать выход CF shr
, установленный в соответствии с последним сдвинутым битом.По крайней мере, если вы заранее убедитесь, что CF имеет известное состояние для случая cl=0
.
Если вы хотите получить целочисленный результат bool
, смещение вправо кажется более целесообразным, чем bt
/setcc
, , но с shr
стоимостью 3 моп на Intel, на самом деле может быть лучше использовать bt reg,reg
/ setc al
. Особенно, если вам нужен только bool
, и вы можете использовать EAX какваше растровое назначение, поэтому предыдущее значение EAX определенно готово до setcc
.(Избегая ложной зависимости от некоторой несвязанной более ранней цепочки деп.)
Кстати, MSVC имеет другие глупости: as Каков наилучший способ установить регистр в ноль в сборке x86: xor, mov или и? объясняет, xor al,al
совершенно глупо по сравнению с xor eax,eax
, когда вы хотите обнулить AL.Если вам не нужно оставлять старшие байты RAX неизмененными, обнулите полный регистр идиомой обнуления.
И, разумеется, ветвление только для возврата 0 или возврата 1 не имеет особого смысла, если только вы этого не ожидаете.быть очень предсказуемым и хотеть сломать зависимость данных.Я ожидаю, что setc al
будет иметь больше смысла, чтобы прочитать результат CF bt