Симуляция процессоров что это такое
Перейти к содержимому

Симуляция процессоров что это такое

  • автор:

Симуляторы компьютерных систем: всем знакомый полноплатформенный симулятор и никому неизвестные потактовый и трассы

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

Симуляторы компьютерных систем: всем знакомый полноплатформенный симулятор и никому неизвестные потактовый и трассы

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

Полноплатформенный симулятор (full platform simulator), или “Один в поле — не воин”

Если требуется исследовать работу одного конкретного устройства, например, сетевой карты, или написать для этого устройства прошивку или драйвер, то такое устройство можно смоделировать отдельно. Однако использовать его в отрыве от остальной инфраструктуры не очень удобно. Для запуска соответствующего драйвера потребуется центральный процессор, память, доступ к шине для передачи данных и прочее. Кроме того, для работы драйвера необходимы операционная система (ОС) и сетевой стек. В дополнение к этому может потребоваться отдельный генератор пакетов и сервер приема ответов.

Полноплатформенный симулятор создает окружение для запуска полного софтверного стека, который включает в себя все, начиная с BIOS и загрузчика и заканчивая самой ОС и различными ее подсистемами, такими как тот же сетевой стек, драйверами, приложениями пользовательского уровня. Для этого в нем реализованы программные модели большинства устройств компьютера: процессор и память, диск, устройства ввода-вывода (клавиатура, мышь, дисплей), а также та самая сетевая карта.

Ниже приведена блок-диаграмма чипсета x58 от компании Intel. В полноплатформенном симуляторе компьютера на этом чипсете необходима реализация большинства перечисленных устройств, в том числе и тех, что находятся внутри IOH (Input/Output Hub) и ICH (Input/Output Controller Hub), не нарисованных детально на блок-диаграмме. Хотя, как показывает практика, не так уж мало устройств, которые не используются тем ПО, которое мы собираемся запускать. Модели таких устройств можно не создавать.

Симуляторы компьютерных систем: всем знакомый полноплатформенный симулятор и никому неизвестные потактовый и трассы

Чаще всего полноплатформенные симуляторы реализуются на уровне инструкций процессора (ISA, см. предыдущую статью ). Это позволяет относительно быстро и недорого создать сам симулятор. Уровень ISA также хорош тем, что остается более или менее постоянным, в отличие от, например, уровня API/ABI, который меняется чаще. К тому же, реализация на уровне инструкций позволяет запускать так называемое немодифицированное бинарное ПО, то есть запускать уже скомпилированный код без каких-либо изменений, ровно в том виде как он используется на реальном железе. Другими словами, можно сделать копию («дамп») жесткого диска, указать его в качестве образа для модели в полноплатформенном симуляторе и – вуаля! – ОС и остальные программы загружаются в симуляторе без всяких дополнительных действий.

Производительность симуляторов

Симуляторы компьютерных систем: всем знакомый полноплатформенный симулятор и никому неизвестные потактовый и трассы

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

Здесь как раз уместно будет коснуться темы производительности симуляторов. Обычно ее измеряют в IPS (instructions per second), точнее в MIPS (millions IPS), то есть количестве инструкций процессора, выполняемых симулятором за одну секунду. В то же время скорость симуляции зависит и от производительности системы, на которой работает сама симуляция. Поэтому, возможно, правильнее говорить о «замедлении» (slowdown) симулятора по сравнению с оригинальной системой.

Наиболее распространенные на рынке полноплатформенные симуляторы, те же QEMU, VirtualBox или VmWare Workstation, имеют неплохую производительность. Для пользователя может быть даже не заметно, что работа идет в симуляторе. Так происходит благодаря реализованной в процессорах специальной возможности виртуализации, алгоритмам бинарной трансляции и другим интересным вещам. Это все тема для отдельной статьи, но если совсем коротко, то виртуализация – это аппаратная возможность современных процессоров, позволяющая симуляторам не симулировать инструкции, а отдавать на исполнение напрямую в реальный процессор, если, конечно, архитектуры симулятора и процессора похожи. Бинарная трансляция – это перевод гостевого машинного кода в хостовый и последующее исполнение на реальном процессоре. В результате симуляция лишь ненамного медленнее, раз в 5-10, а часто вообще работает с той же скоростью, что и реальная система. Хотя на это влияет очень много факторов. Например, если мы хотим симулировать систему с несколькими десятками процессоров, то скорость тут же упадет в эти несколько десятков раз. С другой стороны, симуляторы типа Simics в последних версиях поддерживают многопроцессорное хостовое «железо» и эффективно распараллеливают симулируемые ядра на ядра реального процессора.

