Со временем вендоры добавляли новые и новые инструкции в процессоры, управляющие нашими ноутбуками, серверами, телефонами и многими другими устройствами. Добавление машинных инструкций, решающих конкретные вычислительные подзадачи, является хорошим способом улучшить производительность системы в целом, не усложняя конвейер и не пытаясь нарастить частоту до запредельных величин. Одна новая инструкция, выполняющая ту же операцию, что и несколько старых, позволяет неоднократно увеличить производительность решения заданной задачи.
Новые инструкций, такие как Intel Software Guard Extensions (Intel SGX) и Intel Control-flow Enforcement Technology (Intel CET), также способны предоставить абсолютно новую функциональность.
Хороший вопрос заключается в том, как скоро новые инструкции, добавленные в архитектуру, достигают конечного пользователя. Могут ли операционные системы и другие приложения воспользоваться новыми инструкциями, принимая во внимание, что они, как правило, обеспечивают обратную совместимость и способность исполняться независимо от модели установленного процессора? Много лет назад использование новых инструкций достигалось с помощью пересборки программы под новую архитектуру и добавления проверок, предотвращающих запуск на старой аппаратуре и печатающих что-то вроде “sorry, this program is not supported on this hardware”.
Я воспользовался полноплатформенным симулятором Wind River Simics, чтобы узнать, в какой степени современное программное обеспечение способно использовать новые инструкции, оставаясь при этом совместимым со старым оборудованием.
Экспериментальная установка
Чтобы выяснить, насколько программное обеспечение может динамически адаптироваться к различному оборудования, я воспользовался Simics моделью «generic PC» платформы и двумя различными моделями процессоров: Intel Core i7 первого поколения (кодовое имя Nehalem, выпущен в конце 2008 года) и Intel Core i7 шестого поколения (кодовое имя Skylake, выпущен в середине 2015).
Исследовались следующий сценарии загрузки ОС Linux, запускаемые на описанный выше конфигурациях:
- Ubuntu 16.04, версия ядра 4.4, год выпуска 2016,
- Yocto 1.8, версия ядра 3.14, год выпуска 2014,
- Busybox с ядром 2.6.39, год выпуска 2011.
Один и тот же образ диска использовался для тестирования, тем самым гарантируя, что программный стек останется неизменным. Отличалась только конфигурация процессора в виртуальной платформе. Ожидалось, что Linux, работающий на более новом оборудовании, будет использовать новые инструкции. Каждая конфигурация запускалась с подключенным механизмом инструментации, который считал, сколько раз выполнилась каждая инструкция. Существующий в Simics механизм для инструментации не изменяет поведения гостевых приложений и позволяет изучать загрузку BIOS и ядра операционной системы за счет того, что оперирует на уровне команд процессора. При этом исполняющиеся приложение не может определить, запущено оно с инструментацией или без. Каждая конфигурация исполнялась по 60 секунд виртуального времени. Этого достаточно, чтобы загрузить BIOS и операционную систему. После каждого запуска выбиралось по 100 наиболее часто используемых инструкций, которые использовались для дальнейшего анализа.
Изучение основ идентификации процессоров
В основе данной работы лежит предположение о том, что программное обеспечения может динамически адаптировать исполняемый код в зависимости от используемого оборудования. То есть одна и та же бинарная структура может использовать различные инструкции на разном оборудовании.
Для того, чтобы понять, как работает подобная динамическая адаптация нужно разобраться с тем, как работает железо. Далеко в прошлом, когда процессоров было мало и новые модели появлялись достаточно редко, программное обеспечение могло легко проверить, происходит ли исполнение на Intel 80386 или 80486, Motorola 68020 или 68030 и адаптировать свое поведение соответствующим образом. Сейчас же существует огромное количество разнообразных систем. Для решения задачи идентификации на IA-32 процессорах следует использовать инструкцию CPUID, которая сама по себе является сложной системой, описывающей различные аспекты оборудования.
Вы, наверняка, уже встречали информацию, полученную с помощью инструкции CPUID, даже не задумываясь о ее источнике. Например, Task Manager в Microsoft Windows 8.1 показывает информация о типе процессора и некоторых других его характеристиках, которые получены с помощью инструкции CPUID:
На Linux команда «cat /proc/cpuinfo» способна показать исчерпывающую информация о процессоре, включающую флаги расширений набора команд, которые доступны в текущей системе. Каждое расширение имеет свой флаг, наличие которое программное обеспечение должно проверить перед началом исполнения. Вот пример информации, собранной на процессоре Intel Core i5 четвертого поколения:
CPUID предоставляет информацию о различных расширениях набора команд, доступных в процессоре, но как программное обеспечение на самом деле использует эти флаги для того, чтобы выбрать соответствующий бинарный код в зависимости от аппаратуры? Не разумно было бы применять «if-then-else» конструкцию в каждом месте, которое собирается использовать «нестандартные» инструкции. Достаточно сделать проверку только один раз, так как эти характеристики не изменятся в течении сессии.
Linux обычно использует указатели на функции, использующие различные инструкции для реализации одной и той же функциональности. Хороший пример можно найти в файле arch/x86/crypto/sha1_ssse3_glue.c (источник elixir.free-electrons.com/linux/v4.13.5/source):
Эти функции проверяют наличие определенной функциональности и регистрирует соответствующую hash функцию. Порядок вызова гарантирует, что будет использована наиболее эффективная реализация. Конкретно в данном случае наилучшее решение основывается на инструкциях расширения SHA-NI, но, если они не доступны, используются AVX или SSE реализации.
Результаты
Приведенный ниже график содержит результаты запуска шести разных конфигураций (два процессора и три операционные системы). Он показывает все инструкции, количество которых превышает 1% от общего числа в каком-либо запуске. «v1» означает запуск на модели Core i7 первого поколения, «v6» — шестого.
Первый вывод, который напрашивается: большинство инструкций не очень то и новые. Они скорее относятся к базовым инструкциям, добавленным еще в Intel 8086: move, compare, jump и add. Для более новых инструкций в скобочках написано название расширения, в котором они были добавлены. Всего шесть более или менее новых инструкций в списке из 28 наиболее часто используемых.
Очевидно, что присутствует вариации между различными версиями Linux вдобавок к вариациям, вызванным использованием разных процессоров. Например, BusyBox, сконфигурированный со старым ядром, использует инструкцию LEAVE, которая не является популярной для других версий ядра, к тому же он значительно меньше использует инструкцию POP. Однако это не дает ответ на вопрос, как программное обеспечение использует новые инструкции, когда они доступны. Для нашей цели наиболее интересны вариации, вызванные сменой поколения процессора при запуске одного и того же программного стека.
Все исследуемые в рамках данной работы сценарии представляют собой загрузку операционной системы Linux с различными параметрами ядра. К тому же различные дистрибутивы могут быть собраны разными версиями компилятора с использованием различных флагов. Таким образом бинарный код, даже собранный с использованием одних и тех же исходников, может отличаться.
На примере Yocto, мы видим этот эффект. Yocto использует инструкции ADCX, ADOX и MULX (входящие в расширения ADX и BMI2). Этот пример также хорошо демонстрирует скорость, с которой новые инструкции могут появиться в программном обеспечении. Эти три инструкции были добавлены в процессоре Intel Core пятого поколения, который был выпущен примерно одновременно с Linux ядром, используемым в Yocto. То есть поддержка новых инструкций была добавлена к моменту появления процессора на рынке. И это не удивительно, так как спецификация к новым инструкциям зачастую публикуется раньше, чем их аппаратная реализация. То есть программное обеспечение может заранее адаптировать свое поведение (интересная статья на эту тему) под новую аппаратуру, зачастую используя виртуальные платформы для отладки и тестирования.
Однако Ubuntu 16.04 с более новым ядром не использует ADX и BMI2, что говорит о том, что оно было сконфигурировано по-другому. Возможно, это связано с версией или флагами компилятора, параметрами ядра или набором установленных пакетов.
Изменение потока управления
Еще одна вещь, на которую было интересно обратить вникание — какие инструкции используются для изменения потока управления. Классическое правило, описанное в не менее классической книге Хеннесси и Паттерсона гласит, что каждая шестая инструкция — Jump. Однако проведенные измерения показали, что примерно одна инструкция из пяти является инструкцией, изменяющей поток управления. Ближе к одной из шести для Yocto.
Векторные инструкции
Пожалуй, наиболее известные общественности расширения набора команд — это Single Instruction Multiple Data (SIMD) или, иначе говоря, векторные инструкции. Векторные инструкции присутствуют в IA-32 процессорах, начиная с расширения MMX, добавленного в Intel Pentium в 1997 году. Сейчас наличие MMX инструкций фактически гарантировано. Можно заметить, что некоторые из них присутствуют на графике самых популярных инструкций. Далее было добавлено множество различных Streaming SIMD Extensions (SSE) инструкций и наиболее новые AVX, AVX2 и AVX512.
Я не ожидал большого количества векторных инструкций, учитывая, что изучалась загрузка операционной системы и BIOS. Однако примерно 5-6% исполненных инструкций оказались векторными. Количество исполненных векторных инструкций, измеренное как процент от общего количества выполненных инструкций и сгруппированное по расширениям:
Первое, что бросается в глаза — Busybox фактически не использует векторные инструкции. Следующее интересное наблюдение заключается в том, что, при смене процессора первого поколения на процессор шестого, количество более старых инструкций уменьшается, а количество новых растет. В частности, прослеживается замена старых SSE инструкций, на более новые AVX и AVX2.
Simics
Как было сказано в самом начале, провести данное исследование с помощью Simics было не сложно. Очевидным образом, Simics имеет доступ ко всем инструкциям, исполняющимся на всех процессорах в моделируемой системе (данные эксперименты проводились на двухъядерной системе, однако второе ядро не выполняло никаких инструкций во время загрузки). Сценарии были полностью автоматизированы, включаю выбор устройства, на котором установлена ОС, ввод имени пользователя и пароля в конце загрузки. Каждый сценарий запускался один раз, так как повторные запуски покажут точно такие же результаты (мы исследуем повторяющиеся сценарии, начинающиеся с одного и того же места).
Заключение
Было поучительно узнать, как программные стеки адаптируются и используют новые инструкции на более новых процессорах. Современные программы адаптивны и будут исполнять различный код в зависимости от используемой аппаратуры без перекомпиляции. Во всех изученных сценариях один и тот же программный стек использовался на разных моделируемых системах, при этом используя разные инструкции в зависимости от их доступности. Исследование представляет собой отличный пример данных, которые с легкостью могут быть получены с помощью моделирования, но едва ли могут быть собраны на реальной аппаратуре.
Автор: yulyugin