Почему этот расчет с плавающей точкой дает разные результаты на разных машинах? - PullRequest
9 голосов
/ 26 февраля 2010

У меня есть простая процедура, которая вычисляет соотношение сторон по значению с плавающей запятой. Таким образом, для значения 1.77777779 процедура возвращает строку «16: 9». Я проверил это на своей машине, и она отлично работает.

Процедура задается как:

    public string AspectRatioAsString(float f)
    {
        bool carryon = true;
        int index = 0;
        double roundedUpValue = 0;
        while (carryon)
        {
            index++;
            float upper = index * f;

            roundedUpValue = Math.Ceiling(upper);

            if (roundedUpValue - upper <= (double)0.1 || index > 20)
            {
                carryon = false;
            }
        }

        return roundedUpValue + ":" + index;
    }

Теперь на другой машине я получаю совершенно другие результаты. Таким образом, на моей машине 1.77777779 выдает «16: 9», но на другой машине я получаю «38:21».

Ответы [ 4 ]

24 голосов
/ 26 февраля 2010

Вот интересный фрагмент спецификации C # из раздела 4.1.6:

Операции с плавающей точкой могут быть выполняется с большей точностью, чем тип результата операции. За Например, некоторые аппаратные архитектуры поддерживать «расширенный» или «длинный двойной» тип с плавающей точкой с большим диапазоном и точность, чем двойной тип, и неявно выполнять все операции с плавающей точкой с использованием этого тип более высокой точности. Только в может такая перегрузка в производительности аппаратные архитектуры должны быть сделаны для выполнять операции с плавающей точкой с меньше точности, а не требуют реализации, чтобы потерять и производительность, и точность, C # позволяет более высокую точность типа используется для всех чисел с плавающей точкой операции. Кроме доставки большего точные результаты, это редко имеет какой-либо измеримые эффекты.

Возможно, это один из "измеримых эффектов" благодаря тому призыву к Потолку. Взяв верхний предел числа с плавающей запятой, как отмечали другие, увеличивает разницу в 0,000000002 на девять порядков, потому что он превращает 15.99999999 в 16, а 16.00000001 в 17. Два числа, которые немного отличаются до операции, впоследствии значительно различаются; крошечная разница может быть объяснена тем фактом, что разные машины могут иметь более или менее «дополнительную точность» в своих операциях с плавающей запятой.

Некоторые связанные вопросы:

Чтобы решить вашу конкретную проблему, касающуюся вычисления соотношения сторон из числа с плавающей запятой: я бы, возможно, решил это совершенно другим способом. Я бы сделал такой стол:

struct Ratio
{
    public int X { get; private set; }
    public int Y { get; private set; }
    public Ratio (int x, int y) : this()
    {
        this.X = x;
        this.Y = y;
    }
    public double AsDouble() { return (double)X / (double)Y; }
}

Ratio[] commonRatios = { 
   new Ratio(16, 9),
   new Ratio(4, 3), 
   // ... and so on, maybe the few hundred most common ratios here. 
   // since you are pinning results to be less than 20, there cannot possibly
   // be more than a few hundred.
};

и теперь ваша реализация

public string AspectRatioAsString(double ratio)      
{ 
    var results = from commonRatio in commonRatios
                  select new {
                      Ratio = commonRatio, 
                      Diff = Math.Abs(ratio - commonRatio.AsDouble())};

    var smallestResult = results.Min(x=>x.Diff);

    return String.Format("{0}:{1}", smallestResult.Ratio.X, smallestResult.Ratio.Y);
}

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

8 голосов
/ 26 февраля 2010

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

Можете ли вы изменить код для работы с двойной точностью? (десятичное число будет излишним). Если вы сделаете это, это даст более последовательные результаты?

Почему на разных машинах по-разному, в чем разница между двумя машинами?

  • 32 бит против 64 бит?
  • Windows 7 против Vista против XP?
  • Intel против процессора AMD? (спасибо Одед)

Примерно так может быть причиной.

5 голосов
/ 26 февраля 2010

Попробуйте Math.Round вместо Math.Ceiling. Если вы в итоге наберете 16.0000001 и округлите, вы неверно отбросите этот ответ.

Разные другие предложения:

  • Удваивается лучше, чем поплавки.
  • (double) 0.1 приведение не требуется.
  • Возможно, возникнет исключение, если вы не можете понять, каково соотношение сторон.
  • Если вы вернетесь сразу после нахождения ответа, вы можете отказаться от переменной carryon.
  • Возможно, более точной проверкой будет вычисление соотношения сторон для каждого предположения и сравнение его с входными данными.

Пересмотрено (не проверено):

public string AspectRatioAsString(double ratio)
{
    for (int height = 1; height <= 20; ++height)
    {
        int    width = (int) Math.Round(height * ratio);
        double guess = (double) width / height;

        if (Math.Abs(guess - ratio) <= 0.01)
        {
            return width + ":" + height;
        }
    }

    throw ArgumentException("Invalid aspect ratio", "ratio");
}
2 голосов
/ 26 февраля 2010

Когда индекс равен 9, вы ожидаете получить что-то вроде upper = 16.0000001 или upper = 15.9999999. Какой из них вы получите, будет зависеть от ошибки округления, которая может отличаться на разных компьютерах. Когда это 15.999999, roundedUpValue - upper <= 0.1 равно true, и цикл заканчивается. Когда это 16.0000001, roundedUpValue - upper <= 0.1 ложно, и цикл продолжает идти, пока вы не доберетесь до index > 20.

Вместо этого, возможно, вам следует попробовать округлить сверху до ближайшего целого числа и проверить, мало ли абсолютное значение его отличия от этого целого числа. Другими словами, используйте что-то вроде if (Math.Abs(Math.Round(upper) - upper) <= (double)0.0001 || index > 20)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...