Если говорить про скорость микроархитектурной симуляции, то это обычно на несколько порядков, примерно в 1000-10000 раз, медленнее выполнения на обычном компьютере, без симуляции. А реализации на уровне логических элементов медленнее еще на несколько порядков. Поэтому в качестве эмулятора на этом уровне используют FPGA, что позволяет существенно увеличить производительность.

График ниже показывает примерную зависимость скорости симуляции от детализации модели.

Симуляторы компьютерных систем: всем знакомый полноплатформенный симулятор и никому неизвестные потактовый и трассы

Потактовая симуляция

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

Простейший пример – инструкция доступа в память. Если запрашиваемая ячейка памяти доступна в кэше, то время выполнения будет минимально. Если в кэше данной информации нет («промах кэша», cache miss), то это сильно увеличит время выполнения инструкции. Таким образом, для точной симуляции необходима модель кэша. Однако моделью кэша дело не ограничивается. Процессор не будет просто ждать получения данных из памяти при ее отсутствии в кэше. Вместо этого он начнет выполнять следующие инструкции, выбирая такие, которые не зависят от результата чтения из памяти. Это так называемое выполнение «не по порядку» (OOO, out of order execution), необходимое для минимизации времени простоя процессора. Учесть все это при расчете времени выполнения инструкций поможет моделирование соответствующих блоков процессора. Среди этих инструкций, выполняемых, пока ожидается результат чтения из памяти, может встретится операция условного перехода. Если результат выполнения условия неизвестен на данный момент, то опять-таки процессор не останавливает выполнение, а делает «предположение», выполняет соответствующий переход и продолжает превентивно выполнять инструкции с места перехода. Такой блок, называемый branch predictor, также должен быть реализован в микроархитектурном симуляторе.

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

Симуляторы компьютерных систем: всем знакомый полноплатформенный симулятор и никому неизвестные потактовый и трассы

Работа всех этих блоков в реальном процессоре синхронизуется специальными тактовыми сигналами, аналогично происходит и в модели. Такой микроархитектурный симулятор называют потактовым (cycle accurate). Основное его назначение – точно спрогнозировать производительность разрабатываемого процессора и/или рассчитать время выполнения определенной программы, например, какого-либо бенчмарка. Если значения будут ниже необходимых, то потребуется дорабатывать алгоритмы и блоки процессора или оптимизировать программу.

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

При этом для симуляции остального времени работы программы используется функциональный симулятор. Как такое комбинированное использование происходит в реальности? Сначала запускается функциональный симулятор, на котором загружается ОС и все необходимое для запуска исследуемой программы. Ведь нас не интересует ни сама ОС, ни начальные стадии запуска программы, ее конфигурирование и прочее. Однако и пропустить эти части и сразу перейти к выполнению программы с середины мы тоже не можем. Поэтому все эти предварительные этапы прогоняются на функциональном симуляторе. После того, как программа исполнилась до интересующего нас момента, возможно два варианта. Можно заменить модель на потактовую и продолжить выполнение. Режим симуляции, при котором используется исполняемый код (т.е. обычные скомпилированные файлы программ), называют симуляцией по исполнению (execution driven simulation). Это самый распространенный вариант симуляции. Возможен также и другой подход – симуляция на основе трасс (trace driven simulation).

Симуляция на основе трасс

Она состоит из двух шагов. С помощью функционального симулятора или на реальной системе собирается и записывается в файл лог действий программы. Такой лог называется трассой (trace). В зависимости от того, что исследуется, трасса может включать исполняемые инструкции, адреса памяти, номера портов, информацию по прерываниям.

Следующий шаг – это «проигрывание» трассы, когда потактовый симулятор читает трассу и выполняет все инструкции, записанные в ней. В конце получаем время выполнения данного куска программы, а также различные характеристики этого процесса, например, процент попадания в кэш.

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

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

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

Добавить комментарий Отменить ответ

Для отправки комментария вам необходимо авторизоваться.

Программная симуляция микропроцессора. Коробка передач

В этой статье я хочу рассказать о том, как создатели симуляторов добиваются максимальной производительности моделей процессоров, при этом не жертвуя гибкостью и расширяемостью полного решения. Если кратко, то решение состоит в сосуществовании нескольких движков, наилучшие качества которых используются на различных этапах работы модели.
Содержимое данной заметки будет основываться на моём опыте разработки функциональных симуляторов, а также на публикациях и технических статьях, описывающих различные симуляторы и виртуальные машины: Wind River Simics, VMWare, Qemu, Bochs и другим. Слово «функциональный» в контексте данной статьи обозначает то, что точность моделей ограничена уровнем набора команд (instruction set architecture, ISA).

Интерпретация

