Как работать (быстро) с мантиссой и экспонентной частью double или float на с ++?

Я использую C ++ для вычисления различных типов специальных функций (например, функции Ламберта, итерационных методов для оценки инверсий и т. Д.). Во многих случаях очевидно лучший подход к работе с мантиссой и экспонентой напрямую.

Я нашел много ответов, как извлечь части мантиссы и экспоненты, однако все они были просто «академическими случаями с не очень эффективной скоростью вычислений», которые для меня немного бесполезны (моя мотивация работать с мантиссой и экспонентой заключается в улучшении вычислительной скорость). Иногда мне нужно вызвать какую-то конкретную функцию около миллиарда раз (очень дорогие вычисления), поэтому каждая сохраненная вычислительная работа в порядке. И использование "frexp", которое возвращает мантиссу как double, не очень подходит.

Мои вопросы (для компилятора c ++ с плавающей запятой IEEE 754):

1) Как прочитать конкретный бит мантиссы float / double?

2) Как прочитать всю мантиссу в целое число / байт float / double?

3) Те же вопросы, что и в 1), 2) для экспоненты.

4) Те же вопросы, что и в 1), 2), 3) для письма.

С учетом того, что моя мотивация - это более быстрое вычисление, если я работаю с мантиссой или экспонентой напрямую. Я полагаю, что должно быть очень простое решение.


person Marek Basovník    schedule 09.01.2016    source источник
comment
Взгляните на IEE754 в сети, все подробно объяснено. Я серьезно сомневаюсь, что вам действительно нужна такая вещь ...   -  person Jean-Baptiste Yunès    schedule 09.01.2016
comment
Есть очень простое, непереносимое решение. Вы собираетесь делать странные вещи, если можно так выразиться. Я бы туда не пошел. Напишите эффективный код с float / double, пусть компилятор и FPU сделают все остальное.   -  person Violet Giraffe    schedule 09.01.2016
comment
Вот хорошо известный пример использования битов напрямую, чтобы быстро вычислить обратный квадратный корень. Возможно, это вдохновит вас на работу: betterexplained.com/articles/   -  person Carlos    schedule 09.01.2016
comment
Я рекомендую использовать процессор с плавающей запятой или аппаратную поддержку с плавающей запятой, чтобы ускорить арифметические операции. В противном случае рассмотрите возможность использования нотации с фиксированной точкой.   -  person Thomas Matthews    schedule 09.01.2016
comment
Профиль. Профиль. Профиль. Оптимизируйте наиболее часто используемые или узкие места. Профиль. Недавно я применил некоторые микрооптимизации и получил 7 наносекунд. Другими словами, микрооптимизации могут не иметь такого большого значения, как изменение алгоритма.   -  person Thomas Matthews    schedule 09.01.2016
comment
Битовый уровень, скорее всего, выполняется в ALU. Поскольку большинство процессоров теперь используют FPU в стиле сопроцессора, перемещение между файлом целочисленных регистров и файлом регистров с плавающей запятой очень дорого. Даже в идеальном случае вы действительно получите, скорее всего, только ускорение умножения / деления на степень 2. Если ваш процессор имеет аппаратный FPU, то большинство операций с плавающей запятой обычно выполняется так же быстро, как и целочисленные операции, если нет, то вы действительно следует учитывать фиксированную точку.   -  person user3528438    schedule 10.01.2016


Ответы (3)


Во многих случаях очевидно лучший подход к работе с мантиссой и экспонентой напрямую.

Я слишком хорошо знаю это чувство по моей работе по обработке сигналов, но правда в том, что экспоненты и мантиссы нельзя просто использовать как отдельные числа; IEEE754 определяет некоторые особые случаи, смещения и т. Д.

Я полагаю, что должно быть очень простое решение.

Инженерный опыт подсказывает мне: предложения, заканчивающиеся на «простое решение», обычно не соответствуют действительности.

"академические кейсы"

однако это определенно неправда (я приведу пример в конце).

Оптимизация поплавков IEEE754 в реальном мире очень надежна. Однако я считаю, что с учетом возможностей более поздних процессоров x86 выполнять SIMD (одна инструкция, несколько данных) и общего факта, что с плавающей запятой работает так же быстро, как и большинство «битовых» операций, я обычно подозреваю, что вам не рекомендуется использовать попробуйте сделать это самостоятельно.

Как правило, поскольку IEEE754 является стандартом, вы везде найдете документацию о том, как он хранится в вашей конкретной архитектуре. Если вы посмотрели, вы, по крайней мере, должны были найти статью в Википедии, объясняющую, как сделать 1) и 2) (это не так статично, как вы думаете).

