Возведение двойки в целую степень
Какой код будет быстрее?
res = pow(2, delta);
res = 1 << abs(delta);
if (delta < 0)
res = 1 / res;
if (delta >= 0)
res = 1 << delta;
else
res = 1 / (1 << -delta);
delta- целоеres- double
Ответы (3 шт):
Подозреваю, что будет сильно зависеть от языка и, более того, от его конкретной реализации. Вот тест на Python в Google Colab. И да - в Python есть два синтаксических варианта для возведения в степень, и они, как ни странно, различаются по скорости:
def test_pow(delta):
res = pow(2, delta)
return res
def test_asterisk(delta):
res = 2 ** delta
return res
def test_shift(delta):
res = 1 << abs(delta)
if delta < 0:
res = 1 / res
return res
def test_shift2(delta):
if delta >= 0:
res = 1 << delta;
else:
res = 1 / (1 << -delta)
return res
assert test_pow(2) == 4
assert test_pow(-2) == 1/4
assert test_asterisk(2) == 4
assert test_asterisk(-2) == 1/4
assert test_shift(2) == 4
assert test_shift(-2) == 1/4
assert test_shift2(2) == 4
assert test_shift2(-2) == 1/4
%timeit list(map(test_pow, range(-10,10)))
%timeit list(map(test_asterisk, range(-10,10)))
%timeit list(map(test_shift, range(-10,10)))
%timeit list(map(test_shift2, range(-10,10)))
Результаты:
100000 loops, best of 5: 6.35 µs per loop
100000 loops, best of 5: 5.35 µs per loop
100000 loops, best of 5: 4.93 µs per loop
100000 loops, best of 5: 3.69 µs per loop
Т.е. скорость растёт с каждым вариантом. И можно сделать вывод, что во-первых сдвиги работают быстрее, чем возведение в степень (и это так скорее всего будет в любом языке), а во-вторых (по крайней мере в питоне) вызов любых функций - это накладные расходы, чем меньше вызовов, тем лучше. Я так думаю, выигрыш последнего варианта над предпоследним определяет то, что в нём не вызывается функция abs.
Для контроля сделал такой же тест для степеней в диапазоне [-1000,1000), а то вдруг от этого что-то зависит, но суть результатов не поменялась, хотя разрыв между разными вариантами чуть уменьшился:
1000 loops, best of 5: 993 µs per loop
1000 loops, best of 5: 884 µs per loop
1000 loops, best of 5: 790 µs per loop
1000 loops, best of 5: 668 µs per loop
Если у вас практический интерес, то можно без всяких там делений - собрать нужное число согласно формату IEEE 754.
Тест на скорую руку показывает времена в мс на 2*10^9 операций (при использовании отрицательной степени):
pow 27000
1/res 5866
ieee с ^@ 1513
ieee с absolute 811
пустой цикл 760
Для float/single:
var
s: Single;
pow: Integer;
i: Integer;
begin
// pow := 4;
// pow := 0;
pow := -4;
i := (pow + 127) shl 23;
s := PSingle(@i)^;
Memo1.Lines.Add(s.ToString)
16
1
0.0625
Для double (с использованием Int64):
i := (pow + 1023) shl 52;
Приведение области памяти целого типа к вещественному может быть реализовано любым способом - через указатели, как я сделал, через вариантную запись, через absolute.
Для особо отрицательных степеней (от 2^-126 до 2^-149) нужно делать денормализованные числа - но есть ли нужда в этом?
Если предположить, что для исходного кода используется язык программирования с генерацией непосредственно исполняемого процессором кода (C/c++), а не скриптовый язык, то для оптимизирущего компилятора два последних случая будут практически эквивалентны и на порядок быстрее первого случая (на x86 платформах с FPU).
В первом способе производятся достаточно тяжеловесные операции по преобразованию целого числа в double, а потом ещё и вызов не самой быстрой библиотечной функции.
Слабость же двух последних методов 1) в приведении целого числа к формату с плавающей точкой, 2) в медленной операции деления, 3) в ветвлении в зависимости от знака delta
Если же использовать способ от @MBo (при твёрдой уверенности, что double на платформе соответсвует формату binary64 IEEE 754, а значение delta находится в нужном диапазоне)
...без всяких там делений - собрать нужное число согласно формату IEEE 754.
, то это будет быстрый, хоть и не столь "законный" способ выполнить
res = ldexp(1.0, delta);
, но без реального вызова самой библиотечной функции, выполнения проверок в ней, формирования аргументов для неё.
Достаточно "просто" написать нечто
res = reinterpret_cast<double&&>(
reinterpret_cast<int64_t&&>(1.0) +
(static_cast<int64_t>(delta) << std::numeric_limits<double>::digits -
/* hidden bit */ 1));
Здесь для формирования результата инструкции и ресурсы FPU вообще не используется, все операции целочисленные. Результат формируется сдвигом и сложением с константой.
Если описать используемые переменные
int const volatile delta = -7;
double volatile res;
, а тело "пустого цикла"
(void)delta;
, чтобы оптимизирующий компилятор всё же делал обращения к аргументам, а не свёл цикл к единственной операции, то характерные времена на одну итерацию
| Способ | Время итерации (ns) |
|---|---|
| "пустой цикл" | 0.3525 |
| способ №1 (с pow) | 26.7643 |
| способ №2 (с abs) | 2.4101 |
| способ №3 (с ветвлением по знаку) | 2.4372 |
| способ @MBo бинарной сборки числа в формате IEEE 754 | 0.4051 |
| ldexp | 9.0168 |
Отсюда видно, что способы №2 и №3 в десяток раз быстрее, чем первый, но и способ @MBo значительно быстрее, чем любой из исходной задачи.
Указанные времена похожи на времена, измеренные @MBo, если откалибровать время доступа к исходным данным.
Из этой же таблицы видны и отличия от измерений в интерпретаторе, где оправдываются подозрения @CrazyElf в том, что
разрыв между разными вариантами...
...будет сильно зависеть от языка и, более того, от его конкретной реализации
, а вовсе не от низкоуровневых операций.
Дополнительное замечание об ограничениях указанных способов:
powиldexpобрабатывают выход за диапазоны двоичной экспоненты штатным образом.- Способ @MBo требует контроля диапазона в пределах 11 битного представления
delta. - Диапазон же допустимых значений
deltaдля способов №2 и №3 совсем узкий: не более 6 бит.