Безвременно как бесконечность

Пользователь: Недавно я обнаружил в C# странное поведение относительно деления на ноль чисел с плавающей запятой. Оно не бросает исключение, как целочисленное деление, а возвращает «бесконечность». С чего бы это?

Эрик: Как я частенько говорил, мне трудно отвечать на вопросы «почему». Обычно моя первая попытка ответить на вопрос «почему» - «потому, что так гласит спецификация»; этот случай не исключение. Спецификация С# в секции 4.1.6 требует делать именно так. Но мы это делаем только потому, что так предписывает стандарт IEEE по арифметике с плавающей запятой. Мы хотим быть совместимыми с признанным промышленным стандартом. Подробности есть в стандарте IEEE №754-1985. Большая часть арифметики с плавающей запятой в наше время делается аппаратно, и большая часть железа совместима с этой спецификацией.

Пользователь: А мне кажется, что деление на ноль – это баг, как на него ни посмотри!

Эрик: Ну, поскольку это очевидно не совпадает с тем, как на это смотрели члены комитета по стандартизации IEEE в 1985 году, ваше утверждение, что это должно быть багом «как на него не посмотри», должно быть, некорректно.

Пользователь: Хороший аргумент. А что послужило причиной такого решения?

Эрик: Меня там не было; я в тот момент был занят игрой в Jumpman-а на моём Commodore 64. Но могу обоснованно предположить, что желательно, чтобы все возможные операции над всеми плавающими числами возвращали строго определённый плавающий результат.

Математики назвали бы это свойством «замкнутости»; то есть, множество чисел с плавающей запятой «замкнуто» относительно всех операций.

Положительная бесконечность выглядит разумным выбором для деления положительного числа на ноль. Она выглядит убедительно потому, конечно же, что предел 1 / x при x, стремящемся к нулю (сверху) - это «положительная бесконечность», так почему бы не положить 1/0 равным числу «положительная бесконечность»?

Вообще-то, рассуждая как математик, я нахожу этот аргумент фальшивым. Нечто и предел этого нечто не обязаны иметь каких-то общих свойств; неправомерно рассуждать, что только потому, что, например, у последовательности есть некоторый предел, то факт о пределе является фактом о последовательности. Математически, «положительная бесконечность» (в смысле предела вещественнозначной функции; давайте оставим трансфинитные ординалы, гиперболическую геометрию, и весь этот прочий хлам за пределами этой дискуссии) вообще не число, и не должна трактоватьс я как таковое; скорее, это краткий способ сказать «предела не существует, поскольку последовательность расходится вверх».

Когда мы делим на ноль, мы, в сущности, говорим «реши нам уравнение x * 0 = 1»; ответ на эту задачу не «положительная бесконечность», а «я не могу, потому что решения этого уравнения не существует». Это то же самое, что и просить решить уравнение «x + 1 = x» - сказать, что «x равен плюс бесконечности» не решение; решения вовсе нет.

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

Пользователь: Но ведь в железе совершенно невозможно представить «бесконечность».

Эрик: Это определённо возможно. У вас есть 32 бита в плавающем числе одинарной точности; это более четырёх миллиардов возможных чисел. Все битовые последовательности вида

?11111111???????????????????????

Зарезервированы для «нечисловых» значений. Это больше шестнадцати миллионов возможных NaN-комбинаций. Две из этих шестнадцати миллионов возможных NaN-последовательностей зарезервированы для обозначения положительной и отрицательной бесконечностей. Положительная бесконечность задаётся последовательностью 01111111100000000000000000000000, а отрицательная – 11111111100000000000000000000000.

Пользователь: Все ли языки и приложения используют это соглашение деление-на-ноль-даёт-бесконечность?

Эрик: Нет. Например, С# и Jscript используют, а VBScript – нет. VBScript даёт ошибку при попытке поделить на ноль.

Пользователь: Как тогда реализаторы языков получают нужное поведение, если эта семантика реализуется аппаратно?

Эрик: Есть две основных техники. Во-первых, многие чипы, реализующие этот стандарт, позволяют программисту сделать плавающее деление на ноль исключением, а не бесконечностью. У чипа 80x87, к примеру, можно использовать второй бит регистра контроля точности, чтобы определить, будет ли деление на ноль возвращать бесконечность, или бросать аппаратное исключение.

Во-вторых, если вы хотите, чтобы это было программное исключение, а не аппаратное, то можно проверять второй бит регистра статуса после каждого деления; в нём записано, произошло ли только что событие деления на ноль.

Эта стратегия используется в VBScript; после выполнения операции деления мы проверяем, не записана ли в регистре статуса операция деления на ноль; если так, то среда выполнения VBScript создаёт ошибку деления на ноль, и выполняется обычный процесс обработки ошибок VBScript, также, как и для любой другой ошибки.

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

Существование битов «аппаратных исключений» создаёт проблемы реализаторам современных языков, потому что теперь мы часто оказываемся в мире, где код, написанный на нескольких языках нескольких производителей, выполняется в одном процессе. Аппаратные управляющие биты являются «глобальным состоянием», и все мы знаем, как раздражает наличие глобального публичного состояния, по которому может топтаться произвольный код.

Например: я, возможно, неправильно помню некоторые детали, но, по-моему, элементы управления, написанные на Delphi, устанавливают бит «переполнения вызывают исключение». То есть, авторы Delphi не использовали стратегию VBScript «попробуй это, дай ему закончиться, и проверь, не установлен ли бит переполнения в регистре статуса». Вместо этого они последовали стратегии «дай железу выбросить исключение, а потом поймай его». Это крайне неудачно. Когда VBScript вызывает элемент управления, написанный на Delphi, тот переключает бит для выброса исключений, но никогда не переключает его обратно. Если, при дальнейшем выполнении скрипта, программа на VBScript натыкается на переполнение, то мы получаем необработанное аппаратное исключение, потому что бит всё еще установлен, несмотря на то, что элемента управления из Delphi уже давно нет! Я починил это при помощи сохранения состояния регистра управления перед вызовом компонента, и восстановления после возврата управления. Это неидеально, но больше мы ничего не можем сделать.

Пользователь: Весьма поучительно! Я передам эту информацию своим сослуживцам. Я бы с удовольствием увидел постинг в блоге на эту тему.

Эрик: А вот и он!