Соответствие стандарту IEEE-754 от половины до четности
Стандартная библиотека C обеспечивает round
, lround
, а также llround
семейство функций в C99. Однако эти функции не соответствуют стандарту IEEE-754, так как они не реализуют "округление банкира" от полугода до четного, как того требует IEEE. Полу-четное округление требует округления результата до ближайшего четного значения, если дробный компонент равен точно 0,5. Стандарт C99 вместо этого предписывает половину от нуля, как отмечено на cppreference.com
1-3) Вычисляет ближайшее целочисленное значение к arg (в формате с плавающей запятой), округляя полпути до нуля, независимо от текущего режима округления.
Обычным специальным способом реализации округления в C является выражение (int)(x + 0.5f)
которая, несмотря на неправильность в строгой математике IEEE-754, обычно переводится компиляторами в правильную cvtss2si
инструкция. Однако это, безусловно, не переносимое предположение.
Как я могу реализовать функцию, которая будет округлять любое значение с плавающей запятой с семантикой половин к четности? Если возможно, функция должна полагаться только на семантику языка и стандартной библиотеки, чтобы она могла работать с типами с плавающей запятой не-IEEE. Если это невозможно, ответ, определенный в терминах битовых представлений IEEE-754, также является приемлемым. Пожалуйста, охарактеризуйте любые константы с точки зрения <limits.h>
или же <limits>
,
5 ответов
Стандартная библиотека C обеспечивает
round
,lround
, а такжеllround
семейство функций в C99. Однако эти функции не соответствуют стандарту IEEE-754, потому что они не реализуют "округление банкира" от полупериодов до уровня, предписанного IEEE...
Нет смысла говорить о том, является ли отдельная функция "совместимой с IEEE-754". Соответствие IEEE-754 требует, чтобы был доступен набор операций типов данных с определенной семантикой. Он не требует, чтобы эти типы или операции имели конкретные имена, и при этом не требуется, чтобы были доступны только эти операции. Реализация может предоставлять любые дополнительные функции, которые она хочет, и при этом быть совместимой. Если реализация хочет обеспечить округление до нечетности, округление в случайном порядке, округление от нуля и прерывание, если неточное, она может сделать это.
Что IEEE-754 фактически требует для округления, так это следующие шесть операций:
convertToIntegerTiesToEven (x)
convertToIntegerTowardZero (x)
convertToIntegerTowardPositive (x)
convertToIntegerTowardNegative (x)
convertToIntegerTiesToAway (x)
convertToIntegerExact (x)
В C и C++ последние пять из этих операций связаны с trunc
, ceil
, floor
, round
, а также rint
функции соответственно. C11 и C++14 не имеют привязки для первого, но будущие ревизии будут использовать roundeven
, Как вы видете, round
на самом деле является одной из обязательных операций.
Тем не мение, roundeven
недоступен в текущих реализациях, что приводит нас к следующей части вашего вопроса:
Обычным специальным способом реализации округления в C является выражение
(int)(x + 0.5f)
которая, несмотря на неправильность в строгой математике IEEE-754, обычно переводится компиляторами в правильнуюcvtss2si
инструкция. Однако это, безусловно, не переносимое предположение.
Проблемы с этим выражением выходят далеко за рамки "строгой математики IEEE-754". Это совершенно неверно для отрицательного x
, дает неправильный ответ для nextDown(0.5)
и превращает все нечетные целые числа в бинаде 2**23 в четные целые числа. Любой компилятор, который переводит его в cvtss2si
ужасно, ужасно сломан. Если у вас есть пример этого, я бы хотел это увидеть.
Как я могу реализовать функцию, которая будет округлять любое значение с плавающей запятой с семантикой половин к четности?
Как отметил njuffa в комментарии, вы можете убедиться, что режим округления по умолчанию установлен и используется rint
(или же lrint
, так как это звучит так, как будто вы действительно хотите получить целочисленный результат), или вы можете реализовать свою собственную функцию округления, вызвав round
и затем исправление промежуточных случаев, как предлагает gnasher729. Как только привязки n1778 для C будут приняты, вы сможете использовать roundeven
или же fromfp
функции для выполнения этой операции без необходимости управления режимом округления.
Использование remainder(double x, 1.0)
из стандартной библиотеки C. Это не зависит от текущего режима округления.
Остальные функции вычисляют остаток x REM y, требуемый IEC 60559
remainder()
здесь полезно, так как отвечает требованиям OP к четным требованиям.
double round_to_nearest_ties_to_even(double x) {
x -= remainder(x, 1.0);
return x;
}
Тестовый код
void rtest(double x) {
double round_half_to_even = round_to_nearest_ties_to_even(x);
printf("x:%25.17le z:%25.17le \n", x, round_half_to_even);
}
void rtest3(double x) {
rtest(nextafter(x, -1.0/0.0));
rtest(x);
rtest(nextafter(x, +1.0/0.0));
}
int main(void) {
rtest3(-DBL_MAX);
rtest3(-2.0);
rtest3(-1.5);
rtest3(-1.0);
rtest3(-0.5);
rtest(nextafter(-0.0, -DBL_MAX));
rtest(-0.0);
rtest(0.0);
rtest(nextafter(0.0, +DBL_MAX));
rtest3(0.5);
rtest3(1.0);
rtest3(1.5);
rtest3(2.0);
rtest3(DBL_MAX);
rtest3(0.0/0.0);
return 0;
}
Выход
x: -inf z: -inf
x:-1.79769313486231571e+308 z:-1.79769313486231571e+308
x:-1.79769313486231551e+308 z:-1.79769313486231551e+308
x: -2.00000000000000044e+00 z: -2.00000000000000000e+00
x: -2.00000000000000000e+00 z: -2.00000000000000000e+00
x: -1.99999999999999978e+00 z: -2.00000000000000000e+00
x: -1.50000000000000022e+00 z: -2.00000000000000000e+00
x: -1.50000000000000000e+00 z: -2.00000000000000000e+00 tie to even
x: -1.49999999999999978e+00 z: -1.00000000000000000e+00
x: -1.00000000000000022e+00 z: -1.00000000000000000e+00
x: -1.00000000000000000e+00 z: -1.00000000000000000e+00
x: -9.99999999999999889e-01 z: -1.00000000000000000e+00
x: -5.00000000000000111e-01 z: -1.00000000000000000e+00
x: -5.00000000000000000e-01 z: 0.00000000000000000e+00 tie to even
x: -4.99999999999999944e-01 z: 0.00000000000000000e+00
x:-4.94065645841246544e-324 z: 0.00000000000000000e+00
x: -0.00000000000000000e+00 z: 0.00000000000000000e+00
x: 0.00000000000000000e+00 z: 0.00000000000000000e+00
x: 4.94065645841246544e-324 z: 0.00000000000000000e+00
x: 4.99999999999999944e-01 z: 0.00000000000000000e+00
x: 5.00000000000000000e-01 z: 0.00000000000000000e+00 tie to even
x: 5.00000000000000111e-01 z: 1.00000000000000000e+00
x: 9.99999999999999889e-01 z: 1.00000000000000000e+00
x: 1.00000000000000000e+00 z: 1.00000000000000000e+00
x: 1.00000000000000022e+00 z: 1.00000000000000000e+00
x: 1.49999999999999978e+00 z: 1.00000000000000000e+00
x: 1.50000000000000000e+00 z: 2.00000000000000000e+00 tie to even
x: 1.50000000000000022e+00 z: 2.00000000000000000e+00
x: 1.99999999999999978e+00 z: 2.00000000000000000e+00
x: 2.00000000000000000e+00 z: 2.00000000000000000e+00
x: 2.00000000000000044e+00 z: 2.00000000000000000e+00
x: 1.79769313486231551e+308 z: 1.79769313486231551e+308
x: 1.79769313486231571e+308 z: 1.79769313486231571e+308
x: inf z: inf
x: nan z: nan
x: nan z: nan
x: nan z: nan
Округлите число x, и если разность между x и round (x) точно равна +0.5 или -0.5, а round (x) нечетный, то round (x) был округлен в неправильном направлении, поэтому вы вычитаете разницу из Икс.
float
Тип данных может представлять все целые числа, но не дроби, в диапазоне от 8388608.0f до 16777216.0f. любой float
числа, которые больше 8388607.5f, являются целыми числами, и округление не требуется. Добавление 8388608.0f к любому неотрицательному float
который меньше этого, даст целое число, которое будет округлено в соответствии с текущим режимом округления (обычно с округлением от половины до четности). Вычитание 8388608.0f приведет к получению правильно округленной версии оригинала (при условии, что он находится в подходящем диапазоне).
Таким образом, должно быть возможно сделать что-то вроде:
float round(float f)
{
if (!(f > -8388608.0f && f < 8388608.0f)) // Return true for NaN
return f;
else if (f > 0)
return (float)(f+8388608.0f)-8388608.0f;
else
return (float)(f-8388608.0f)+8388608.0f;
}
и использовать преимущества естественного округления при сложении, не прибегая к какому-либо другому "округлению до целого".
Начиная с C++11, в STL была функция, выполняющая округление до полусмерти. Если для режима округления с плавающей запятой установлено значениеFE_TONEAREST
(по умолчанию), затем std::nearbyint
сделает свое дело.
Ниже приведена простая реализация программы округления от половины до четности, которая соответствует стандарту округления IEEE.
Логика: ошибка = 0,00001
- число = 2,5
- темп = этаж (2,5)%2 = 2%2 = 0
- х = -1 + темп = -1
- х * ошибка + номер = 2,40009
- раунд (2.40009) = 2
Примечание: ошибка здесь равна 0,00001, т. Е. Если произойдет 2,500001, то она округляется до 2 вместо 3
Реализация Python 2.7:
temp = (number)
rounded_number = int( round(-1+ temp%2)*0.00001 + temp )
Реализация на C++: (используйте math.h для функции пола)
float temp = (number)
int rounded_number = (int)( (-1+ temp%2)*0.00001 + temp + 0.5)
Результат, который это дало бы, был бы следующим согласно. стандартам:
(3.5) -> 4
(2.5) -> 2
Редактировать 1: Как указано @Mark Dickinson в комментариях. Ошибка может быть изменена в соответствии с требованиями вашего кода для ее стандартизации. Для python, чтобы превратить его в наименьшее возможное значение с плавающей запятой, вы можете сделать следующее.
import sys
error = sys.float_info.min