Я использую c ++ для вычисления различных типов специальных функций (например, функции Ламберта, итерационных методов для вычисления инверсий и т. Д.). Во многих случаях существует явно лучший подход к работе с мантиссой и показателем степени напрямую.
Я нашел много ответов о том, как извлечь части мантиссы и экспоненты, однако все они были просто «академическими случаями с не очень эффективной скоростью вычислений», которые для меня немного бесполезны (моя мотивация работать с мантиссой и показателем заключается в улучшении вычислительной скорость). Иногда мне нужно вызывать какую-то конкретную функцию около миллиарда раз (очень дорогие вычисления), чтобы каждая сохраненная вычислительная работа была в порядке. И использование «frexp», которые возвращают мантиссу как double, не очень подходит.
Мои вопросы (для компилятора c ++ с плавающей точкой IEEE 754):
1) Как прочитать конкретный бит мантиссы с плавающей / двойной?
2) Как прочитать всю мантиссу в целое число / байт с плавающей запятой / двойной?
3) Те же вопросы, что и 1), 2) для показателя степени.
4) Те же вопросы, что и 1), 2), 3) для записи.
С уважением, моя мотивация — более быстрое вычисление, если я работаю с мантиссой или экспонентой напрямую. Я полагаю, что должно быть очень простое решение.
Во многих случаях существует явно лучший подход к работе с мантиссой и показателем степени напрямую.
Я знаю это чувство слишком хорошо из моей работы по обработке сигналов, но правда в том, что показатели и мантиссы не просто могут использоваться как отдельные числа; IEEE754 определяет несколько особых случаев, смещений и т. Д.
Я полагаю, что должно быть очень простое решение.
Инженерный опыт подсказывает мне: предложения, заканчивающиеся на «простое решение», обычно не соответствуют действительности.
«академические кейсы»
однако, это определенно не соответствует действительности (я приведу пример в конце).
На поплавках IEEE754 в реальном мире очень хорошо используется оптимизация. Тем не менее, я обнаружил, что благодаря более поздним возможностям процессоров x86 выполнять SIMD (одна инструкция, несколько данных) и общему факту, что число с плавающей запятой так же быстро, как и большинство операций с «сдвигом в битах», я обычно подозреваю, что вам не рекомендуется Попробуйте сделать это на небольшом уровне самостоятельно.
Как правило, поскольку IEEE754 является стандартом, вы найдете документацию о том, как он хранится в вашей конкретной архитектуре повсюду. Если вы смотрели, вы, по крайней мере, должны были найти статью в Википедии, объясняющую, как это сделать 1) и 2) (это не так статично, как вы думаете).
Что важнее:
не постарайтесь быть умнее вашего компилятора. Вы, вероятно, не будете, если вы явно не знаете, как векторизовать несколько идентичных операций.
Поэкспериментируйте с математическими оптимизациями вашего конкретного компилятора. Как уже упоминалось, в настоящее время они обычно мало что делают; Процессоры не медленнее выполняют вычисления с плавающей запятой, чем на целых числах, обязательно.
Я лучше посмотрю на ваши алгоритмы и поищу там потенциал для оптимизации.
Кроме того, пока я делаю это, давайте представим VOLK (Vector Optimized Library of Kernels), которая в основном является математической библиотекой для обработки сигналов. http://libvolk.org есть обзор. Заглянуть в ядра которые начинаются с 32f, например 32f_expfast. Вы заметите, что существуют разные реализации, общие и оптимизированные для ЦП, разные для каждого набора команд SIMD.
Вы можете скопировать адрес значения fp в unsigned char*
и обрабатывать полученный указатель как адрес массива, который перекрывает значение fp.
В C или C ++, если x
является двойной IEEE тогда, если L
64-битное int, выражение
L = *((long *) &x);
позволит получить доступ к битам напрямую.
Если s
это байт, представляющий знак (0 = ‘+’, 1 = ‘-‘), e
представляет собой целое число, представляющее беспристрастный показатель, и f
является длинным int, представляющим дробные биты, то
s = (byte)(L >> 63);
e = ((int)(L >> 52) & 0x7FF) - 0x3FF;
f = (L & 0x000FFFFFFFFFFFFF);
(Если f — нормализованное число, т.е. не 0, ненормальное, inf или NaN, то последнее выражение должно иметь 0x0010000000000000
добавлен к нему для учета неявного старшего бита 1 в двойном формате IEEE.)
Перепаковка знака, экспоненты и дроби обратно в двойник похожа:
L = (с << 63) + ((e + 0x3FF) << 52) + (е & 0x000FFFFFFFFFFFFF);
х = * ((двойной *) &Л);
Приведенный выше код генерирует только несколько машинных инструкций без вызовов подпрограмм на 64-битных машинах, скомпилированных с 64-битным кодом. В 32-битном коде иногда требуется выполнить 64-битную арифметику, но хороший компилятор обычно генерирует встроенный код. В любом случае этот подход очень быстрый.
Аналогичный подход работает для C # с использованием L = bitConverter.DoubleToInt64Bits(x);
а также x = BitConverter.Int64BitsToDouble(L);
или точно так же, как указано выше, если разрешен небезопасный код.