Существует ли такое понятие, как чрезмерная точность?
Так, хватит пустой болтовни, возвращаемся к проектированию языков программирования.
Предположим, вы разрабатываете программное обеспечения для электронного пианино. Как мы уже обсуждали ранее, настройка «равномерно темперированного строя» пианино выглядит так: 49-я клавиша слева стандартного 88 клавишного пианино настроена на «ля», с частотой 440 Гц. Увеличение октавы на единицу – удваивает частоту, а уменьшение на единицу – уменьшает частоту вдвое. Почему? Потому что человек воспринимает отношение между двумя частотами в виде относительной, а не абсолютной величины. Октава состоит из 12 полутонов или хроматических ступеней, поэтому для их равномерного распределения частота следующего тона рассчитывается, как частота предыдущего, умноженная на корень двенадцатой степени из двух.
Проще говоря, частота n-й клавиши равна 440 x 2 (n-49) / 12
Сегодня с реальными пианино все немного сложнее. С высокими нотами существует интересная проблема. Для длинных струн отношения диаметра струны к ее длине очень мало; на практике это значение можно считать равным нулю. Но для более коротких струн это отношения нельзя считать равным нулю. Наличие у струн толщины приводит к более низким гармоническим колебаниям, чем они должны быть. Это понятие называется «дисгармоничностью». Кроме того, человек склонен воспринимать высокие ноты так, будто их частота немного ниже, чем есть на самом деле. Наша способность различать относительные высоты звуков немного искажается на высоких частотах. По этим причинам, настройщики пианино «растягивают» верхние октавы, что делает основные частоты высоких нот более резкими, чем это требует равномерная темперация. Очевидно, у электронных пианино отсутствует дисгармоничность и мы в нашей задаче можем игнорировать физикоакустические проблемы, поскольку я собираюсь говорить о вычислениях с плавающей запятой.
Предположим, вы говорите: «приведенное выше выражение эквивалентно следующему: 440 x R(n-49), где R – корень двенадцатой степени из двух. Вы вычисляете корень двенадцатой степени из двух с помощью calc.exe и пишите следующий код:
static double Frequency(int key)
{
const double TwelfthRootOfTwo = 1.0594630943592952645618252949463;
const double A4Frequency = 440.0;
return A4Frequency * Math.Pow(TwelfthRootOfTwo, key-49);
}
Теперь, с кодом все в порядке, он работает и его можно оставить в покое. В него можно добавить проверку параметра, чтобы гарантировать значение, равное от 1 до 88, но, это не важно. Но не беспокоит ли вас точность константы? Ее длина почти на двадцать знаков превосходит допустимую точность double! Точность double ограничена примерно 14-15-ю знаками.
Мы можем сделать вывод, что calc.exe выполняет вычисления не в double, что, как мне кажется, весьма интересно. Вместо этого, он должен использовать значительно более точный внутренний формат.
Еще нужно отметить, что для этого приложения даже 15 знаков точности более чем достаточно. Самое большое значение, которое мы можем получить не превышает 4200 Гц. Ошибка округления в пятнадцать знаков будет слишком мала, чтобы человек почувствовал разницу. Если вы разделите октаву на 1200 интервалов, вместо 12, то получите интервал называемый центом (100 центов составляет полутон). Достаточная точность составляет несколько центов, поскольку большинство людей не почувствуют такую разницу.
Но если компилятор просто отбросит всю эту точность, тогда эта часть программы станет бессмысленной. Должен ли компилятор выдавать предупреждение о том, что он выбрасывают указанную вами точность?
Нет, не совсем. И вот почему.
Предположим, вы написали
const double d = 108.595;
Мы хотим представить точное математическое значение 108595/1000 в виде double. Но double использует двоичное представление: нам нужно представить дробную часть в виде степени двойки, а не степени десяти. Самое близкое, что мы можем получить, используя 52 разряда точности, следующее: 1101100.1001100001010001111010111000010100011110101110 или, если вы предпочитаете дроби: 3820846886986711 / 35184372088832. Точное значение этой дроби в десятичном виде: 108.594999999999998863131622783839702606201171875, что, следует признать, является чрезвычайно близким к значению 108.595, хотя и немного меньшим.
Вот такая штука получается. Вы хотите представить точное значение числа 108.595, но мы вам этого не позволяем. Мы заставляем вас внести некоторую ошибку, о которой даже не предупреждаем, поскольку считаем, что вы знаете об этом. (Если вам нужен точный результат, вам следует использовать decimal, а не double.) Но что, если бы вы захотели получить в точности такое значение: 108.594999999999998863131622783839702606201171875? Оно может быть совершенно точно представлено в двоичном виде! Поэтому, нам не следует выдавать предупреждение о возможной потере точности, если вы напишите следующее:
const double d = 108.594999999999998863131622783839702606201171875;
поскольку вы получите нужную точность в случае отсутствия ошибок представления. Глупо говорить: «пожалуйста, округлите это значение до пятнадцати знаков, чтобы мы смогли вернуть его обратно без потери точности». Почему мы должны заставлять вас быть более точными?
Лучшее, что мы могли бы сделать – это выдавать предупреждение, если точность превосходит возможности представления, *и* ошибка представления не равна нулю, *и* потеря дополнительной точности уменьшает ошибку представления, *и*… теперь мы напишем кучу сложного математического кода, после чего пользователи будут рвать волосы на голове и говорить: «каким, черт его дери, способом мне угодить компилятору с этой константой?» Предупреждения, которые становятся всего лишь источником раздражений для пользователя, являются неудачными, и мы стараемся их избегать.