«Интерпретатор» — некто (или нечто), совершающий перевод текста или речи с одного языка на другой «на лету». Смысл этого термина приблизительно сохраняется при его переносе в область вычислительной техники. Оригинальные реализации многих языков программирования: Basic, Unix Shell, Rexx, PHP и др. — были интерпретаторами. Характерная их черта — обработка за один раз одной строки программы, написанной на входном языке. Следующая строка будет преобразована (проинтерпретирована) только тогда, когда в этом возникнет необходимость. По этой причине термин «интерпретатор» обычно противопоставляется понятиям «транслятор» и «компилятор», которые обрабатывают более крупные блоки входного языка: процедуры, файлы, модули и т.п.

Цикл работы

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

О числе стадий в конвейере

Отмечу, что длины конвейеров настоящих процессоров бывают очень разными. У простых микроконтроллеров, например, PIC, может быть всего две стадии. У Intel Pentium 4 последних поколений число стадий доходило до 31. Есть и невообразимо монструозные продукты, например, Xelerated Xelerator X10q имеет 1040 (!) стадий конвейера.

  1. Fetch — чтение машинного кода из памяти.
  2. Decode — декодирование текущей функции, заключённой в инструкции, а также её аргументов — операндов.
  3. Execute — исполнение функции над аргументами.
  4. Writeback — запись результатов память.
  5. Advance PC — продвижения регистра-указателя инструкций (PC, program counter).
Чтение инструкции из памяти

В реальных компьютерах оперативная память размещается достаточно далеко (во многих смыслах) от процессора. Код, как и данные хранится в ней. Требуется доставить порцию машинного кода для последующей обработки в процессор, что и делается на стадии Fetch. Как и в реальных микропроцессорах, имеющих перед памятью более быстрый кэш инструкций, модель может также использовать кэширование ранее прочитанных инструкций. И опять же, как и в реальных системах, чтение памяти вполне может завершиться возникновением исключения, которое необходимо будет просимулировать.

Декодирование

Задача декодера, как аппаратного, так и симулируемого — вычленить из последовательности байт, полученных из памяти, отдельную инструкцию и разобрать, что же она означает.
Написание модели декодера — не самая простая вещь. Создателю симулятора приходится иметь дело со многими особенностями гостевых архитектур (об одной из них, присущей IA-32, на Хабре недавно писал yulyugin). Само число инструкций, которые надо уметь различить, может быть велико.
Если написание симулятора начинается с чтения документации на ISA, то создание декодера в его составе — с просмотра таблиц инструкций, чтобы получить представление о масштабах бедствия. Приведу несколько примеров таблиц кодировок инструкций для различных архитектур. Все они взяты из официальных мануалов или сопутствующих материалов.
Для ARM [3], 32-битные инструкции.

Таблица

Для PowerPC 3.0 [4]
Таблица

Для Itanium 2.3 [2]. Мне всегда нравилась задорная цветастость этого руководства!
Таблица

Для IA-32 [5]. Диаграмма, показывающая все поля, использующиеся после введения префикса EVEX.
Таблица

Жуткая схема, но мне она помогла понять, что вообще происходит в AVX3.

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

Исполнение

Исполнение (execute) состоит из непосредственной симуляции функции уже декодированной инструкции. Это может быть вычисление результата арифметической или логической операции, чтение или запись в память, изменение текущего режима процессора или передача контроля управления при ветвлении или вызове процедуры.
Каждой распознанной инструкции должна соответствовать сервисная процедура (service routine). В самой простой схеме интерпретатора её выбор производится по коду операции инструкции (опкоду) с помощью конструкции множественного выбора — переключателя — используемого языка программирования. В языке Си это оператор switch. Данная схема получила название «переключаемая» (switched):

switch (opcode)

Существует несколько альтернатив переключаемой схеме интерпретации, характеризующихся большей скоростью работы [10], однако о них я напишу в другой раз.
Как и создание декодера вручную, написание сервисных процедур может оказаться утомительным занятием, особенно если они похожи друг на друга. Например, в системе команд могут быть семейства инструкций, имеющих одинаковый смысл, но различающихся по ширине обрабатываемого вектора и элементов, а также положением обрабатываемых данных (в регистре или в памяти). Руки сами тянутся к методам обобщённого программирования. Или же хочется написать генератор кода, что возьмёт на себя рутину.
Опять же, сервисные процедуры должны быть синхронизованны с декодером: при правке набора команд надо не забывать про добавление/удаление семантики для их симуляции. Поэтому самым разумным оказывается иметь такое совмещённое описание, чтобы из него автоматически производить и декодер, и процедуры, и (до кучи) дизассемблер.

Запись в память