Что еще важнее: не пытайтесь быть умнее своего компилятора. Вероятно, вы этого не сделаете, если вы явно не знаете, как векторизовать несколько идентичных операций.

Поэкспериментируйте с математической оптимизацией вашего конкретного компилятора. Как уже упоминалось, в настоящее время они обычно мало что делают; Процессоры обязательно не медленнее выполняют вычисления с плавающей запятой, чем с целыми числами.

Я лучше посмотрю на ваши алгоритмы и поищу там потенциал для оптимизации.

Кроме того, пока я занимаюсь этим, давайте представим VOLK (Vector Optimized Library of Kernels), которая в основном представляет собой математическую библиотеку для обработки сигналов. На http://libvolk.org есть обзор. Посмотрите на ядра, которые начинаются с 32f, например 32f_expfast. Вы заметите, что существуют разные реализации, общие и оптимизированные для ЦП, разные для каждого набора инструкций SIMD.

person Marcus Müller    schedule 09.01.2016
comment
Я согласен с аргументом, что компилятор c ++, вероятно, умнее меня, однако вот простой пример быстрого логирования alg: int log2_evil (double d) {return ((reinterpret_cast ‹unsigned long long &› (d) ›› 52) & 0x7ff ) - 1023; } Это занимает примерно в 11 раз меньше вычислительных затрат, чем реализация стандартного журнала (если у вас нет требований к высокой точности). Я просто не уверен, как именно работает упомянутый алогритм. Я бы хотел избежать неожиданного поведения (не только для хорошо известного журнала, но и для моих сложных вычислений), поэтому это причина моих 4 вопросов. Кстати, спасибо за ссылку на VOLK! - person Marek Basovník; 10.01.2016
comment
@ MarekBasovník: попробуйте использовать алгоритм VOLK. Например, volk_32f_log2_32f.h в реализации SSE4.1 занимает 118 мс для количества вычислений, которое на моем ПК потребовалось для общего _2 _ / _ 3_ 2916 мс. Это немного более впечатляюще, чем ваше скучное 11-кратное ускорение, потому что его точность намного выше. Хотя я думаю, что только что увидел ошибку производительности в общей реализации. - person Marcus Müller; 10.01.2016

Вы можете скопировать адрес значения fp в unsigned char* и рассматривать полученный указатель как адрес массива, который перекрывает значение fp.

person Pete Becker    schedule 09.01.2016
comment
Это приводит к неопределенному поведению. - person Phil Miller; 09.01.2016
comment
Тем не менее, есть способ сделать то же самое, чтобы избежать нарушения правил набора текста в стандарте: blog .regehr.org / archives / 959 - person Phil Miller; 09.01.2016
comment
@Novelocrat проблема заключается в строгом псевдониме, который здесь не применяется. char * и unsigned char * - исключения, которым разрешено использовать псевдоним любого другого типа, в частности, для проверки представления объекта. Это лучший подход, если вам действительно нужно делать эти низкоуровневые хаки. en.cppreference.com/w/cpp/language/reinterpret_cast имеет раздел о правилах алиасинга. - person user1942027; 09.01.2016
comment
@RaphaelAddile По-прежнему неопределенное поведение. Машина может быть с прямым порядком байтов, прямым порядком байтов или смешанным порядком байтов. - person MarkWeston; 19.12.2017
comment
@MarkWeston Не определено. Порядок байтов определяется реализацией. Это означает, что мы не призываем демонов, используя его. Нам просто нужно посмотреть документацию по реализации (или протестировать порядок байтов программно, что легко). - person user1942027; 19.12.2017

В C или C ++, если x - это IEEE double, то если 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, denormal, inf или NaN, тогда к последнему выражению следует добавить 0x0010000000000000, чтобы учесть неявный бит 1 высокого порядка в двойном формате IEEE.)

Переупаковка знака, экспоненты и дроби обратно в двойную аналогична:

L = (s ‹< 63) + ((e + 0x3FF) ‹< 52) + (f & 0x000FFFFFFFFFFFFF);

х = * ((двойной *) & L);

Приведенный выше код генерирует только несколько машинных инструкций без вызовов подпрограмм на 64-битных машинах, скомпилированных с 64-битным кодом. В 32-битном коде иногда требуется выполнить 64-битную арифметику, но хороший компилятор обычно генерирует встроенный код. В любом случае это очень быстрый подход.

Аналогичный подход работает для C # с использованием L = bitConverter.DoubleToInt64Bits(x); и x = BitConverter.Int64BitsToDouble(L); или точно так же, как указано выше, если небезопасный код разрешен.

person user113670    schedule 30.01.2016