Часто бывает нужно посчитать целую часть логарифма по основанию 2 от любого целого числа.
Решение в лоб это сделать цикл и в этом цикле постоянно делить число на два, пока оно не станет равно нулю. Сколько таких делений произошло, таково и значение логарифма. Да, такой способ рабочий и имеет право на жизнь, но я хочу показать как это можно сделать без всяких циклов и сложных конструкций.
Итак, мы хотим посчитать следующую формулу:
Для тех, кому не интересны рассуждения, я дам сразу готовые функции для вычисления логарифма:
Для начала переведем число x в двоичную запись определенной длины.
Например, длины = 8, но это не принципиально и длина числа может быть любой.
А теперь вспомните, на чем основан перевод числа в двоичную запись. На том, чтобы представить число в виде суммы степеней двойки. Номер степени будет определять позицию бита, который равен 1. Например: . Т.е. номера степеней 5, 3, 2 и 0. Это значит что 5-ый, 3-ий, 2-ой, 0-ой биты равен 1. Остальные биты между ними равны нулю. Отсчет битов начинается с правой стороны. Получилось, что
Можно заметить, что перевод в двоичную запись тесно связан с возведением в степень, а логарифм это же операция обратная возведению в степень и равна она показателю степени.
Притом показатель степени, в которую нужно возвести 2, это номер единичного бита в двоичной записи. Получается, если найти номер единичного бита, то получим целую часть значения логарифма по основанию два. Например, если 32 = 100000, единичный бит стоит на 5 месте, поэтому логарифм равен 5.
Но поскольку единичных битов, может быть не 1, а несколько, то встает вопрос номер какого именно единичного бита брать, чтобы найти логарифм. Ответ — номер последнего единичного бита, начиная отсчет с правой стороны записи числа, потому что именно старшая степень двойки определяет целую часть логарифма, остальные составляют дробную часть логарифма.
Рассмотрим другой пример — число . Последний единичный бит стоит на 5 месте, поэтому целая часть логарифма от 45 равна 5. и действительно . Дробную часть мы отбрасываем и остается 5.
Также работает и с другими числами.
В итоге мы получили, что целая часть логарифма равна номеру последнего единичного бита отсчитывая справа. Вопрос: как найти номер последнего единичного бита?
Для этого есть функции основанные на побитовых операциях, которые я нашел в книжке Г.Уоррена «Алгоритмические трюки для программистов».
Обе функции хорошо там описаны, а их код я привел ранее.
Используя эти две функции алгоритм вычисления алгоритма следующий:
У логарифма есть исключительная ситуация, когда x = 0. По идее такого логарифма не существует (или в пределе он равен -?). Однако, поскольку мы в программировании немного отходим от законов математики, то функции все равно работают даже когда на вход функции подается ноль. В таком случае значение логарифма будет равно 32 (если число 32-разрядное). Это происходит потому что функция округления до ближайшей степени двойки выдаст 0, потом мы из нуля вычитаем единицу и получаем число 0xFFFFFFFF, а единиц в таком числе 32 поэтому логарифм и будет равен 32.
Да, с точки зрения математики это некорректно, но есть случаи, когда это полезно с точки зрения программирования.
Применять подобную функцию вряд ли станут для вычисления именно математического логарифма, потому что чаще считают логарифмы от вещественных чисел, а не целых.
Однако подсчет длины двоичного кода — задача, которая на практике возникает чаще.
Пусть дан двоичный код определенной длины. Это может быть, например, путь в бинарном дереве. Если перед эти кодом записать единичный бит, то можно вычислять длину этого кода без использования вспомогательных переменных через взятие целочисленного логарифма.
Например, пусть дан код 0001110110. Он записан например в ячейку из 32 бит и нам нужно часто считать длину этого кода. Для этого припишем перед кодом дополнительный единичный бит.
Получим: 10001110110. И теперь можем смело считать длину этого кода через целочисленный логарифм, не храня отдельно длину этого кода где-то еще.
Если считать длину кода, где все нули, то функция выдаст длину = 32, что может быть некорректно, поэтому такую ситуацию надо предусмотреть. В каких-то ситуациях полезно, чтобы функция выдавала 32, а в каких-то, например, ноль.
UPD:
В комментариях верно отметили, что асимптотика функций getHighBit_32 и getBitCount_32 не O(1), а , где n — разрядность числа. Поэтому итоговая сложность алгоритма расчета логарифма не O(1), как может показаться на первый взгляд, а O(log2(n)).
Решение в лоб это сделать цикл и в этом цикле постоянно делить число на два, пока оно не станет равно нулю. Сколько таких делений произошло, таково и значение логарифма. Да, такой способ рабочий и имеет право на жизнь, но я хочу показать как это можно сделать без всяких циклов и сложных конструкций.
Итак, мы хотим посчитать следующую формулу:
Решение
Для тех, кому не интересны рассуждения, я дам сразу готовые функции для вычисления логарифма:
uint32_t getHighBit_32(uint32_t x)
{
x |= x >> 1;
x |= x >> 2;
x |= x >> 4;
x |= x >> 8;
x |= x >> 16;
return x - (x >> 1);
}
uint32_t getBitCount_32(uint32_t x)
{
x = (x & 0x55555555) + ((x >> 1) & 0x55555555);
x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
x = (x & 0x0F0F0F0F) + ((x >> 4) & 0x0F0F0F0F);
x = (x & 0x00FF00FF) + ((x >> 8) & 0x00FF00FF);
return (x & 0x0000FFFF) + (x >> 16);
}
uint32_t getLog2_32(uint32_t x)
{
return getBitCount_32(getHighBit_32(x) - 1);
}
Объяснения
Для начала переведем число x в двоичную запись определенной длины.
Например, длины = 8, но это не принципиально и длина числа может быть любой.
А теперь вспомните, на чем основан перевод числа в двоичную запись. На том, чтобы представить число в виде суммы степеней двойки. Номер степени будет определять позицию бита, который равен 1. Например: . Т.е. номера степеней 5, 3, 2 и 0. Это значит что 5-ый, 3-ий, 2-ой, 0-ой биты равен 1. Остальные биты между ними равны нулю. Отсчет битов начинается с правой стороны. Получилось, что
Можно заметить, что перевод в двоичную запись тесно связан с возведением в степень, а логарифм это же операция обратная возведению в степень и равна она показателю степени.
Притом показатель степени, в которую нужно возвести 2, это номер единичного бита в двоичной записи. Получается, если найти номер единичного бита, то получим целую часть значения логарифма по основанию два. Например, если 32 = 100000, единичный бит стоит на 5 месте, поэтому логарифм равен 5.
Но поскольку единичных битов, может быть не 1, а несколько, то встает вопрос номер какого именно единичного бита брать, чтобы найти логарифм. Ответ — номер последнего единичного бита, начиная отсчет с правой стороны записи числа, потому что именно старшая степень двойки определяет целую часть логарифма, остальные составляют дробную часть логарифма.
Рассмотрим другой пример — число . Последний единичный бит стоит на 5 месте, поэтому целая часть логарифма от 45 равна 5. и действительно . Дробную часть мы отбрасываем и остается 5.
Также работает и с другими числами.
В итоге мы получили, что целая часть логарифма равна номеру последнего единичного бита отсчитывая справа. Вопрос: как найти номер последнего единичного бита?
Для этого есть функции основанные на побитовых операциях, которые я нашел в книжке Г.Уоррена «Алгоритмические трюки для программистов».
- Округление до степени двойки в меньшую сторону (или выделение последнего единичного бита в двоичной записи числа). На самом деле можно округлять и в большую сторону, но тогда значение логарифма тоже будет округлено в большую сторону.
- Подсчет количества единичных битов в двоичной записи числа
Обе функции хорошо там описаны, а их код я привел ранее.
Используя эти две функции алгоритм вычисления алгоритма следующий:
- Выделить последний единичный бит в числе. Теперь число стало записано в виде 100000
- Вычесть единицу из полученного числа. Тогда число станет таким: 011111
- Подсчитать количество единичных битов и это будет целое значение логарифма
Исключительная ситуация
У логарифма есть исключительная ситуация, когда x = 0. По идее такого логарифма не существует (или в пределе он равен -?). Однако, поскольку мы в программировании немного отходим от законов математики, то функции все равно работают даже когда на вход функции подается ноль. В таком случае значение логарифма будет равно 32 (если число 32-разрядное). Это происходит потому что функция округления до ближайшей степени двойки выдаст 0, потом мы из нуля вычитаем единицу и получаем число 0xFFFFFFFF, а единиц в таком числе 32 поэтому логарифм и будет равен 32.
Да, с точки зрения математики это некорректно, но есть случаи, когда это полезно с точки зрения программирования.
Подсчет длины двоичного кода
Применять подобную функцию вряд ли станут для вычисления именно математического логарифма, потому что чаще считают логарифмы от вещественных чисел, а не целых.
Однако подсчет длины двоичного кода — задача, которая на практике возникает чаще.
Пусть дан двоичный код определенной длины. Это может быть, например, путь в бинарном дереве. Если перед эти кодом записать единичный бит, то можно вычислять длину этого кода без использования вспомогательных переменных через взятие целочисленного логарифма.
Например, пусть дан код 0001110110. Он записан например в ячейку из 32 бит и нам нужно часто считать длину этого кода. Для этого припишем перед кодом дополнительный единичный бит.
Получим: 10001110110. И теперь можем смело считать длину этого кода через целочисленный логарифм, не храня отдельно длину этого кода где-то еще.
Если считать длину кода, где все нули, то функция выдаст длину = 32, что может быть некорректно, поэтому такую ситуацию надо предусмотреть. В каких-то ситуациях полезно, чтобы функция выдавала 32, а в каких-то, например, ноль.
Источники
- Г. Уоррен «Алгоритмические трюки для программистов. Исправленное издание.», 2004
UPD:
В комментариях верно отметили, что асимптотика функций getHighBit_32 и getBitCount_32 не O(1), а , где n — разрядность числа. Поэтому итоговая сложность алгоритма расчета логарифма не O(1), как может показаться на первый взгляд, а O(log2(n)).
rus084
Насколько этот алгоритм быстрее простого цикла?
Написал код, измеряющий время
include
uint32_t getHighBit_32(uint32_t x)
{
x |= x >> 1;
x |= x >> 2;
x |= x >> 4;
x |= x >> 8;
x |= x >> 16;
return x — (x >> 1);
}
uint32_t getBitCount_32(uint32_t x)
{
x = (x & 0x55555555) + ((x >> 1) & 0x55555555);
x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
x = (x & 0x0F0F0F0F) + ((x >> 4) & 0x0F0F0F0F);
x = (x & 0x00FF00FF) + ((x >> 8) & 0x00FF00FF);
return (x & 0x0000FFFF) + (x >> 16);
}
uint32_t getLog2_32(uint32_t x)
{
return getBitCount_32(getHighBit_32(x) — 1);
}
int getLog2Loop_32(uint32_t x)
{
int result = -1;
while (x)
{
x >>=1;
result++;
}
return result;
}
int main()
{
volatile int iterations = 2000000000;
volatile int value = 0x8;
int volatile temp;
unsigned int start_Log2 = clock(); // начальное время
for (int i = 0; i < iterations; i++)
{
temp = getLog2_32(value);
}
unsigned int end_Log2 = clock(); // начальное время
}
Вывод программы такой:
first algo time: 8179
second algo time:3771
В итоге простой цикл быстрее в 2 раза, но не уверен насчет адекватности измерений
rus084
Извините, время редактирования истекло, а сразу не увидел сломавшееся форматирование
Politura
А если getLog2Loop_32 переделать как-нибудь так? :)
assad77
в getBitCount_32 в сумме 30 операций
в getHighBit_32 еще 10
это против 64 для цикла. причем для цикла констант меньше. так что вполне может быть. здесь все очень близко в плане количества операций тк O(N) и (O(lg(N)) при N=32 отличаются всего в 6 раз, то большую роль начинают играть константы при O. так что могло получится и то что алгоритм O(N) быстрее, а в некоторых случая O(lg(N)). все зависит от архитектуры и времени исполнения конкретных операций. всегда надо тестировать в таких случаях.
btw для логарифма можно было бы сократить число операций, за счет того, что getBitCount_32 для 32 бит числа не больше 32. это сэекономило бы 4 операции в getHighBit_32, но правда принципиально не изменило бы ничего.
koldyr
Сложность в O нотации считается для тотальных функций.
GoldNotch Автор
Просветите пожалуйста, что такое тотальные функции?
koldyr
Определённые на всем множестве натуральных чисел.
dmitryredkin
Я вам объясню. Сложность вашего алгоритма — такая же (логарифмическая), как и у цикла. Просто вы искусственно ограничили перебор диапазоном uint_32. Если расширить алгоритм на 64-битные числа, то в функцию getHighBit придется добавлять операции, и во второй константы тоже расширятся. И так далее с ростом диапазона.
Да, рост будет небольшим, так на то и логарифмическая сложность!
alexprey
В принципе это тот же цикл, только развернутый вручную. Интересно смог ли ты таким способом превзойти оптимизацию компилятора в сравнении с циклом. Ну и в сравнении со встроенной функцией тоже интересно посмотреть. Без этого это так, детское развлечение)
raamid
Можно сделать бинарный поиск по отсортированному массиву степеней двоек, так вроде быстрее будет.
QtRoS
И ещё развернуть цикл в двоичном поиске :)
Alexey_Alive
А лучше не изобретать велосипед и использовать специальные процессорные инструкции :)
sleirsgoevy
В
gcc
существует интринсика__builtin_clz
, которая считает количество ведущих нулей в числе.novoselov
Есть еще встроенные функции для вызова инструкции popcnt
И ряд встроенных функций аналогичных lzcnt
Для любителей писать код всегда есть IntegerLogDeBruijn
VelocidadAbsurda
Более того, на x86 без поддержки ABM (добавляющей инструкцию LZCNT) оно «под капотом» сначала вычисляет как раз то, что нам нужно (инструкцией BSR, выдающей номер самого старшего единичного бита), а затем вычитает результат из полной разрядности (тут тонкость, вместо SUB используется XOR, важно для оптимизации дальше) чтобы вернуть количество ведущих нулей. Вот такое:
int log2 = 31 ^ __builtin_clz(someval);
с оптимизацией -O2 даёт «голую» BSR — вычисление искомого результата в одну инструкцию.
Примечание: на многих других популярных архитектурах (ARM, PowerPC) всё наоборот — есть CLZ в одну инструкцию, но нет аналога BSR, там выйдет логарифм в две инструкции (что тоже неплохо).
toyban
Боюсь, что Ваш алгоритм вовсе не константный, а линейный. Он является константным только для чисел фиксированного размера, например, как в Вашем случае для 32-битных чисел. Но если мы зафиксируем длину числа, то тут любой алгоритм будет константным.
Дело в том, что Вам надо вычислять все те "константы", что у Вас в коде. Так, например, Ваш код не будет работать для 64-битных чисел, так как для них будут нужны другие константы. А если мы возьмем 10,000-битное число? Тогда нам потребуется высчитать 10,000-битные константы. Как Вы видите, ваши "константы" зависят от длины входного числа.
А чтоб высчитать эти "константы", Вам придется применить линейное количество сдвигов битов. Причем много раз, так как у Вас множество этих "констант". В итоге мы получаем, что Ваш алгоритм имеет такую же линейную асимптотику, как и деления на двойки, но будет выполняться дольше тривиального алгоритма для больших чисел.
Хотя, возможно, что для относительно небольших чисел этот трюк довольно забавен и даже, думаю, будет эффективен.
Videoman
toyban
Покажите, пожалуйста, как Вы за log2(n) создадите число 0xFF00FF00FF00...FF00, где количество групп битов FF00 равно n/16.
Videoman
Ну так элементарно же:
х = 0xff00;
x |= x << 16;
x |= x << 32;
x |= x << 64;
x |= x << 128;
x |= x << 256;
...
toyban
Да, похоже, Вы правы. Тогда этот алгоритм будет в самом деле эффективней сдвигов. К сожалению, все равно не константный.
Videoman
Ну строго говоря да. При нефиксированном количестве бит его сложность O(log(N)). С другой стороны, это намного эффективнее чем O(N). На практике это обычно не важно, т.к. мы все-равно имеем фиксированное количество бит в целом, а на С++ я писал код, который генерирует все эти сдвиги и маски на этапе компиляции.
toyban
Я бы уточнил, что скорее всего там даже O(log?(n)), так как функция getBitCount выполняет log? итерации и в каждой надо высчитать "константу" за log? времени. Правда, мы тут предполагаем, что побитовые операции выполняются за единицу времени для любых n, что немного, наверное, наивно.
Videoman
Все верно. Но тут главное вовремя остановиться и зафиксировать что у нас считается константной операцией, иначе мы до тактов процессора дойдем и реализацию инструкций в микрокоде :)
maxzhurkin
Politura
Полагаю под «константный» в данном случае имеется ввиду, что независимо от значения аргумента, всегда будет выполнено одно и то же количество действий.
Я даже посчитал, там 31 операция. :)
Если брать простой перебор, там на каждый бит делается по 3 операции: сравнение, сдвиг, увеличение счетчика, и чем меньше значение аргумента, тем меньше будет сделано операций.
netch80
Всё так, но тут начинает работать другой фактор: обычно не находится безумца, который будет хранить 10000-битные числа в области фиксированного размера, не сохраняя размер числа. Всё распространённые библиотеки длинной арифметики хранят число в виде примерно как
Термин limb — из Gnu MP. Бывают разные оптимизации — например, в mpz-слое nlimbs знаковый, и его знак равен знаку самого числа; в C# BigInteger, если body == null, число хранится в самом nlimbs (важный частный случай короткого числа); и так далее. Но всё это не меняет общего принципа, что длина числа с точностью до лимба уже известна, и алгоритм статьи надо применить только для body[nlimbs-1].
Ну, как всегда, мерять надо.
Вообще в оригинале ("Hackers?s Delight") показано много интересных методов, как вам такой?
toyban
А тут это неважно. Если Вы прочитаете всю ветку, то заметите, что там обсуждали сложность алгоритма в терминах количества бит числа, то есть мы предполагаем, что мы знаем это количество, а не высчитываем его.
И да, с помощью бинарного поиска найти лидирующую единицу для больших чисел будет быстрей, чем данный алгоритм, если мы предполагаем, что все побитовые операции занимают константу времени. Хотя на самом деле они должны занимать линейное время, а потому и у бинарного поиска, и у данного алгоритма будет одинаковая асимптотика. Но вот константа у бинарного поиска будет куда меньше, чем у алгоритма автора.
Также я, видимо, неправильно понял Ваш комментарий. Если Вы сохраняете положение максимальной единицы, то тут тогда вообще нет о чем говорить. Тогда и вовсе никакой алгоритм не нужен. Но автор утверждал, будто он изобрел константный алгоритм, что совсем не соответствует действительности, если исходить из тех предпосылок, что он дал, а именно, что нам дано число в бинарной записи. Положение максимальной единицы в его входных данных не было, и именно это он пытался высчитать.
netch80
Я начал с "всё так", то есть по сути сказанного в пределах заданного контекста у меня не было возражений. Дальше уже шли комментарии по осмысленности такого применения на практике — то есть, контекста рассмотрения.
Она сохраняется обычно с точностью до лимба, но не до конкретного бита. Вот тут уже вступает в силу какой-то алгоритм из обсуждаемых. Какой окажется лучше — линейный, двоичный поиск на сдвигах, данный хитрый на масках — надо смотреть.
Вот тут я очень сильно сомневаюсь, но это опять же вопрос контекста. Если мы работаем с теми же 10000-битовыми числами и у нас есть эффективные 10000-битовые операции — в это можно поверить. Но с нынешним железом этого нет, и ткнув в произвольное слово в памяти вы не знаете, есть ли старшие не равные 0 значения. А значит — линейный поиск.
Потому получается, что бинарный поиск такого рода это чисто "сферическое в вакууме" применение. Дальше уже вопрос, хотите ли вы его в этом случае всё равно рассматривать теоретически...
Автор ничего не утверждал, что он изобрёл — посмотрите внимательно — он явно ссылается на "Hacker?s Delight". Статья по сути изложение одного метода из книги. Не знаю, какая ценность статьи в этом случае — разве что привлечь почитать книгу. Но я бы тогда лучше пересказал хотя бы два метода, а не один. Может, ему чем-то понравился именно этот метод — не знаю, пусть сам скажет.
toyban
Это верно, но обычно, когда упоминается O(*), то имеется в виду какая-то абстрактная формальная вычислительная модель, которая к нашим реальным вычислительным устройствам имеет довольно отдаленное отношение, чтоб как раз особо не заморачиваться вопросами особенностей архитектуры.
В книге все же не говорилось, что алгоритм константный. Но автор, по-моему, сделал на это ударение, вынеся этот "факт" прямо в оглавление. Что в реальности совершенно не так. Я критикую именно это.
dkondratiev
> Целочисленный логарифм по основанию 2 за O(1)
> Для начала переведем число x в двоичную запись определенной длины.
Смена системы из десятиричной в двоичную — сам по себе алгоритм O(log(n))
> постоянно делить число на два, пока оно не станет равно нулю
А это грозит бесконечным циклом :)
maxzhurkin
NeoCode
Я правильно понимаю, что это то же самое что делает одна единственная ассемблерная инструкция BSF (Bit Scan Forward)?
PS Вообще жалко что эти команды, как и битовые вращения, не вывели в операции языка C/C++ — они были бы более известны, как сдвиги например:) Для битовых сканирований можно было бы применить например унарные << и >>, для вращений бинарные <<< и >>>. Еще из полезных инструкций есть popcnt (количество единиц). А вот битового разворота (RBIT) в x86/x64 так и не завезли, к сожалению.
kibb
Да, BSF или поновее LZCNT для x86, CLZ для ARMv7/8.
Siemargl
FYL2X чем то не устраивает? Колеса недостаточно квадратные?
Alexey_Alive
x-87 давно уже не стоит использовать. AVX, для старых процессоров SSE.
Siemargl
А подскажи инструкцию, я сходу не нашел.
Alexey_Alive
Ну, одной инструкцией не делается. Посмотреть реализацию можно у Agner Fog. Производительность, естественно, выше, чем у x87.
P.S. А за что минусы?! Сами Intel же говорили ещё в нулевых, что x87 больше не стоит использовать — юзайте SSE. Да и посмотрите на современные высокопроизводительные либы, как много x87 инструкций вы там найдёте?!
netch80
Ну высокопроизводительные обычно векторные, а x87 не векторизуется. Но и для обычной плавучки сейчас рекомендуют предпочитать SSE.
Siemargl
Просто там нет инструкции для логарифма, а считать логарифм для вектора может оказаться не быстрее.
Такое вот исключение =)
Alexey_Alive
Так что x87 остаётся для обратной совместимости. Float и double вычисления (даже не векторные) можно легко перенести на SSE/AVX и это будет, как минимум, не медленнее, чем x87, а, почти всегда, быстрее.
Alexey_Alive
Ну вот опять минусы. Вы хоть с аргументами поясните где я обосрался.
netch80
А вы меряли?
FYL2X требует на входе уже плавучее число, а для преобразования целого в него процессор уже исполняет операцию, идентичную задаче исходной статьи — чтобы получить порядок и сдвинуть мантиссу в нужную позицию.
Siemargl
Это еще одна операция FILD
netch80
Мнэээ… не совсем. Дело не в FILD самой по себе — она как раз, скорее всего, грузит без нормализации. Дело в том, что для получения логарифма FYL2X требует нормализации входного числа. А для него требуется вычисление ожидаемой позиции точки, для чего в свою очередь и вычисляется та самая позиция старшего ненулевого бита.
И вот тут если это действие аппаратно ускорено (ну, начиная с 486, наверняка, да) — то в простых случаях оно может работать и быстрее этого хитрого целочисленного подхода. Но я уверен, что вычисление всех значимых цифр логарифма перекроет этот выигрыш на десятки тактов. Ну да, померять надо. Соберу вдохновения — померяю.
Siemargl
FILD быстрая, 3 такта вроде, FYL2X — долгая, но будет ли быстрее пересчет битов AVX инструкциями (там такие есть) — хз
netch80
Хм, выпрямил руки и прогнал тест. i3-4170, gcc 8 (или 7, без разницы), 64-битка.
Код.
Типовые результаты по времени:
это в пикосекундах получается на итерацию (то есть 24 нс с FYL2X) и с разрешением LZCNT для x86; без него получается ~2400 во второй строке. Третья строка — алгоритм из статьи, скопировал ничего не меняя.
Ну, я подозреваю, для FYL2X используется такой же аппаратный ускоритель на приоритетном энкодере, как и для BSR и LZCNT. Иначе бы не были такие хорошие времена.
Про AVX с реальной векторностью сейчас точно облом думать, но она вроде исходно и не предполагалась.
700 тактов, названные в статье по вашей ссылке, скорее всего, цифра от чего-то сильно более древнего (часом не оригинальный 8087?) Тут у меня на весь цикл 96 тактов, значит, сама команда сильно меньше занимает.
PS: Вообще вся та статья какая-то кривая. Где он на int(2+3000000000.0f) нашёл денормализованные??
Siemargl
Интересно посмотреть на код
builtin CLZ превращается просто в BSR или LZCNT(vplzcntd для O3) в зависимости от архитектуры.
А вот в какой фарш при -O3 превращается алгоритм из статьи, страшно смотреть. Но и ускоряется вдвое относительно O2
Lemko
Долго думал. Сколько мне еще учится?
neurocore
Всю жизнь
raamid
Не верьте ему. Жизни обычно не хватает.
maxzhurkin
Больше 10 лет, видимо
assad77
Только здесь сложность не O(1), а O(ln(N))
N=32 бита
2^5=32 в функции, например,
uint32_t getHighBit_32(uint32_t x)
5 строчек.
для 64 бит будет 6 срочек.
то что развернули цикл сложность не уменьшило
anonymous
То есть, чтобы сложность не росла, вы искусственным образом сделали ее сразу максимальной и константной.
maxzhurkin
Автор книги же: автор статьи её только «процитировал»
uhnalev
Идея: преобразуем целое число в число с плавающей запятой, «вытаскиваем» биты, отвечающие за порядок.
unsigned int mylog2(unsigned int x) {
unsigned int e;
*(float*)&e = (float)x;
e = (e >> 23) — 0x7F;
return e;
}