Работа с симулируемой памятью, даже если рассматривать её только с точностью, достаточной для функциональной модели, тоже нетривиальна. Здесь надо учитывать многое: гостевую схему преобразования виртуальных адресов в физические, отношение гостевой системы к выравниванию данных, порядок байт в машинном слове (endianness), попадание доступа в отображённое на память устройство (memory mapped input/output), разрешения страницы на чтение/запись/исполнение, опциональную сегментацию (уфф!)…
Запись или чтение памяти может привести к исключению, которое, как и в случае fetch, необходимо корректно просимулировать.

Продвижение PC

PC (program counter) — регистр, хранящий адрес текущей инструкции. В разных системах и книгах он зовётся по-разному: IP/EIP/RIP, pc, IP… Для «линейных» операций его надо просто продвинуть на длину только что обработанной инструкции. Но для команд ветвления, вызова процедур, программных прерываний он будет меняться более сложным образом, соответствующим их семантике.

Промежуточный итог

Интерпретатор — практически всегда первый тип модели, создаваемый для новой архитектуры процессора. По этой причине их существует великое множество, и перечисление более-менее известных заняло бы много места. Здесь приведу лишь пару примеров: Bochs [6], Zsim [7]. Хочу также порекомендовать FAQ Марата Файзуллина.

Двоичная трансляция

Главное достоинство моделей, основанных на интерпретации — их простота. Главный недостаток — низкая скорость работы.
Как и в случае с языками высокого уровня, для ускорения можно использовать технику, исторически называвшуюся трансляцией, но сейчас чаще именуемую компиляцией. Так, название языка Fortran — это сокращение от FORmula TRANslator.
Вместо того, чтобы тратить время на разбор и исполнение каждой инструкции каждый раз, когда она встречается, можно просто перевести достаточно длинный блок гостевого кода в эквивалентный по смыслу блок машинного хозяйской системы. Затем, при симуляции, достаточно передать управление на него, и он будет исполняться «без замедления». Это идея работает потому, что на практике код, однажды исполненный, скорее всего будет вскоре будет исполнен снова — ведь большую часть времени программы проводят в циклах относительно небольшого размера.
В контексте симуляции данная техника получила название «двоичная трансляция» (ДТ), или «бинарная трансляция» (binary translation). Понятие это очень близко к «компиляции времени исполнения» (just in time compilation, JIT).
Ниже я опишу одну из простых используемых на практике схем.

Капсулы и блоки трансляции

На вход процесса трансляции подаётся группа машинных команд, находящихся рядом (например, на одной странице) и/или исполняющихся последовательно (трасса). Они декодирутся. Затем каждая гостевая инструкция ассоциируется с блоком хозяйского кода, называемого капсулой. Несколько капсул собираются в более крупные образования, именуемые блоками трансляции. Каждый из них может иметь одну или более точек входа, а также несколько мест, из которых управление может его покинуть.

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

Может ли капсула быть короче?

Инструкции, которые при функциональной симуляции не вызывают видимых архитектурных эффектов, могут быть представлены пустой капсулой в ноль команд, особенно если блок трансляции перед исполнением подвергается оптимизациям. Примеры: варианты инструкции NOP (no-operation), инструкции предзагрузки данных (prefetch), барьеры (fence).

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

Проблема обнаружения кода
  1. В оперативной памяти данные (переменные, массивы, константы, строки и т.д.) и код программ, их обрабатывающий, хранятся вместе. Никаких границ между ними не обозначено. Двоичная трансляция блоков данных (рассматриваемых как код!) бесполезна: управление на них никогда не будет передано, — и даже вредна: затрачиваемое на это время уходит впустую.
  2. В архитектурах, допускающих переменную длину инструкций, очень важен адрес, с которого начинается их декодирование. Сдвиг даже на один байт приводит к полной смене смысла последовательности.
  3. Результат декодирования зависит от режима процессора. Например, для архитектуры ARM есть фактически два набора инструкций — полный 32 битный и урезанный 16-битный Thumb, переход между которыми происходит с помощью команды BX .

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

Самомодифицирующися код

Исполняемый код программ и обрабатываемые ими данные располагаются в одной физической памяти. Поэтому возможно в процессе исполнения изменять машинный код. Это используется, например, операционными системами для загрузки в память приложений и динамических библиотек, полиморфными вирусами для сокрытия своего присутствия. Назовём такую ситуацию самомодифицирующийся код (self-modifying code, SMC). В случае двоичной трансляции это означает, что блоки трансляции могут устаревать — делаться некорректными, если гостевой код, из которого они были получены, изменился.

Бывало ли у вас такое?

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

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

Ограниченность оптимизаций

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

Промежуточный итог

Более подробно о техниках двоичной трансляции, применяемых для нужд симуляции, можно прочитать в серии постов на Intel Developer Zone:
Часть 1
Часть 2
Часть 3

Пара ссылок на известные мне симуляторы, в которых двоичный транслятор является основным движком для симуляции: SoftSDV [8], оригинальная реализация Qemu [9].

