Извлечь дробную часть двойного * эффективно * в C
Я хочу взять двойной IEEE и удалить любую его целую часть наиболее эффективным способом.
Я хочу
1035 ->0
1045.23->0.23
253e-23=253e-23
Мне не важно правильно обрабатывать денормалы, бесконечности или NaN. Я не возражаю против того, чтобы немного крутить, поскольку я знаю, что я работаю с удвоением IEEE, поэтому он должен работать на разных машинах.
Непрерывный код будет очень предпочтительным.
Моя первая мысль (в псевдокоде)
char exp=d.exponent;
(set the last bit of the exponent to 1)
d<<=exp*(exp>0);
(& mask the last 52 bits of d)
(shift d left until the last bit of the exponent is zero, decrementing exp each time)
d.exponent=exp;
Но проблема в том, что я не могу придумать эффективный способ сдвинуть d до тех пор, пока последний бит экспоненты не будет равен нулю, плюс, похоже, что ему нужно будет вывести нуль, если все последние биты не были установлены, Это, по-видимому, связано с проблемой логарифма базы 2.
Помощь с этим алгоритмом или любыми лучшими будет оценена.
Я должен, наверное, отметить, что причина, по которой я хочу разветвленный код, заключается в том, что я хочу, чтобы он эффективно векторизовал.
Ответы
Ответ 1
Как насчет чего-то простого?
double fraction = whole - ((long)whole);
Это просто вычитает целочисленную часть двойника из самого значения, остаток должен быть дробным компонентом. Разумеется, это может иметь некоторые проблемы с представлением.
Ответ 2
Оптимальная реализация полностью зависит от целевой архитектуры.
На последних процессорах Intel это может быть достигнуто с помощью двух команд: roundsd
и subsd
, но это не может быть выражено в переносном коде C.
На некоторых процессорах самый быстрый способ сделать это - это целые операции над представлением с плавающей запятой. Ранний Atom и многие процессоры ARM приходят на ум.
На некоторых других процессорах самым быстрым является приведение к целому и обратно, а затем вычитание, ветвление для защиты больших значений.
Если вы собираетесь обрабатывать множество значений, вы можете установить режим округления в округло к нулю, затем добавить и вычесть +/- 2 ^ 52 на число, усеченное на целое, а затем вычесть из оригинала значение для получения доли. Если у вас нет SSE4.1, но у вас есть современный процессор Intel и вы хотите его векторизовать, это, как правило, лучшее, что вы можете сделать. Это имеет смысл только в том случае, если у вас есть много значений для обработки, потому что изменение режима округления несколько дороже.
В других архитектурах оптимальны другие реализации. В общем, не имеет смысла говорить о "эффективности" программ на С; только эффективность конкретной реализации по конкретной архитектуре.
Ответ 3
#include <math.h>
double fraction = fmod(d, 1.0);
Ответ 4
Предложение
Функция remainder
вычисляет остаток, но не целую часть, такую как modf
:
#include <math.h>
double fracpart(double input)
{
return remainder(input, 1.);
}
Это самый эффективный (и переносимый) способ, поскольку он не вычисляет ненужные значения для выполнения задания (см. modf
, (long)
, fmod
и т.д.)
Benchmark
Как предложил Mattew в комментариях, я написал код для сравнения этого решения со всеми другими, предлагаемыми на этой странице.
Ниже приведены измерения времени для вычислений 65536 (скомпилированные с Clang с отключенными оптимизациями):
method 1 took 0.002389 seconds (using remainder)
method 2 took 0.000193 seconds (casting to long)
method 3 took 0.000209 seconds (using floor)
method 4 took 0.000257 seconds (using modf)
method 5 took 0.010178 seconds (using fmod)
Опять же с Clang, на этот раз с использованием флага -O3
:
method 1 took 0.002222 seconds (using remainder)
method 2 took 0.000000 seconds (casting to long)
method 3 took 0.000000 seconds (using floor)
method 4 took 0.000223 seconds (using modf)
method 5 took 0.010131 seconds (using fmod)
Похоже, что самое простое решение дает наилучшие результаты на большинстве платформ, а конкретные методы для выполнения этой задачи (fmod
, modf
, remainder
) на самом деле супер-медленны!
Ответ 5
Стандартная функция библиотеки modf решает эту проблему довольно аккуратно.
#include <math.h>
/*...*/
double somenumber;
double integralPart;
double fractionalPart = modf(somenumber, &integralPart);
Это должно делать то, что вы просили, является портативным и достаточно эффективным.
Недокументированная деталь заключается в том, может ли второй аргумент быть NULL, а затем избегать неотъемлемой части временной, что было бы желательно при использовании, описанном вами.
К сожалению, многие реализации не поддерживают NULL для второго аргумента, поэтому придется использовать временное значение, использующее это значение.
Ответ 6
Некоторые профилирование и экспериментирование с использованием С++ в Microsoft Visual Studio 2015 показывают, что лучший метод для положительных чисел:
double n;
// ...
double fractional_part = n - floor(n);
Это быстрее, чем modf
, и, как уже упоминалось, функция остатка округляется до ближайшего целого числа и поэтому не используется.