GCC собирает неверный счетчик, если Хасуэлл не указал

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 в двойном дополнении.