Прямое исполнение и аппаратная поддержка

Важным на практике случаем ДТ является ситуация, когда архитектуры гостя и хозяина совпадают (или почти совпадают). При этом возникает возможность значительно упростить трансляцию — в некоторых случаях она сводится к копированию гостевого кода как хозяйского или даже исполнению его «на месте», без дублирования. Подобные режимы симуляции имеют общее название «прямое исполнение» (direct execution, DEX).

Общая идея

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

Почему это не работает
  1. Доступы к памяти. Адресное пространство гостя занимает лишь часть памяти симулятора. Данные и код симулируемой системы не обязательно будут находиться по тем же адресам, по которым они располагались в реальности.
  2. Возврат управления. Как можно «заставить» симулируемое приложение отдать управление обратно симулятору?
  3. Привилегированные инструкции. Симулятор работает в непривилегированном режиме пользовательского приложения, а гостевой код может содержать инструкции системных режимов. Попытка их исполнения приведёт к аварийному завершению симулятора.
Аппаратная поддержка

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

Я постарался осветить классические условия эффективной виртуализации и состояние дел в современных системах в моём посте «Аппаратная виртуализация. Теория, реальность и поддержка в архитектурах процессоров».

Ограничения
  1. Аппаратно поддерживаемое прямое исполнение невозможно, если архитектуры гостя или хозяина не совпадают. Код ARM на IA-32 или программа для MIPS на PowerPC волшебным образом не побежит.
  2. Не все режимы процессора могут поддерживаться внутри гостя. Например, древний 16-битный реальный режим IA-32 затруднительно исполнять с помощью VMX, если нет возможностей т.н. Unrestricted Guest.
  3. Высокая стоимость входа и выхода в гостевой режим может убить выигрыш в скорости.
  4. Сложнее наблюдать и управлять работой гостевых приложений. Становится затруднительно ставить точки останова, с точностью до инструкции выполнять пошаговое исполнение, изменять семантику отдельных инструкций и т.д.
  5. Симулятор требует наличия модуля ядра/драйвера ОС, который будет осуществлять переключение между режимами. Написание, отладка этого модуля требует специфический знаний и инструментов.
Промежуточный итог

Довольно большое число симуляторов используют аппаратную поддержку виртуализации Intel VT-x для своих нужд. Можно упомянуть Wind River Simics, Oracle VirtualBox, Qemu и продукты VMWare.

Собираем всё вместе

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

  • Лёгкость реализации в коде. Самый простой в написании — интерпретатор. При этом сложно сказать, что труднее написать: ДТ или DEX — это зависит от свойств хозяйской системы.
  • Переносимость между хозяйскими архитектурами. Интерпретатор берёт первое место: если он правильно написан, его код будет переносим. ДТ не сильно отстаёт при условии, что его генератор кода можно перенацелить (retarget) на другую систему. DEX не работает при существенных различиях в архитектурах, т.к. завязан на конкретную аппаратуру.
  • Простота добавления функциональности, например новых инструкций ЦПУ. Вновь интерпретатор лидирует с отрывом. ДТ требует больше усилий: необходимо писать капсулы. DEX не позволяет выйти за границы хозяйской аппаратуры; новые инструкции не будут распознаны.
  • Скорость работы. Тут интерпретатор в целом сильно проигрывает двум остальным движкам. ДТ обычно также уступает DEX. Однако вспомним, что слабая сторона ДТ — это SMC, а у DEX — это частые выходы из виртуализации, например, на привилегированных инструкциях.
  • Если быстрый режим недоступен, переходить на более медленный. Например, не все инструкции могут поддерживаться ДТ или DEX.
  • Переключаться с быстрой модели на медленную, если быстрая перестала быть таковой. Если детектрован SMC, то режим ДТ лучше временно отключить.
  • Откладывать трансляцию порции кода до тех пор, пока не станет ясно, что этот код — «горячий». Зачем тратить время на генерацию блока, который будет исполнен лишь несколько раз?
  • Не использовать DEX, если есть высокая вероятность скорого выхода по исключению.
  • Необходимо предоставлять пользователю возможность следить за текущей скоростью моделирования с разбиением по режимам. Ведь эвристики ограничены в своей прозорливости. Часто только человек может определить меры, необходимые для её ускорения.

Платой за скорость и гибкость является необходимость поддерживать фактически три независимых модели. Вылавливать в них ошибки, устранять «мелкие» различия в поведении, натыкаться на странности реализаций виртуализации разных поколений аппаратуры. В общем, пытаться обуздать трёхглавого Змея Горыныча.

Иван Билибин. Змей Горыныч. 1912.

Заключение

