#java #floating-point #double #micro-optimization #type-punning
#java #с плавающей запятой #double #микрооптимизация #каламбур типа
Вопрос:
Я хочу увеличить показатель примитива double
d
, умножив его таким образом на степень двойки, например 2 ^ 64.
Я могу сделать d *= math.pow(2, 64)
или рассчитать мощность заранее, если известно : d *= 18446744073709551616
.
Но я предполагаю, что это выполняет некоторые дорогостоящие операции, такие как умножение, где мы знаем, что в этом случае все, что нужно, — это одно добавление показателей. Например, если возможно интерпретировать double как long без его преобразования, мы могли бы сделать: (это так не работает)
long longFromD = d; // imagine the casting does not change any bits from d.
double dTimes2ToThe64 = longFromD (64L << 52) // shifting 64 to be aligned with the exponent of d
И таким образом мы могли бы получить это умножение с помощью простого сложения.
Комментарии:
1. Можно интерпретировать double как long . Смотрите
Double.doubleToLongBits()
иDouble.longBitsToDouble()
2. переосмысление битов называется «каламбуром типов» (особенно в «небезопасных» языках, таких как C и C , где вы можете сделать это, например, с помощью memcpy, не требуя специальной поддержки со стороны языка). Повторно помечен, к сожалению, поэтому ограничивается только 5 тегами.
3. обратите внимание, что вместо
math.pow(2, 64)
вы можете просто использовать 0x1.0p644. Если Java имеет
ldexp()
подобный C (я подозреваю, что это не так), используйтеldexp(x, n)
для быстрого масштабирования по степени 2.
Ответ №1:
С помощью функций из комментария @Johannes Kuhn:
public static double multiplyByAPowerOfTwo(double d, int exponent) {
long dInterpretedAsLong = Double.doubleToLongBits(d);
dInterpretedAsLong = ((long) exponent) << 52; // add exponents
return Double.longBitsToDouble(dInterpretedAsLong)
}
Другой вариант согласно комментарию @phuclv, используя HexadecimalFloatingPointLiteral
:
d *= 0x1.0p64;
Для этого метода показатель степени, очевидно, должен быть известен заранее. Предположительно, это то же d *= 18446744073709551616d;
самое, что и, но писать его гораздо элегантнее.
С помощью этого метода у нас есть одно двойное умножение, которое, как мы надеемся, примерно так же эффективно, как простое сложение показателей (если умножение действительно происходит, то на единицу).
Комментарии:
1. Для известной константы
exponent
это может быть медленнее, чемvmulsd
инструкция x86, в зависимости от того, насколько хорошо выполняет работу JIT-компилятор. Для переменной экспоненты это может быть хорошо, если это позволяет избежать фактического экспоненциального вычисления, которое не использовало бы преимущества числа, являющегося известной степенью 2. Если2^exponent
вписывается в 64-разрядное целое число, я думаю, вы могли бы сделатьd *= 1LL<<exponent;
или что-то в Java-версии, чтобы избежать 32-разрядного переполнения,и это должно эффективно компилировать / JIT.2. Остерегайтесь, что большой показатель будет переноситься в знаковый бит IEEE double, а не насыщаться до -Inf, как при обычном поведении переполнения IEEE.
3. если умножение действительно происходит, оно на единицу — вы имеете в виду в мантиссе? Современные модули FP mul / add / FMA имеют производительность, не зависящую от данных, поэтому их можно полностью конвейеризировать (запускать новую операцию каждый такт на единицу выполнения — если некоторые закончили раньше, они столкнулись бы). Все, что имеет значение для производительности, это то, что оно не переполняется, а все входные и выходные данные являются «нормализованными» двойными. (На некоторых процессорах NaN или Inf могут быть медленнее, и часто ненормальный крошечный ввод выполняется медленнее.)