GCC поддерживает __builtin_clz(int x)
builtin, в котором подсчитывается количество начальных нулей (последовательных наиболее значимых нулей) в аргументе.
Среди прочего 0 это отлично подходит для эффективной реализации функции lg(unsigned int x)
, которая берет логарифм базы-2 x
, округляя вниз 1:
/** return the base-2 log of x, where x > 0 */
unsigned lg(unsigned x) {
return 31U - (unsigned)__builtin_clz(x);
}
Это работает прямолинейно - в частности, рассмотрим случай x == 1
и clz(x) == 31
- then x == 2^0
so lg(x) == 0
и 31 - 31 == 0
, и мы получим правильный результат. Более высокие значения x
работают аналогично.
Предполагая, что встроенный модуль эффективно реализован, это намного лучше, чем альтернативные чистые C-решения.
Теперь, как это происходит, операция, ведущая нулевым счетчиком, по существу является двойственной для инструкции bsr
в x86. Это возвращает индекс наиболее значимого 1-битного 2 в аргументе. Итак, если есть 10 ведущих нулей, первый 1-бит находится в бит 21 аргумента. В общем случае 31 - clz(x) == bsr(x)
и так bsr
фактически непосредственно реализует нашу желаемую функцию lg()
без лишней части 31U - ...
.
Фактически вы можете читать между строкой и видеть, что функция __builtin_clz
реализована с учетом bsr
: она определяется как поведение undefined, если аргумент равен нулю, когда, конечно, "ведущие нули" операция отлично определена как 32 (или любой размер бита int
) с нулевым аргументом. Таким образом, __builtin_clz
, безусловно, был реализован с идеей эффективного отображения инструкции bsr
на x86.
Однако, глядя на то, что действительно делает GCC, в -O3
с другими параметрами по умолчанию: добавляет тонну дополнительного мусора:
lg(unsigned int):
bsr edi, edi
mov eax, 31
xor edi, 31
sub eax, edi
ret
Линия xor edi,31
фактически является not edi
для нижних 4 бит, которые действительно имеют значение, что по-одному 3 из neg edi
, что превращает результат bsr
в clz
. Затем выполняется операция 31 - clz(x)
.
Однако с -mtune=haswell
код упрощает в точно ожидаемую команду bsr
:
lg(unsigned int):
bsr eax, edi
ret
Почему это так, для меня очень непонятно. Инструкция bsr
существует уже несколько десятилетий до Хасуэлла, и поведение, AFAIK, не изменилось. Это не просто проблема настройки для определенной арки, поскольку bsr
+ набор дополнительных инструкций не будет быстрее обычного bsr
и, кроме того, с помощью -mtune=haswell
все еще результаты в более медленном коде.
Ситуация для 64-разрядных входов и выходов еще немного хуже: есть дополнительный movsx
в критическом пути, который, кажется, ничего не делает, поскольку результат из clz
никогда не будет отрицательным. Опять же, вариант -march=haswell
оптимален с помощью одной инструкции bsr
.
Наконец, давайте проверим больших игроков в пространстве компилятора, отличном от Windows, icc
и clang
. icc
просто выполняет плохую работу и добавляет лишние вещи вроде neg eax; add eax, 31; neg eax; add eax, 31;
- wtf? clang
выполняет хорошую работу независимо от -march
.
0 Например, сканирование растрового изображения для первого заданного бита.
1 Логарифм 0 не определен, и поэтому вызов нашей функции с аргументом 0 - это поведение undefined.
2 Здесь LSB - это 0-й бит, а MSB - 31-й.
3 Напомним, что -x == ~x + 1
в двойном дополнении.