Заинтересовавшихся в вопросах внутреннего устройства программных моделей компьютеров я хочу отослать к учебнику [1]. Электронный вариант второго издания, а также «бета-версия» третьего доступна на сайте курса «Основы программного моделирования».

В этой заметке ни слова не было сказано об ещё одном способе повышения скорости моделирования — параллельном исполнении. Об проблемах, связанных с построением таких симуляторов, и их решениях рассказывается в другой серии постов на Intel Developer Zone: 0, 1, 2, 3.

Задний ход

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

Литература

[1] Основы программного моделирования ЭВМ: Учебное пособие / Речистов Г.С., Юлюгин Е.А., Иванов А.А., Шишпор П.Л., Щелкунов Н.Н., Гаврилов Д.А. — 2-е изд., испр. и доп. — Издательство МФТИ, 2013.
[2] Intel Corporation. Intel® Itanium® Architecture Software Developer’s Manual Rev. 2.3
[3] ARM Limited. ARM Architecture Reference Manual — 2005
[4] IBM Corporation. PowerPC® Microprocessor Family: The Programming Environments Manual for 64-bit Microprocessors. Version 3.0
[5] J.C.S. Adrian et al. Systems, Apparatuses, and Methods for Blending Two Source Operands into a Single Destination Using a Writemask. US Patent Application Publication. № 2012/0254588 A1
[6] D. Mihoka, S. Shwartsman. Virtualization Without Direct Execution or Jitting: Designing a Portable Virtual Machine Infrastructure bochs.sourceforge.net
[7] Yair Lifshitz, Robert Cohn, Inbal Livni, Omer Tabach, Mark Charney, Kim Hazelwood. Zsim: A Fast Architectural Simulator for ISA Design-Space Exploration www.cs.virginia.edu/kim/docs/wish11zsim.pdf
[8] SoftSDV: A Presilicon Software Development Environment for the IA-64 Architecture / Richard Uhlig, Roman Fishtein, Oren Gershon, Israel Hirsh, Hong Wang // Intel Technology Journal. — 1999. —№ 14. — ISSN: 1535-766X. — noggin.intel.com/content /softsdv-a-pre-silicon-software-development-environment-for-the-ia-64-architecture/
[9] Fabrice Bellard. QEMU, a Fast and Portable Dynamic Translator // FREENIX Track: 2005 USENIX Annual Technical Conference. — 2005. — www.usenix.org/publications/library/proceedings/usenix05/tech/freenix/full_papers/bellard/bellard.pdf
[10] James E.Smith, Ravi Nair. Virtual machines — Versatile Platforms for Systems and Processes. — Elsevier, 2005. — ISBN: 978-1-55860-910-5.

  • simulation
  • interpreter
  • binary translation
  • direct execution
  • virtualization
  • симуляция
  • виртуализация
  • интерпретатор
  • двоичный транслятор
  • прямое исполнение
  • simics

Моделирование и симуляция

Революционный потенциал моделирования с ускорением на GPU

Графические процессоры ускоряют выполнение высокопроизводительных вычислений (HPC) за счет распараллеливания фрагментов кода, требующих большого объема вычислений. Это дает ученым, исследователям и инженерам возможность быстрее проводить симуляции и совершать открытия.

Кто использует симуляцию и моделирование?

Моделирование применяется для широкого спектра задач: лабораторных исследований, инженерных видов деятельности и анализа финансового состояния компании.

Исследователи

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

Инженеры

Инженеры-механики, геоученые и специалисты промышленного машиностроения моделируют сложные проекты на системах с ускорением на GPU.

Аналитики

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

Ускорение моделирования

Приложения помогают ученым, инженерам и исследователям выполнять различные задачи: от моделирования жидкостей до молекулярной динамики. Сегодня тысячи таких приложений получили ускорение на GPU, что позволило специалистам повысить эффективность своей работы. Популярные HPC-приложения вы можете найти в каталоге NVIDIA NGC ™ .

GROMACS

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

LAMMPS

LAMMPS (Large-Scale Atomic/Molecular Massively Parallel Simulator) — пакет для классической молекулярной динамики.

NAMD

NAMD (NAnoscale Molecular Dynamics) — это приложение молекулярной динамики, предназначенное для проведения высокопроизводительного моделирования крупных биомолекулярных систем.

Узнать о показателях производительности некоторых приложений для высокопроизводительных вычислений можно на странице для разработчиков NVIDIA Developer Zone. Чтобы начать работу с GPU-ускоренными приложениями, перейдите в каталог NVIDIA NGC.

Разрабатывайте приложения с ускорением на GPU с SDK NVIDIA для HPC .

Полный набор компиляторов, инструментов и библиотек для HPC

