Почему приведение double к int может дать разные результаты? - PullRequest
1 голос
/ 16 мая 2019

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

const char* testString = "99.85";
double percent = atof(testString);
double hundred = 100;
uint16_t reInt1 = (uint16_t)(hundred * percent);
double stagedDouble = hundred * percent;
uint16_t reInt2 = (uint16_t)stagedDouble;

Пример вывода:

percent:      99.850000 
stagedDouble: 9985.000000
reInt1:       9984
reInt2:       9985

Ошибка видна примерно в 47% всех значений от 0 до 10000 (в представлении с фиксированной точкой). Он не появляется вообще при касте с stagedDouble. И я не понимаю, почему два целых числа разные. Я использую GCC 6.3.0.

Edit: Улучшен фрагмент кода для демонстрации переменной percent и унификации коэффициента между двумя операторами. Изменение 100 в double выглядит как изменение качества, которое может повлиять на вывод, но это ничего не меняет в моей программе.

Ответы [ 2 ]

2 голосов
/ 16 мая 2019

Сообщаемое поведение согласуется с объявлением percent float и использованием базовых 32-разрядных и 64-разрядных двоичных чисел с плавающей точкой IEEE-754 для float и double.

uint16_t reInt1 = (uint16_t)(100.0 * percent);

Поскольку 100.0 является константой double, это преобразует percent в double, выполняет умножение в double и преобразует результат в uint16_t. Умножение может иметь очень небольшую ошибку округления, вплоть до ½ ULP двойного формата, относительную ошибку около 2 -53 .

double stagedDouble = 100 * percent;
uint16_t reInt2 = (uint16_t)stagedDouble;

Поскольку 100 является константой int, это преобразует 100 в float, выполняет умножение в float и преобразует результат в uint16_t. Ошибка округления при умножении может составлять до 1/2 ULP формата float, относительная ошибка около 2 -24 .

Поскольку все значения составляют около сотых целого числа, соотношение ошибок 50:50 вверх: вниз будет составлять примерно половину результатов, чуть меньше того, что необходимо для целочисленного порога. В умножениях все те, чьи значения равны 0, 25, 50 или 100 сотых, будут точными (потому что 25/100 равно ¼, что точно представлено в двоичной переменной с плавающей запятой), поэтому 96/100 будет иметь округление ошибки. Если направления ошибок округления float и double ведут себя как независимые, однородные случайные величины, примерно половина будет округляться в разных направлениях, приводя к разным результатам, давая примерно 48% несовпадений, что согласуется с 47%, сообщенными в вопрос.

(Однако, когда я измеряю фактические результаты, я получаю 42% -ную разницу между методами float и double. Я подозреваю, что это имеет какое-то отношение к конечным битам в умножении float перед округлением - распределение может не действовать как равномерное распределение двух возможностей. Это может быть то, что код OP готовит значения percent не так, как деление целочисленного значения на 100).

2 голосов
/ 16 мая 2019

Является ли percent поплавком? Если так, посмотрите, какие типы вы умножаете.

reInt1 равно double * float, а stagedDouble равно int * float. Перепутывание математики с плавающей запятой может привести к ошибкам округления этих типов.

Изменение 100 на оба double или оба int приводит к одному и тому же ответу.

...