2016-01-09 16 views
2

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

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

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

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

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

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

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

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

+2

Взгляните на IEE754 в Интернете, все подробно объяснено. Я серьезно сомневаюсь, что вам действительно нужна такая вещь ... –

+2

Существует очень простое, но не переносное решение. Если я могу так сказать, вы будете делать странные вещи. Я бы туда не поехал. Напишите эффективный код с 'float' /' double', пусть компилятор и FPU сделают все остальное. –

+0

Используйте бит twiddling –

ответ

6

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

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

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

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

"академические дела"

однако, безусловно, не соответствует действительности (я упомяну пример в конце).

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

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

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

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

Я бы предпочел посмотреть на ваши алгоритмы и искать потенциал для оптимизации там.

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

+0

Я согласен с аргументом, что компилятор C++, вероятно, умный, чем я. Однако вот простой пример быстрого журнала: int log2_evil (double d) { return ((reinterpret_cast (d) >> 52) & 0x7ff) - 1023; } Это занимает примерно в 11 раз меньше вычислительных затрат, чем стандартная реализация журнала (если у вас нет высокой точности). Я просто не уверен, как упомянутый alogritm работает точно. Я бы хотел избежать неожиданного поведения (не только для известного журнала, но и для сложных расчетов моего), так что это причина моих 4 вопросов. Кстати, спасибо за вашу ссылку на VOLK! –

+1

@ MarekBasovník: попробуйте использовать алгоритм VOLK. Например, 'volk_32f_log2_32f.h' в реализации SSE4.1 занимает 118 мс для количества вычислений, которые генерировал общий файл' math.h'/'log2()' 2916ms для моего ПК. Это немного более впечатляет, чем ваш «скучный» 11-кратный ускорение, потому что его точность намного выше. Хотя я думаю, что я просто видел ошибку производительности в общей реализации. –

1

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

+0

Это приводит к неопределенному поведению. – Novelocrat

+0

Существует способ сделать то же самое, что позволяет избежать падения правил использования символов в стандарте, хотя: http://blog.regehr.org/archives/959 – Novelocrat

+2

@Novelocrat проблема заключается в строгом псевдониме, примените здесь. 'char *' и 'unsigned char *' - это исключения, которые разрешены для псевдонимов любого другого типа, в частности для проверки представления объекта. Это лучшие подходы, если вам действительно нужно делать эти хаки низкого уровня. http://en.cppreference.com/w/cpp/language/reinterpret_cast содержит раздел о правилах псевдонимов. –

0

В 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, denormal, инф, ни NaN, то последнее выражение должно быть 0x0010000000000000 добавили к нему аль . Низкий для неявных высоких порядка 1 бит в IEEE двойного формата)

повторная упаковка знака, показатель степени и фракцию обратно в двойной аналогичен:

L = (ы < < 63) + ((е + 0x3FF) < < 52) + (f & 0x000FFFFFFFFFFFFF);

x = * ((double *) & L);

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

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