SDK NVIDIA для HPC содержит проверенные компиляторы, библиотеки и программные инструменты, позволяющие максимизировать продуктивность разработчиков, а также производительность и совместимость приложений для HPC. Популярные приложения для высокопроизводительных вычислений, включая VASP, Gaussian, ANSYS Fluent, GROMACS и NAMD, используют CUDA ® , OpenACC ® и математические библиотеки с ускорением на GPU, что позволяет обеспечить революционную производительность. Вы можете использовать те же программные инструменты и с помощью графических процессоров NVIDIA добиться невероятного ускорения приложений и значительной энергоэффективности.

Исследование процессора и его функциональная симуляция

Наверное каждый программист ASM / C / C++ когда-то задумывался о написании своей собственной операционной системы.

И наверное каждый разработчик Verilog / VHDL для ПЛИС когда нибудь задумывался о создании своего процессора.

Собственно реализовать более-менее традиционный процессор на сегодняшний день кажется не очень и большая проблема. Принципы работы процессоров описаны во многих книгах и статьях. Кроме того, существует много процессоров с открытой архитектурой вроде openRISC или openSPARC и многие другие. Их вполне можно рассмотреть перед тем как изобретать свой велосипед.

Я решил поизучать ARM совместимый процессор AMBER. Его исходники есть на http://opencores.org.

  • все таки это ARM – один из самых популярных процессоров в телефонах, планшетах и всяких embedded системах. Изучение системы команд ARM не пройдет зря.
  • исходный код процессора написан на Verilog – мне этот язык знаком.
  • разработчиком системы на кристалле AMBER предоставляются патчи ядра Linux для его сборки и вроде бы инструкции вполне подробные.

Несмотря на то, что исходники системы открыты, работу предстояло сделать большую. Оригинальный проект AMBER запускался на отладочной плате с ПЛИС Xilinx. Я же собирался портировать проект на совсем другую плату с ПЛИС Altera.

Я портирую проект AMBER на плату Марсоход2:

На этой плате установлена ПЛИС Cyclone III EP3C10E144C8 (10 тысяч логических элементов), микросхема памяти SDRAM 8Mb, встроенный программатор на базе FTDI FT2232H, микросхема АЦП, кнопочки, светодиодики, разъем VGA. Вполне бюджетная плата.

Микросхема FTDI дает плате USB интерфейс. Чип FT2232H имеет два канала, которые могут программироваться в разные режимы работы. В данной плате предполагается, что канал A микросхемы FTDI будет использоваться для программирования ПЛИС через JTAG, а канал B будет как скоростной последовательный порт (921600 бит/сек). Оба канала работают независимо друг от друга. Таким образом, ПЛИС может принимать или передавать данные через последовательный порт, а тем временем внутренние сигналы ПЛИС можно изучать инструментом Altera SignalTap через JTAG. Это довольно удобно – все через один кабель USB – и питание платы, и передача данных и загрузка/отладка проекта.

К сожалению, портирование проекта AMBER в плату Марсоход2 было не очень гладким и потребовало довольно много усилий. Некоторые проблемы были предсказуемыми и понятными, вроде того, что в проекте нужно было заменить Xilinx компоненты DLL и блоки памяти для кеша или bootram на аналогичные компоненты Altera. Или вот еще понятная задача – вместо компоненты DDR контроллера использовать модуль SDRAM. Я использовал Wishbone BUS совместимый SDRAM контроллер с открытыми исходниками с того же opencores. Еще нужно было сделать новые назначения входных/выходных сигналов пинам ПЛИС согласно схемы платы Марсоход2.

Другие проблемы, которые пришлось решать, оказались совсем не тривиальными.

Например, оказалось, что некоторые конструкции языка Verilog трактуются компилятором Altera Quartus II несколько не так, как у компилятора Xilinx. Более того, для функциональной симуляции проекта я использовал Icarus Verilog и похоже он трактует Verilog так же, как и Xilinx, а не так, как Altera. Это несколько расстраивает, но в конце концов, мне удалось выявить и исправить проблемные места в исходном коде Verilog процессора AMBER после симуляции икарусом и внутрисхемной отладки с Altera SignalTap.

Следующая трудность была связана с ядром Linux, которое я компилировал для запуска на плате. Здесь я совсем не ожидал проблем, ведь автор системы на кристалле AMBER Santifort Conor описал в своей инструкции как собирать ядро и как его запускать. Не знаю почему, но его инструкция не сработала. Я встретил и проблему наложения патчей и ошибки компиляции ядра, которые пришлось решать по ходу дела. Так же пришлось вносить изменения в конфигурационные файлы ядра, вносить изменения в само ядро.

Тем не менее, результат уже есть. Как это работает в железе:

