Прямое увеличение показателя double, чтобы умножить на степень два

#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.0p64

4. Если 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 могут быть медленнее, и часто ненормальный крошечный ввод выполняется медленнее.)