Извлеките дробную часть double * эффективно* в C

Я ищу, чтобы взять IEEE double и удалить любую целочисленную часть его наиболее эффективным способом.

Я хочу

1035 ->0
1045.23->0.23
253e-23=253e-23

Я не заботятся о правильной обработке denormals, бесконечностей, или Нанс. Я не против немного крутить, так как я знаю, что работаю с IEEE Double, поэтому он должен работать на разных машинах.

Branchless код был бы гораздо предпочтительнее.

моя первая мысль (в псевдо-код)

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 влево, пока последний бит экспоненты не будет равен нулю, плюс, похоже, ему нужно будет вывести ноль, если все последние биты не были установлены. Это, по-видимому, связано с проблемой логарифма base 2.

помощь с этим алгоритмом или любые лучшие из них были бы очень признательны.

Я, вероятно, следует отметить, что причина, по которой я хочу, чтобы код внеофисному потому что я хочу, чтобы он эффективно векторизовать.

6 ответов


Как насчет чего-то простого?

double fraction = whole - ((long)whole);

Это просто вычитает целочисленную часть double из самого значения, остаток должен быть дробным компонентом. Возможно, конечно, это может иметь некоторые проблемы представительства.


оптимальная реализация полностью зависит от целевой архитектуры.

на последних процессорах Intel, это может быть достигнуто с помощью двух инструкций: roundsd и subsd, но это не может быть выражено в портативный код C.

на некоторых процессорах самый быстрый способ сделать это-с целочисленными операциями над представлением с плавающей запятой. Ранний Атом и многие процессоры ARM приходят на ум.

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

Если вы собираетесь обрабатывать много значений, вы можете установить режим округления до нуля, затем добавить и вычесть +/-2^52 в число, усеченное до целого числа, а затем вычесть из исходного значения, чтобы получить дробь. Если у вас нет SSE4.1, но есть в противном случае современный процессор Intel и хотите векторизовать, это, как правило, лучшее, что вы можете сделать. Это имеет смысл только если у вас много значений процесс, однако, потому что изменение режима округления несколько дорого.

на других архитектурах другие реализации являются оптимальными. В общем, не имеет смысла говорить об "эффективности" программ на С; только об эффективности конкретной реализации на конкретной архитектуре.


#include <math.h>
double fraction = fmod(d, 1.0);

предложение

функции remainder вычисляет остаток, но не целую часть, как modf тут:

#include <math.h>

double fracpart(double input)
{
    return remainder(input, 1.);
}

это самый эффективный (и портативный) способ, поскольку он не вычисляет ненужные значения для выполнения задания (см. modf, (long), fmod, etc.)


Benchmark

как предложил Мэттью в комментариях, я написал некоторые эталонный код сравнить это решение для всех других, предлагаемых на этой странице.

пожалуйста, найдите ниже измерения времени для 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) на самом деле очень медленно!


стандартная библиотечная функция modf решает эту проблему весьма изящно.

#include <math.h>
/*...*/
double somenumber;
double integralPart;
double fractionalPart = modf(somenumber, &integralPart);

Это должно делать то, что вы просили, это портативный и достаточно эффективен.

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

к сожалению, многие реализации не поддерживают NULL для второго аргумента, поэтому придется использовать временное использование этого значения.


некоторые профилирования и эксперименты с использованием C++ в Microsoft Visual Studio 2015 показывают, что лучший метод для положительных чисел:

double n;
// ...
double fractional_part = n - floor(n);

это быстрее, чем modf, и, как уже упоминалось, функция остатка округляется до ближайшего целого числа и поэтому не используется.