Неожиданное поведение Math.Floor(double) и Math.Ceiling(double)

Этот вопрос касается порога, при котором Math.Floor(double) и Math.Ceiling(double) решают дать вам предыдущее или следующее целочисленное значение. Меня беспокоило, что порог, похоже, не имеет ничего общего с Double.Epsilon, который является наименьшим значением, которое может быть представлено двойным. Например:

double x = 3.0;
Console.WriteLine( Math.Floor( x - Double.Epsilon ) );  // expected 2, got 3
Console.WriteLine( Math.Ceiling( x + Double.Epsilon) ); // expected 4, got 3

Даже умножение Double.Epsilon на справедливый бит не помогло:

Console.WriteLine( Math.Floor( x - Double.Epsilon*1000 ) );  // expected 2, got 3
Console.WriteLine( Math.Ceiling( x + Double.Epsilon*1000) ); // expected 4, got 3

С некоторыми экспериментами я смог определить, что порог где-то около 2.2E-16, что очень мало, но VASTLY больше, чем Double.Epsilon.

Причина этого вопроса в том, что я пытался вычислить число цифр в числе с помощью формулы var digits = Math.Floor( Math.Log( n, 10 ) ) + 1. Эта формула не работает для n=1000 (которую я случайно наткнулся случайно), потому что Math.Log( 1000, 10 ) возвращает число, равное 4.44E-16 от его фактического значения. (Позже я обнаружил, что встроенный Math.Log10(double) обеспечивает гораздо более точные результаты.)

Если порог не должен быть привязан к Double.Epsilon или, если нет, не должен быть документирован порог (я не мог найти упоминания об этом в официальной документации MSDN)?

Ответ 1

Если порог не должен быть привязан к Double.Epsilon

Нет.

Представимые двойники неравномерно распределены по действительным числам. Ближе к нулю существует множество отображаемых значений. Но чем дальше от нуля вы получаете, тем дальше друг от друга представляются двойники. Для очень больших чисел даже добавление 1 к двойнику не даст вам нового значения.

Поэтому порог, который вы ищете, зависит от того, насколько велика ваша цифра. Это не константа.

Ответ 2

Значение Double.Epsilon равно 4.94065645841247e-324. Добавление или вычитание этого значения до 3 результатов в 3, из-за того, как работает плавающая точка.

A double имеет 53 бит мантиссы, поэтому наименьшее значение, которое вы можете добавить, которое будет иметь какое-либо влияние, будет примерно на 2 ^ 53 раз меньше вашей переменной. Так что вокруг примерно 1е-16 звучит примерно справа (порядок величины).

Итак, чтобы ответить на ваш вопрос: нет "порога"; floor и ceil просто действуют на свой аргумент точно так, как вы ожидали.

Ответ 3

Это будет размахивать руками, а не ссылками на технические характеристики, но я надеюсь, что мое "интуитивное объяснение" вам подходит.

Epsilon представляет наименьшую величину, которая может быть представлена, отличная от нуля. Учитывая мантиссою и показателем двойника, это будет крайне крошечным, - подумайте 10 ^ -324. Там находится более трехсот нулей между десятичной точкой и первой ненулевой цифрой.

Однако Double представляет примерно 14-15 цифр точности. Это все еще оставляет 310 цифр нулей между Epsilon и целыми числами.

Double фиксируются на определенную длину бита. Если вам действительно нужны произвольные вычисления точности, вместо этого вы должны использовать библиотеку с произвольной точностью. И будьте готовы к тому, чтобы он был значительно медленнее - для представления всех 325 цифр, которые необходимы для хранения числа, такого как 2+epsilon, потребуется примерно в 75 раз больше места на диске. Это хранилище не является бесплатным, и вычисление с ним, конечно, не может идти с полной скоростью процессора.