Особенности использования вещественных регистров x86 архитектуры
В этой статье рассмотрим опыт автора, столкнувшегося с особенностями реализации работы с вещественными числами на аппаратном уровне. Многие современные специалисты в области информационных технологий работают с высокими уровнями абстракции данных. Думается, что статья откроет им глаза на некоторые интересные вещи.
Давным давно на лекциях ПЯВУ (Программирование на языках высокого уровня) нам рассказали о вещественных числах. Первая информация была поверхностная. Ближе познакомился с ними уже после того, как закончил учёбу в университете, и это знакомство заставило сильно задуматься. А произошло это знакомство после того как мы в расчётах не влезли в тип данных double.
Досталась мне программа, написанная на языке C++ с использованием компилятора Borland Turbo C++. Для вычислений в ней использовался тип данных double, т.е. вещественный тип двойной точности. В определенные моменты времени программа этот самый double переполняла и успешно падала. В работе программы вычислялся факториал, а максимальный факториал, который может поместиться в double это 170! ≈ 7,3306. Вычисление факториала 171! ≈1,2309 вызвало переполнение типа данных double. Именно проблема с переполнением и привела к исследованию современного положения в вычислениях с вещественными числами. Подробнее об этом далее в статье.
Переполнение вещественных чисел двойной точности глобальная проблема, которая состоит из трех составных частей: поддержка языком программирования, поддержка компилятором и архитектурой процессора, на котором наша программа будет работать.
С языком программирования всё просто и стандартизировано. На нашем ненавистном любимом языке C++ есть три вещественных типа данных: float, double и long double, соответсвенно одинарной, двойной и больше чем двойной точности. Причем стандартом языка сказано, что «the type long double provides at least as much precision as double». То есть long double должен быть не меньше чем double. Именно этой лазейкой в стандарте разработчики компилятора Borland Turbo C++ и воспользовались, приравняв long double к double.
С архитектурой x86 тоже не всё гладко. Для простейших математических операций (сложение, вычитание, умножение, деление, сдвиги, вычисление математических функций sin, cos, и т.п.) разработчики процессоров предусматривают соответствующие регистры. Регистры условно можно разделить на те, которые работают с целочисленными числами и на те, которые работают с вещественными числами. Бывают процессорные архитектуры, в которых отсутствуют регистры для работы с вещественными числами. Например, ARMv7. В таких случаях время операций над вещественными числами возрастает на несколько порядков, так как эти операции теперь нужно эмулировать программно с использованием целочисленных регистров и операций сложения, вычитания и сдвига. Вычисление, например, тригонометрических функций программно могло замедлить вычисления на несколько порядков, так как такие функции приближенно вычисляются с использованием математических рядов.
Лирическое отступление. Именно с такой проблемой мы столкнулись на одном из проектов. Необходимо было подсчитывать количество людей, проходящих под камерой. Использовали встроенное решение с ARMv7 для обработки видео в режиме реального времени. Распознавали и считали прошедших людей. А обработка изображения — это работа с вещественными числами, которых в используемой архитектуре как раз и не было. Пришлось переходить на более продвинутое аппаратное решение, но это уже другая история. Вернемся обратно.
Широко используемая x86 архитектура до выхода процессора 80486, тоже не имела вещественных регистров. Старожилы помнят наверное такую вещь как математический сопроцессор, который устанавливался рядом с обычным процессором и имел соответствующее обозначение (8087, 80287 или 80387) и работал без активного охлаждения и даже без радиатора. Именно появление сопроцессора 8087 послужило толчком к появлению стандарта IEEE 754–1985, о нем поразмышляем позже.
Эти сопроцессоры добавляли три типа абстрактных вещественных типа данных, восемь 80-битных регистров и кучу ассемблерных команд для работы с ними. Теперь, условно, за один такт можно было вещественные числа сложить, вычесть, умножить, разделить, а также извлечь корень, посчитать тригонометрическую функцию и т.п. Ускорение вычислений достигло 500% на специфических задачах. А на задачах обработки текста никакого ускорения не было, потому и ставили этот сопроцессор за 150$ опционально. Музыку тогда редко кто на компьютере слушал, а видео так вообще было не для широкого пользователя.
Начиная с процессоров серии 80486 сопроцессор стали интегрировать в сам процессор. Кроме Intel486SX, этот процессор вышел позже и имел отключенный сопроцессор. Физически от остальных процессоров он особо не отличался. Видимо, Intel решила реализовать и бракованные экземпляры с ошибками в области сопроцессора.
Рассмотрим подробнее вещественные регистры математического сопроцессора. Хотя, на самом деле, это регистр одного вида. Большой, 80-ти битный, и в наличии их 8 штук стеком. Но программисту доступны три вида абстракции вещественных чисел: короткий (одинарный) формат (single precision), длинный (double precision) и расширенный формат представления чисел (extended precision). Здесь русский перевод терминов дан из книги [1]. Характеристики вещественных чисел представлены в таблице:
Если программист выбирал для использования, например, короткий формат (32 бита), то сопроцессор вставлял число в 80-ти битный регистр, производил над ним операции, а потом возвращал обратно число в уменьшенном размере, если в процессе работы происходил выход за пределы короткого формата, то возвращался NaN (not a number — не число).
Дальнейшее развитие x86 архитектуры добавило кучу расширений (MMX, SSE, SSE2, SSE3, SSSE3, SSE4, SSE5, AVX, AVX2, AVX-512 и др.), а вместе с расширениями новые регистры длинной 128, 256, 512 бит[2], и кучу новых ассемблерных команд для работы с ними. Эти расширения предоставляют возможность для работы только с вещественными числами одинарной и двойной точности, например, каждый 512 битный регистр способен работать либо с четырьмя 64-битными числами двойной точности, либо с восемью 32-битными числами одинарной точности.
От размышлений на тему архитектуры перейдём к компиляторам. На языке программирования C++ тип данных float соответствует 32-х битным вещественным числам x86 архитектуры, double 64-х битным, а вот с long double всё намного интереснее. Как было сказано выше многие разработчики компиляторов пользуются допущением стандарта и делают тип long double равным double. Но «железо» x86-ое позволяет оперировать расширенным 80-ти битным форматом. И есть компиляторы, которые позволяют ими воспользоваться. Рассмотрим компиляторы подробнее.
Как ни странно, но среди игнорирующих 80-ти битный расширенный формат представления данных много известных и широко применяемых компиляторов, вот неполный список: Microsoft Visual C++, C++ Builder, Watcom C++, Comeau C/C++. А вот список компиляторов поддерживающих расширенный формат довольно интересен: Intel C++, GCC, Clang, Oracle Solaris Studio. Рассмотрим компиляторы подробнее.
В компиляторе от Intel не могло не быть расширенного формата — как же производитель оставит свое железо без соответствующего инструмента? Пользование компилятором не бесплатное. Компилятор широко используется в научных расчетах и в высокопроизводительных многопроцессорных системах.
Свободный компилятор GCC легко поддерживает расширенный формат под операционной системой Linux. С Windows всё интереснее. Существует две адаптации компилятора под операционную Windows: MinGW и Cygwin. Обе могут манипулировать расширенным форматом, но MinGW использует runtime от Microsoft и это означает, что вещественные числа, которые превышают 64-х битный double увидеть/вывести куда-либо не удастся. С Cygwin всё немного лучше, так как портирование более комплексное.
Clang аналогично GCC, поддерживает расширенный формат.
Ну и немного об Oracle Solaris Studio, ранее Sun Studio. Под конец существования корпорация Sun, сделала доступными многие свои технологии. Включая свой компилятор. Изначально он был разработан для ОС Solaris с процессорами архитектуры SPARC. Позднее операционную систему вместе с компилятором портировали и под x86-ую архитектуру. Компилятор вместе с IDE доступен под операционной системой Linux. К сожалению этот компилятор «подзаброшен» и не поддерживает последних веяний языка C++.
Для решения озвученной в начале статьи проблемы переполнения формата double, после всех размышлений, страданий и исканий, было решено полностью переписать код и использовать особенности компилятора GCC Cygwin. Был использован тип данных long double для хранения данных. Производительность аналогичных систем использующих 64-х и 80-ти битные вещественные числа отличается. При использовании 64-х битных вещественных чисел компилятор старается все оптимизировать и использовать самые быстрые «новейшие» расширения x86 архитектуры. При переходе на 80-ти битные числа задействуется «древняя» «сопроцессорная» часть архитектуры.
Конечно же, можно было решить проблему переполнения, задействовав программный метод обработки больших вещественных чисел, но тогда падение производительности было бы значительным, так как программа рассчитывала математические модели содержащие тригонометрические функции, извлечение корня и вычисление факториала. Работа по расчету модели с использованием расширенного формата занимала около 8 — 12 часов процессорного времени, в зависимости от входных параметров.
В конце статьи немного поразмышляем о стандарте IEEE 754 [3,4,5]. Первая версия стандарта, как было отмечено, вышла благодаря именно математическому сопроцессору 8087. Последующие версии данного стандарта выходили в 1997 и 2008 годах. Именно стандарт 2008 года наиболее интересен. В нём описаны вещественные числа четверной точности (квадрупольной, quadruple-precision floating-point format)[6]. Именно этот формат хранения данных оптимально подошел бы для вышеописанной задачи. Но он не реализован в доступной процессорной архитектуре распространенных компьютеров. С другой стороны, x86 архитектура давно уже имеет регистры (128, 256, 512 бит) нужного размера, но они служат для быстрой работы с несколькими числами одинарной и двойной точности. Я встречал в интернете информацию о том, что корпорация Intel собиралась в будущих процессорах внедрить поддержку четверной точности, но видимо это осталось только на бумаге.
Из современных архитектур, которые железно поддерживают четверную точность можно выделить архитектуры SPARC V8 и V9. Хотя они и появились ещё в 1990 и 1993 годах соответственно, но физическая реализация четверной точности появилась только в 2004-м году. В 2015 году IBM выпустила спецификацию POWER9 CPU (ISA 3.0), в которой есть поддержка четверных вещественных чисел.
Точность четверных вещественных чисел широкому кругу пользователей излишна. Она, в основном, используется в научных расчетах. Например в астрофизических вычислениях. Именно этим можно объяснить, что выпускавшиеся в 70–80-х годах компьютеры IBM360 имели поддержку вещественных чисел размером 128 бит, но, конечно же, не соответствующих современному стандарту IEEE 754. Пользовались этой машиной в основном в научных расчётах.
Так же скажем пару слов о российском разработчике процессоров МЦСТ. Данная компания разрабатывает и производит процессоры архитектуры SPARC. Но, что интересно, сначала они разработали и выпустили процессоры «старой» архитектуры SPARC V8 (МЦСТ-R150 в 2001 году и МЦСТ R500 в 2004-м) без поддержки вещественных чисел четверной точности, хотя новая архитектура SPARC V9 уже давно была. И только в 2011 году выпустили процессор МЦСТ R1000 с архитектурой SPARC V9 с поддержкой вещественных чисел четверной точности.
Ещё пару слов о стандарте IEEE 754. В интернете есть интересная статья[3], в которой, весьма эмоционально, описываются проблемы и недостатки существующего стандарта. В статье[4] также описывается стандарт и его проблемы. Кроме того, в ней сказано о потребности новых подходов в представлении вещественных чисел. В двух вышеуказанных статьях описаны многие недостатки представления чисел, со своей стороны добавлю вот что. В программировании есть такой термин как «костыль» это нечто неправильное, но помогающее в данный момент времени решить текущую проблему не самым оптимальным образом. Так вот, вещественные числа соответствующие стандарту IEEE754 являются костылем.
А вывод этот появился вот почему. Потому что не бывает отрицательного нуля, преобразование в десятичный формат и обратно неоднозначное, при работе с вещественными числами программист всегда должен помнить об опасном поведении вещественных при приближении к допустимым границам возможного диапазона значений, а при сравнении вещественных чисел нужно сравнивать диапазон с допустимой точностью.
Увлекательные материалы и источники:
- Юров В.И. Assembler. Учебник для вузов. 2-е изд. — СПб.: Питер 2005
- x86
- Юровицкий В.М. IEEE754-тика угрожает человечеству
- Яшкардин В. IEEE 754 — стандарт двоичной арифметики с плавающей точкой
- Статьи в википедии посвященные стандарту IEEE 754: раз, два и три.
- Статья в википедии посвященная четверной точности вещественных чисел