Программой терминала TeraTerm открываю COM порт принадлежащий плате и из другой программы программатора Altera Quartus II загружаю образ HW системы в ПЛИС. Процессор стартует из bootram ПЛИС и в консоли терминала появляется интерактивный «монитор». С его помощью можно читать или писать в ячейки памяти, а так же загрузить любой файл из компьютера в ОЗУ платы через тот же последовательный порт протоколом XMODEM.

Так, я загружаю initrd по адресу 0x700000 и загружаю само ядро Linux по адресу 0x80000. Команда “j 80000” из консоли монитора запускает ядро и видно, как оно стартует:

Полный вывод консоли выглядит вот так:

Linux version 2.4.27-vrs1 (nick@ubuntu) (gcc version 4.5.2 (Sourcery G++ Lite 2011.03-46) ) #1 Tue Jan 22 23:48:37 PST 2013 CPU: Amber 2 revision 0 Machine: Amber-FPGA-System On node 0 totalpages: 256 zone(0): 256 pages. zone(1): 0 pages. zone(2): 0 pages. Kernel command line: console=ttyAM0 mem=8M root=/dev/ram 19.91 BogoMIPS (preset value used) Memory: 8MB = 8MB total Memory: 6304KB available (783K code, 222K data, 64K init) Dentry cache hash table entries: 4096 (order: 0, 32768 bytes) Inode cache hash table entries: 4096 (order: 0, 32768 bytes) Mount cache hash table entries: 4096 (order: 0, 32768 bytes) Buffer cache hash table entries: 8192 (order: 0, 32768 bytes) Page-cache hash table entries: 8192 (order: 0, 32768 bytes) POSIX conformance testing by UNIFIX Linux NET4.0 for Linux 2.4 Based upon Swansea University Computer Society NET3.039 Initializing RT netlink socket Starting kswapd ttyAM0 at MMIO 0x16000000 (irq = 1) is a WSBN pty: 256 Unix98 ptys configured Serial driver version 5.05c (2001-07-08) with no serial options enabled ttyS00 at 0x03f8 (irq = 10) is a 16450 ttyS01 at 0x02f8 (irq = 10) is a 16450 RAMDISK driver initialized: 16 RAM disks of 208K size 1024 blocksize NET4: Linux TCP/IP 1.0 for NET4.0 IP Protocols: ICMP, UDP, TCP IP: routing cache hash table of 4096 buckets, 32Kbytes TCP: Hash tables configured (established 4096 bind 8192) NET4: Unix domain sockets 1.0/SMP for Linux NET4.0. RAMDISK: ext2 filesystem found at block 0 RAMDISK: Loading 200 blocks [1 disk] into ram disk. done. Freeing initrd memory: 208K VFS: Mounted root (ext2 filesystem) readonly. Freeing init memory: 64K BINFMT_FLAT: Loading file: /sbin/init Mapping is 2b0000, Entry point is 8068, data_start is 8e4c Load /sbin/init: TEXT=2b0040-2b8e4c DATA=2b8e50-2b8e83 BSS=2b8e83-2b8e88 start_thread(regs=0x21f9fa4, entry=0x2b8068, start_stack=0x2affb4) Hello, World! Hello, Marsohod! 

Возможно, самое интересное начинается, когда производишь функциональную симуляцию работы процессора. Это дает возможность «копнуть глубже», узнать то, что недоступно, когда пишешь или дебажишь программу для какого нибудь микроконтроллера или RaspberyPI. Симуляция процессора позволяет рассмотреть в подробности любые сигналы внутри процессора или целой системы.

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

Например, есть система AMBER, портированная для платы Марсоход2. Это тот проект, который я компилирую с помощью Altera Quartus II и потом получившийся образ загружаю в ПЛИС. У модуля самого верхнего уровня SOC AMBER есть входной сигнал тактовой частоты 100Мгц, вход и выход последовательного порта TX/RX, а так же сигналы к микросхеме памяти SDRAM.

Если написать к этой системе тестбенч, то у него уже не должно быть ни входов ни выходов – теперь это будет уже модуль самого верхнего уровня – иммитатор печатной платы с ее компонентами. Тестбенч должен имитировать кварцевый генератор платы, внешний приемопередатчик последовательного порта (возможно посылающий команды через порт), а так же иммитировать микросхему SDRAM. Модели микросхем памяти можно получить у их производителей. По крайней мере Micron выкладывает Verilog модели своих микросхем памяти в свободный доступ, где я ее (модель микросхемы MT48LC4M16A2) и взял.

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

Например, в симуляторе можно видеть циклы исполнения команд процессора, содержимое регистров, служебные сигналы:

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

Вообще, можно полностью симулировать в тестбенче запуск и исполнения ядра Linux.
Я это делал, в консоли командной строки с помощью Icarus Verilog:

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

Я вот тоже мечтаю сделать свой процессор. Думаю этот опыт мне пригодится…

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *