, специальный буфер, позволяющий оптимизировать выполнение стековых команд, и аппаратную реализацию сложных алгоритмов сборки мусора.
Претворение в жизнь основополагающего принципа технологии Java - "написанное однажды работает везде" - стало возможным во многом благодаря появлению виртуальной машины Java (JVM). Именно JVM полностью отвечает за взаимодействие приложений с аппаратурой. При этом код Java предварительно обрабатывается транслятором, встроенным в виртуальную машину. Реализация транслятора может принимать различные формы - от простейших интерпретаторов до гораздо более сложных динамических компиляторов JIT (just-in-time) - но при этом его суть остается неизменной: необходимо преобразовать инструкции виртуальной машины Java (так называемый байт-код) в машинно-зависимые двоичные команды, понятные конкретному процессору.
Основное преимущество байт-кода заключается в возможности создания унифицированного образа программы, который будет одинаково (в принципе) выполняться на любом компьютере, оснащенном виртуальной машиной Java. Однако у этого нового набора команд есть еще два важных достоинства.
Во-первых, байт-код программы в отличие от двоичного машинного кода полностью безопасен. Предварительно он проверяется виртуальной машиной Java. Такая проверка гарантирует корректность программы. Этот этап занимает очень важное место в общей модели безопасности Java, позволяя избежать разрушения и потери данных и свести практически к нулю вероятность сбоя или "зависания" системы в результате выполнения недопустимого кода. Технология Java поддерживает модель сетевых вычислений "толстый сервер - тонкий клиент", в соответствии с которой код формируется в одном месте, хранится и обслуживается - в другом, а для выполнения на локальных станциях распространяется по требованию.
Другим важным преимуществом байт-кода является его высокая плотность или, иными словами, относительно небольшое число байт, необходимое для представления программы. Измерение образов одной и той же программы, написанной на языке Java и С++ и скомпилированной соответственно в виде байт-кода и машинных инструкций, показывает, что объем машинных команд приблизительно в два раза превышает объем байт-кода. (В некоторых случаях разница может оказаться еще более значительной.) Все это не только уменьшает стоимость хранения приложений на Java, но и существенно повышает пропускную способность любой сетевой архитектуры, что особенно важно при работе в беспроводных сетях или в другой медленной сети.
Преобразование байт-кода в машинные команды
Чем сильнее выполняемый модуль программы привязан к платформе, на которой он работает, тем более сложной становится конфигурация приложений, представленных в виде байт-кода. Производительность определяется не только быстродействием платформы, но и эффективностью преобразования (независимо от того, выполняется ли оно интерпретатором или компилятором), с помощью которого инструкции байт-кода переводятся в команды процессора. В первой виртуальной машине Java использовался очень простой (и соответственно, неэффективный) интерпретатор, умещавшийся в постоянной памяти емкостью всего 45 Кбайт и предъявлявший весьма скромные требования к емкости оперативной памяти. Сегодня на смену ему пришли гораздо более сложные (и эффективные) динамические компиляторы, которые будут работать лишь при наличии сотен килобайт постоянной и нескольких мегабайт оперативной памяти. Для выполнения столь масштабных программ требуются мощные компьютеры. Java-процессоры служат именно для того, чтобы преодолеть ограничения производительности. Они должны обрабатывать байт-код со скоростью, присущей сложным динамическим компиляторам, обладая при этом характерной для интерпретаторов низкой ресурсоемкостью.
Что такое Java-процессор?
Java-процессорами называются специализированные процессоры, предназначенные для непосредственного исполнения команд байт-кода Java на аппаратном уровне. Они позволяют обойтись без динамических трансляторов, а использование модели непосредственной обработки кода Java обеспечивает прямую зависимость между быстродействием программы и производительностью базовой платформы. В результате Java становится привлекательным средством для написания многочисленных встроенных приложений: специализированных Web-браузеров, программ для ТВ-приставок, интеллектуальных и мобильных телефонов, персональных цифровых помощников и других карманных устройств, автомобильных компьютеров, интеллектуальных контроллеров, смарт-карт и т.д.
Единственный альтернативный способ непосредственного выполнения Java-программ - прямая компиляция приложений, написанных на Java, в двоичный машинно-зависимый код (без формирования байт-кода). Это обеспечивает достаточную производительность, но разработчики лишаются всех преимуществ байт-кода: независимости от конкретной платформы, безопасности и высокой плотности команд.
В общем случае задача Java-процессоров заключается в том, чтобы расширить возможности динамических трансляторов, а не подменить их. При разумном сочетании обеих технологий (аппаратного механизма прямого выполнения байт-кода и динамических трансляторов) Java-программы смогут функционировать как на мощных компьютерах, так и на недорогих встроенных устройствах.
Конструктивные особенности picoJava
Первый этап разработки Java-процессоров заключался в создании собственно механизма выполнения байт-кода - ядра picoJava. Главным блоком ядра стал модуль целочисленных вычислений. В состав других блоков вошли модуль вычислений с плавающей точкой, а также раздельные буферы для хранения команд и данных (размер каждого из них варьируется от 0 до 16 Кбайт). Для этого ядра при помощи Java-компилятора генерируется файл классов, который содержит код и статические данные приложений.
Базовый байт-код
Набор из 226 команд, определенных для виртуальной машины Java, можно разделить на 15 функциональных категорий (см. табл. 1). Чтобы обеспечить требуемую плотность кода, решено было включить в состав набора команды разной длины. При этом чем чаще используется команда, тем короче ее код. Большая часть из них (62%) умещается всего в один байт. На втором и третьем местах в процентном соотношении находятся двухбайтные (20%) и трехбайтные (15%) команды. И лишь шесть команд (3%) занимают в памяти машины более трех байт. Короткие команды преобладают и в проанализированных реальных программах, преобразованных в байт-код: средняя длина составила 1,8 байта.
Табл.1 Базовый набор команд | |||||
Тип команд | Общее количество | Число команд заданной длины | |||
1 байт | 2 байта | 3 байта | более 3 байт | ||
Поместить константу в стек | 13 | 8 | 2 | 3 | |
Чтение и запись локальной переменной | 82 | 40 | 41 | 1 | |
Управление стеком | 10 | 10 | |||
Арифметические операции | 24 | 24 | |||
Логические операции и операции сдвига | 12 | 12 | |||
Операции ветвления и сравнения | 27 | 5 | 1 | 19 | 2 |
Обработка исключений | 1 | 1 | |||
Управление массивами | 20 | 17 | 1 | 1 | 1 |
Возврат из функций | 7 | 7 | |||
Ветвление | 2 | 2 | |||
Операции преобразования | 15 | 15 | |||
Мониторинг | 2 | 2 | |||
Манипулирование полями объектов | 4 | 4 | |||
Вызов метода | 4 | 3 | 1 | ||
Различные команды управления объектами | 3 | 3 | |||
Итого | 226 | 141 | 45 | 34 | 6 |
Процентное соотношение | 100 | 62 | 20 | 15 | 3 |
Предусмотрены данные следующих типов: байт (byte), короткое целое (short), целое (integer), длинное целое (long), число с плавающей точкой (float), число с плавающей точкой двойной точности (double), объект (object) и адрес возврата (return address). Первый байт команды, как правило, представляет собой код операции, последующие же байты в общем случае являются операндами. В используемой модели данные по умолчанию извлекаются из стека, а результаты выполнения операции помещаются в стек. Поэтому во многих командах операнды просто не указываются (этим и объясняется большое количество однобайтных команд). К примеру, однобайтный код операции iadd полностью определяет команду целочисленного сложения. При этом нет необходимости дополнительно указывать, что с чем складывается, и куда помещается результат. Подразумевается, что оба слагаемых извлекаются из стека, а сумма вновь заносится в стек.
Использование стекового механизма в виртуальной машине Java позволило не только существенно уплотнить код, но и избавиться от необходимости в регистрах. Это повысило переносимость виртуальной машины, поскольку строение регистров одного процессора, как правило, совершенно не похоже на структуру регистров другого.
Отличия от RISC-архитектуры
Байт-код виртуальной машины Java принципиально отличается от набора команд типичного RISC-процессора. В соответствии со своим названием RISC-процессоры характеризуются уменьшенным числом поддерживаемых команд (не более 100). При этом все они имеют фиксированную длину четыре байта (32 бита). Предположим, что у RISC-процессора 32 регистра. Тогда команда add состоит из кода операции (несколько бит) и трех операндов (длиной по 5 бит). Первые два задают регистры слагаемых, а третий - регистр результата. (Оставшиеся биты четырехбайтного слова обычно резервируются для хранения констант, используемых в том случае, когда один из операндов выбирается не из регистра.)
Отличие набора команд виртуальной машины Java от набора команд RISC-процессора обусловлено разными конечными целями. RISC-архитектура разрабатывалась для того, чтобы упростить конструкцию и повысить быстродействие процессора. Виртуальная машина Java создавалась для разработки компактных, безопасных и независимых от платформы программ.
С точки зрения построения Java-процессора совершенно неважно, в какой степени набор команд байт-кода похож на набор команд RISC-процессора. Java-процессоры разрабатываются для уже существующего набора команд байт-кода. Естественно, Java-процессор обязан обрабатывать все 226 команд виртуальной машины Java.
Умышленная неполнота
С точки зрения аппаратуры набор команд байт-кода не только принципиально отличается от набора команд RISC-процессора. В байт-коде намеренно опущена возможность выполнения ряда функций. Например, отсутствуют команды для диагностики и тестирования оборудования, для чтения и записи состояния процессора, для управления регистрами и кэш-памятью процессора. Все эти функции зависят от особенностей конкретных аппаратных средств и не могут быть реализованы в наборе команд, предназначенном для переносимой виртуальной машины.
Ряд других команд отсутствует не столько потому, что они не удовлетворяют ограничениям переносимой виртуальной машины, сколько из-за несоответствия ее основным целям. В модели безопасности Java память воспринимается как черный ящик. Программисты могут создавать объекты, инициализировать их, выгружать из памяти, но при этом они ничего не знают о том, где и как система хранит тот или иной объект. Такой подход делает невозможным написание вирусов, которым необходима информация о распределении памяти виртуальной машиной.
Отсутствуют команды, позволяющие получать прямой доступ к памяти. Команды виртуальной машины Java оперируют только ссылками на объекты и не могут манипулировать их размещением в физической памяти.
Прикладная программа не занимается распределением памяти, поскольку эта задача возложена на виртуальную машину. Однако приложению предоставлена возможность обращаться к объекту по ссылке (ссылка позволяет виртуальной машине Java однозначно определить местоположение любого объекта).
Отметим в то же время, что сама виртуальная машина Java не может быть приложением. Работать в столь узких рамках Java-приложениям позволяет наличие других программ, которые написаны не на Java и не подвержены столь серьезным ограничениям. В конце концов, среда их исполнения состоит не только из виртуальной машины Java, но и из других программ, на которые опирается она сама: кода загрузки, служб ядра, драйверов устройств, обработчиков прерываний, системных библиотек и т.д.
Расширенный байт-код
Архитектура Java-процессора не позволяет модифицировать или выбрасывать команды, определенные в наборе команд виртуальной машины Java. Однако разработчики могут и должны расширять этот набор командами, которые помогают заполнить вышеперечисленные пропуски. Java-процессор - это не переносимая виртуальная машина Java, а реальное вычислительное устройство. Ему нужны команды для диагностики оборудования, и другие команды, необходимые для низкоуровневого управления аппаратными средствами (в том числе и функции прямой выборки и записи информации в произвольные области памяти). В реальном мире для выполнения этой задачи требуется написать большой объем кода на языке, отличном от Java, даже если специализированная машина, предназначенная для обработки Java-команд, может достаточно эффективно интерпретировать код, написанный на другом языке.
В состав набора команд Java-процессора включается целая группа специально разработанных расширенных байт-кодов. В табл. 2 представлены 115 дополнений к 226 байт-кодам, определенным для виртуальной машины Java. Вместе наборы из 341 команды, приведенной в таблицах 1 и 2, полностью описывают архитектуру picoJava ISA (instruction set architecture).
Табл.2 Команды расширенного байт-кода | ||||
Тип инструкций | Общее количество | Число команд заданной длины | ||
1 байт | 2 байта | 3 байта | ||
Диагностика | 8 | 8 | ||
Чтение и запись в регистры | 49 | 49 | ||
Чтение и запись в произвольную область памяти | 35 | 26 | 9 | |
Поддержка других языков | 6 | 5 | 1 | |
Поддержка системного ПО | 17 | 2 | 10 | 5 |
Итого | 115 | 2 | 98 | 15 |
Процентное соотношение | 100 | 2 | 85 | 13 |
Расширенные байт-коды должны ликвидировать бреши, имеющиеся в платформе Java. В свое время разработчики виртуальной машины зарезервировали две команды, не вошедшие в состав 256 байт-кодов, для будущих реализаций специализированных расширений. Большинство расширенных байт-кодов начинаются с одного из двух префиксов. Если декодирующая логическая схема picoJava обнаруживает в начале инструкции любой из этих префиксов, ей становится ясно, что в данном случае речь идет о расширенном байт-коде. Только после анализа второго байта команды инструкция передается на выполнение.
picoJava ISA описывает все байт-коды виртуальной машины Java, а также дополнительные байт-коды, опущенные архитекторами Java намеренно. Благодаря этому Java-процессор способен выполнять не только все то, что позволяет сделать виртуальная машина Java, но и то, чего она сделать не может. Появилась возможность организации контроля за оборудованием и прямым доступом к памяти. Процессор может эффективно выполнять код, написанный на других языках высокого уровня. Короче говоря, архитектура picoJava больше не отождествляется с набором команд виртуальной машины Java. Она представляет собой полный набор команд реальной машины.
По определению, ни одна программа, написанная на Java и корректно преобразованная в байт-код, не может содержать расширенных команд. Расширенные байт-коды, включенные в состав архитектуры picoJava, предназначены для обработки программ, написанных не на Java. Эти программы включают все базовые коды Java (поддерживаемые виртуальной машиной), а также код любого унаследованного приложения, которое было написано на другом языке и должно быть перенесено на платформу Java.
Поскольку расширенные байт-коды могут быть обработаны только процессором picoJava, ни одна программа, в которой используются данные команды, не должна выполняться на другой платформе. Некоторые расширенные байт-коды обеспечивают возможность прямого доступа к памяти, а это противоречит модели безопасности Java. Следовательно, ни одна программа, содержащая расширенные байт-коды, не может считаться безопасной. Другими словами, выполняются на процессоре Java точно так же, как и на любом другом процессоре (программный код привязан к конкретной платформе и не обеспечивает нужной степени безопасности).
Сложность управления
Управлять архитектурой, поддерживающей более 300 инструкций, непросто. Если помните, мы уже говорили о том, что Java-процессоры создавались с целью переноса данной технологии на многочисленные встроенные и персональные устройства. В условиях ограниченных ресурсов либо динамический транслятор окажется чересчур медленным, либо его слишком высокая цена заставит покупателей поискать что-нибудь более доступное. Учитывая особенности целевого рынка, очень важно предложить пользователям компактные и недорогие процессоры. Разработчики picoJava сумели решить эту задачу, разделив все команды на три категории: простые, умеренно сложные и очень сложные.
Простые команды
Простые команды напоминают команды RISC-процессора в том смысле, что все они реализованы на аппаратном уровне. К этой группе относится основная часть команд виртуальной машины Java, а также большинство расширенных байт-кодов. В ядре picoJava все подобные команды выполняются за один такт процессора. Большая часть команд в типичной программе относится именно к этой категории (например, все арифметические операции с целыми числами, а также быстрая загрузка полей объекта).
Умеренно сложные команды
Команды, относящиеся к данной группе, встречаются не так часто, как простые, но и редкими их не назовешь. Группа состоит из 30 команд виртуальной машины Java и нескольких расширенных байт-кодов. Эти команды, напоминающие команды CISC-процессоров, реализованы при помощи микрокодов.
По сравнению с реализацией "в кремнии" микрокод отличается относительно низкой стоимостью. В компактное постоянное запоминающее устройство заносятся последовательности управляющих сигналов, требуемые для выполнения таких команд. В ядре picoJava имеется два постоянных запоминающих устройства; емкость каждого из них равна приблизительно 2 Кбайт. Первое используется для целочисленных вычислений, второе - для вычислений с плавающей точкой. Микрокод обеспечивает нужный баланс между простотой аппаратной реализации и экономичностью. Некоторые реализованные на базе микрокода команды выполняются всего за несколько тактов (например, для команды iaload достаточно трех), в то время как другим требуется гораздо большее количество тактов (команде invokestatic_quick нужно 11 тактов, а команда invokesuper_quick выполняется за 21).
Очень сложные команды
Последняя группа состоит приблизительно из 30 команд. К ней относятся все оставшиеся байт-коды, определенные для виртуальной машины Java. Они отличаются повышенной сложностью или обращаются к службам нижележащей операционной системы. К примеру, инструкция new, используемая для создания нового объекта, удовлетворяет обоим этим критериям. Она достаточно сложна, выполняет сложную процедуру определения класса нового объекта, просматривая список системных классов, и требует выделения объекту необходимого фрагмента памяти. Если файл с описанием класса создаваемого объекта еще не загружен, виртуальная машина должна найти файл класса в сети или в локальной файловой системе. Выделение памяти для нового объекта также координируется операционной системой.
Поскольку относящиеся к данной категории команды отличаются высокой сложностью и предъявляют повышенные требования к гибкости реализации (вследствие зависимости от операционной системы), их, как правило, предпочитают реализовывать программно. При обращении программы к одному из таких байт-кодов процессор генерирует прерывание instruction emulation (эмуляция команды). В зависимости от того, какой именно байт-код вызвал прерывание, обработчик исключительных ситуаций вызывает соответствующую последовательность команд, реализованных аппаратно или микропрограммно.
Безусловно, подобные команды выполняются очень медленно и требуют нескольких сотен или даже тысяч тактов. Но и несмотря на это данный подход вполне оправдан. Подобно picoJava, любому интерпретатору или динамическому компилятору время от времени приходится синтезировать очень сложные процедуры, состоящие из длинных последовательностей более простых операций. Если процессор picoJava встречает один из подобных байт-кодов (к счастью, они довольно редки), то время его выполнения вполне сравнимо с тем временем, которое затрачивает на обработку того же байт-кода динамический транслятор. Технология picoJava имеет в данном случае даже некоторое преимущество, поскольку эмулирующая процедура не только заранее загружена, но и вызывается мгновенно путем генерации аппаратного прерывания.
Более эффективная работа со стеком
Следующим препятствием, с которым пришлось столкнуться разработчикам picoJava, была присущая стековой машине малая эффективность. С точки зрения создателя процессора тот факт, что стековая архитектура делает программы компактными, безопасными и переносимыми, гораздо менее важен по сравнению с крайне низкой скоростью выполнения стековых команд. Стековая машина тратит дополнительные такты на то, чтобы сначала переместить операнды на вершину стека (этого требует формат команд), а затем сохранить результат, находящийся на вершине стека, в основной памяти. В отличие от этого регистровая машина не тратит время и ресурсы на перенос данных из одной области памяти в другую, так как регистровый файл позволяет одновременно обращаться сразу к нескольким значениям. Исследования показывают, что объем вычислений в стековой машине при выполнении одной и той же последовательности действий в среднем на 30% превышает объем вычислений в регистровой машине [2]. На рис.1 приведен простой пример, наглядно иллюстрирующий различия между двумя подходами.
Стековый регистровый файл
Поскольку полное заимствование набора регистровых команд в picoJava было неприемлемым, его создателям необходимо было найти разумное сочетание набора байт-кодов и эффективной регистровой модели. Решить эту задачу позволил регистровый файл, организованный таким образом, чтобы сделать возможной стековую обработку. На рис.2 изображен регистровый файл, который используется для буферизации рассчитанного на 64 элемента стека. Ядро picoJava представляет регистровый файл в виде циклического буфера с указателем на вершину стека. Как только исполнительное устройство заносит в вершину стека новый элемент, стек расширяется, а его указатель уменьшается на единицу. Если элемент извлекается из стека, стек сжимается, а указатель увеличивается на единицу. (Следуя принятой традиции, стек picoJava направлен в памяти "сверху вниз"; сначала заполняются старшие адреса, а затем младшие.)
Циклическая организация означает, что дно стека непосредственно смыкается с его вершиной. Таким образом, 65-й элемент, занесенный в стек, замещает элемент, который был помещен на вершину первым (до этого момента последнюю точку входа в стеке).
Регистровый файл имеет три порта чтения и два порта записи. Одновременно можно считывать два операнда и записывать результат. В фоновом режиме могут выполняться также операции наполнения и извлечения из буфера. Тем самым оставшиеся порт считывания и порт записи своевременно обеспечиваются необходимой информацией. (Команда извлечения spill переносит элемент из регистрового файла в буфер данных, освобождая пространство для новых значений. Команда наполнения fill, напротив, перемещает элемент, используемый в вычислениях, из буфера данных в регистровый файл.)
Из области хранения констант, области локальных переменных и области объектов данные поступают в стек. При выполнении любой вычислительной операции операнды извлекаются из стека, а результат заносится обратно в стек. При этом все исходные и результирующий операнды находятся в стековом регистровом файле.
Верхняя и нижняя граница
Несмотря на то, что стек реализован в виде регистрового файла, он позволяет избежать непредсказуемого переполнения, присущего обычному массиву регистров. При помещении каждого нового значения на вершину стека тот расширяется; когда конечные результаты снимаются с вершины стека, он сжимается. Независимо от того, расширяется стек или сжимается, вся информация о движении элементов доступна и может использоваться.
В конце концов число элементов стека в результате накопления результатов в нем может превысить программно задаваемый уровень верхней границы. В этом случае срабатывает триггер механизма извлечения и самые старые промежуточные результаты переносятся в буфер данных. Физический размер стека ограничен 64 ячейками, но данная технология позволяет сделать процесс расширения фактически бесконечным (при этом элементы, находящиеся на дне стека, записываются в основную память, а их место занимают другие значения). Таким образом, фоновый механизм извлечения создает иллюзию неограниченного размера регистрового файла.
В процессе сжатия аналогичный механизм наполнения препятствует полному опустошению стека и предотвращает возможный простой процессора. Стек постоянно пополняется новыми элементами. По мере извлечения данных из вершины стека число элементов может опуститься ниже заранее определенной границы. В этом случае срабатывает триггер механизма фонового наполнения, и информация из основной памяти начинает поступать в стек. Механизм извлечения снимает ограничение на количество свободных регистров, предназначенных для размещения новых элементов. Аналогичным образом механизм наполнения создает иллюзию неиссякаемости заполненных регистров.
Зацепление команд
Важной особенностью стека picoJava является то, что он позволяет избежать характерной для стековых машин неэффективности доступа. Поскольку стек фактически представляет собой регистровый файл с возможностью произвольного доступа, конвейер picoJava может обращаться не только к двум верхним элементам стека, но и ко всем остальным. Это создает необходимые предпосылки для применения технологии, называемой зацеплением команд (instruction folding).
Еще раз взглянем на приведенную на рис.1 последовательность, реализующих операцию сложения. Оба суммируемых значения находятся в стеке (в области параметров и локальных переменных текущего метода). Но процессор работает только с вершиной стека, а нужные элементы находятся во внутренних ячейках. Следовательно, процессор предварительно должен затратить несколько тактов на их перемещение их на вершину стека. Команда локальной записи не может вынести результат суммирования за границы стека, а лишь перемещает полученное значение обратно в область переменных текущего метода. В большинстве случаев все эти локальные перемещения происходят в пределах 64 верхних элементов стека. Таким образом, само собой напрашивается объединение всех последовательных действий в единую команду.
Если машина получает прямой доступ ко всем 64 элементам стека, перемещения теряют всякий смысл. Операция сложения может выбирать операнды непосредственно из области локальных переменных текущего метода (используя два порта чтения из стека). Соответственно, и результат может напрямую записываться в область локальных переменных (для этого служит порт записи). На рис.3 показаны детали этого процесса.
В общем случае ядро picoJava оперирует командами байт-кода следующим образом. На основе анализа потока байт-кода выделяются группы команд, объединяемые в единое целое. Эти последовательности состоят из четырех команд байт-кода, которые позволяют:
- переместить локальные данные на вершину стека и выполнить нужную операцию;
- записать результат выполнения операции в отведенное для него место.
Если ядро обнаруживает подобную последовательность команд, оно синтезирует одну регистровую операцию наподобие команды RISC-процессора. Операнды извлекаются из области хранения локальных переменных, над ними производится определенное действие, после чего результат вновь записывается в область локальных переменных.
Другими словами, picoJava помещает служебные команды манипулирования стеком, необходимые для выполнения вычислительной операции, непосредственно в целевую команду. Это снимает со стековой машины значительную часть нагрузки и позволяет выполнять больше команд за один такт. Таким образом, архитектура picoJava имеет много общего с архитектурой RISC-процессоров: поддерживает регистровый файл из 64 элементов, и обеспечивает выполнение регистровых команд с тремя параметрами.
Результаты измерений производительности
В табл. 3 приводятся полная информация о измерении производительности picoJava на наборе задач Dhrystone 2.1. Полученные данные сравниваются с результатами, показанными типичным RISC-процессором. Используя стандартный набор регистровых команд с тремя операндами, RISC-процессор за десять циклов способен выполнить девять команд фиксированной длины (длина каждой команды в данном случае равна четырем байтам). Для выполнения той же самой задачи ядро picoJava использует набор из 18 команд переменной длины. Но поскольку средний размер команды байт-кода более чем вдвое меньше команды фиксированной длины, последовательность байт-кодов занимает всего 28 байт (в то время как команды RISC-процессора занимают 36 байт). Кроме того, за счет зацепления команд picoJava позволяет выполнить 18 команд всего за девять циклов процессора (в среднем на две операции тратится один такт).
Табл.3 Сравнение потоков команд RISC-процессора и ядра picoJava II при выполнении тестов Dhrystone 2.1 | ||
Номер такта | Команды RISC-процессора | Команды picoJava II |
1 | MOV R3,#0 | iconst_0+istore_3 |
2 | ADD R2,R1,#10 | iload_1+bipush 10+iadd+istore_2 |
3 | LDR R5,[R0+#d1] | aload_0+getfield CharlGlob |
4 | - | - |
5 | CMP R5,#65 | bipush 65+if_icmpne L28 |
6 | BNE L28 | aload_0+getfield IntGlob |
7 | LDR R5,[R0+#d2] | iinc 2,255 |
8 | SUB R2,R2,#-1 | iload_2+isub+istore_1 |
9 | SUB R1,R5,R2 | iconst_1+istore_3 |
10 | MOV R3,#1 |
Исследования показали, что в различных Java-приложениях от 23 до 37% команд (в среднем 28%) можно зацепить с другими командами. Подобное зацепление позволяет устранить практически все недостатки предыдущих моделей стековой обработки. Зацепление команд обеспечивает существенное повышение производительности практически всех операций, поддерживаемых ядром picoJava, независимо от того, на каком языке была написана программа - на ассемблере, Си или Java.
Аппаратные средства поддержки выполнения
Еще одной специфической задачей, которую пришлось решать команде разработчиков picoJava, стало обеспечение аппаратной поддержки байт-кода Java на этапе выполнения. Наибольшие сложности были связаны с эффективным управлением памятью виртуальной машины Java.
Несмотря на то, что Java-приложение способно создавать объекты, манипулировать ими и переставать их использовать, оно не может напрямую обращаться к памяти. Виртуальная машина Java предоставляет программе возможность динамически выделять фрагмент памяти нужного размера и освобождать неиспользуемые более фрагменты. Избавление программиста от необходимости управлять памятью значительно повышает его продуктивность и увеличивает надежность приложений. Ведь большая часть ошибок в программах связана именно с неверным распределением и несвоевременным освобождением памяти, а также с обращениями по недопустимым адресам.
Простая сборка мусора
Для освобождения неиспользуемой памяти и возвращения ее системе виртуальная машина Java использует технологию, за которой закрепился очень точный (хотя и не слишком элегантный) термин "сборка мусора". В простейшем случае сборщик мусора периодически сканирует память, выискивая объекты, к которым могла бы обращаться одна из выполняющихся программ. Найденные объекты специальным образом помечаются. После того, как все потенциально работающие объекты помечены, оставшаяся память освобождается. После этого сборщик мусора помещает все свободные области памяти в общий пул. Чтобы избежать фрагментации, используемые объекты перемещаются и располагаются компактно, непосредственно друг за другом; после этого свободные области также образуют непрерывный блок.
До завершения работы сборщика мусора (то есть до того момента, когда все ненужные объекты будут удалены, актуальные данные перенесены на новое место, а ссылки обновлены) система находится в неустойчивом состоянии. Сборка мусора может производиться автоматически; в этом случае процесс продолжается непрерывно до полного завершения.
При работе с памятью большой емкости простая схема сборки мусора существенно снижает производительность. В ее ходе все прочие вычисления приходится приостанавливать. Это не только уменьшает общее быстродействие, но и делает невозможной обработку в режиме реального времени.
Сборка мусора нескольких "поколений"
Низкую производительность простых сборщиков мусора призвана увеличить более сложная схема, основанная на выделении нескольких "поколений" памяти. В этом случае всякий раз сканируется лишь относительно небольшая область. В результате процесс завершается гораздо быстрее, общая производительность заметно увеличивается, появляется возможность обрабатывать информацию в режиме реального времени.
Подобная схема основана на том, что большинство объектов живут относительно недолго. Исследования доказали справедливость такого предположения. За то время, пока программе потребуются очередные 32 Кбайт памяти, 90% вновь созданных объектов уже прекратят свое существование [3].
Все новые объекты помещаются в сравнительно небольшую область памяти, "инкубатор". Регулярно проверяя ее, сборщик мусора удаляет из системы все ненужные объекты. Объекты, прошедшие стадию инкубационного периода, имеют все шансы стать "долгожителями". Чтобы избежать переполнения инкубатора, устойчивые объекты перемещаются в память долговременного хранения. Более сложная схема предусматривает выделение нескольких постепенно стареющих поколений объектов. Данные, успешно пережившие инкубационный период, перемещаются во "взрослую" область, а затем - в области еще более старших поколений.
Данная схема не позволяет полностью избавиться от необходимости сканирования всей памяти, поскольку количество устойчивых объектов со временем растет и в конце концов они могут заполнить собой всю память. В определенный момент сборщик мусора будет вынужден проверять память целиком, включая области устойчивых объектов. Но необходимость полного просмотра - все же скорее исключение, чем правило. Сканирование небольших инкубационных областей позволяет удалить большую часть ненужных объектов. Концепция поколений применяется и в более сложных алгоритмах сборки мусора, например, в так называемом "алгоритме поезда". Таким образом, максимальное время процесса сборки мусора все же существенно уменьшается [4,5].
Сегментация
Для реализации схемы поколений (или любой другой поэтапной технологии) необходимо разбить память на небольшие сегменты, позволяющие ускорить процесс сканирования и компоновки. Эти сегменты и будут играть роль поколений (или роль "вагонов" в алгоритме поезда).
Для быстрой проверки какой-либо области памяти на наличие актуальных объектов механизм сборки мусора должен иметь список всех объектов сегмента и обладать информацией о связях этих объектов с данными, хранящимися в других сегментах. В противном случае сборщику мусора придется просмотреть всю оставшуюся память и проверить, нет ли ссылок на проверяемые объекты в других областях. Если такие ссылки имеются, объект удалять нельзя.
Основной механизм для ведения такого списка называется "барьером записи". Технология барьера записи позволяет оперативно отыскать все указатели на данный объект, хранящиеся в других сегментах. В случае обнаружения подобных указателей объект сохраняется. При работе с системой поколений ищутся ссылки между разными поколениями.
picoJava обеспечивает гибкий метод определения границ сегментов. После того, как сегменты определены, процессор просматривает все указатели и проверяет, не ссылается ли указатель на объект, расположенный в другом сегменте. Если это так, соответствующая пометка делается в списке сборщика мусора.
В других архитектурах приходится проводить дополнительную программную проверку указателей и их связей с другими поколениями. Это может привести к значительным издержкам при сборке мусора. Возможность проверки ядром picoJava всех указателей с использованием аппаратных, а не программных средств позволяет проектировать сложные механизмы "сборки мусора", минимизирующие задержки.
Ядро picoJava имеет встроенные аппаратные средства поддержки этапа выполнения и обеспечивает повышение скорости выполнения других важнейших функций виртуальной машины Java (например, управления потоками и манипулирования объектами). В итоге это обеспечивает существенный рост быстродействия программ при минимальных потерях памяти и незначительном увеличении стоимости и энергопотребления ядра picoJava по сравнению с другими процессорными архитектурами.
Ядро picoJava позволяет выполнять команды байт-кода непосредственно на аппаратном уровне и исключить из арсенала пользователей динамические трансляторы. Теперь даже портативные устройства с ограниченным вычислительными ресурсами можно оснащать программами, представленными в виде байт-кода. К настоящему времени Java-процессоры (одним из них является представленный самой Sun процессор microJava 701), обладающие высокой производительностью и минимальной ресурсоемкостью, готовы выпускать целый ряд компаний, получивших лицензии на технологию picoJava.
Harlan McGhan, Mike O'Connor. PicoJava: A Direct Execution Engine For Java Bytecode. IEEE Computer, October 1998, pp. 22-30 Reprinted with permission, Copyright IEEE CS.
All rights reserved.
Об авторах
Харлан Макгэн - руководитель группы технического маркетинга Sun Microelectronics. Долгие годы он занимался разработкой высокопроизводительных микропроцессоров: возглавлял коллектив программистов в National Semiconductor Corp. во время проектирования одних из первых в мире 32-разрядных процессоров Series 32000, затем руководил софтверным подразделением в компании Intergraph (в то время Fairchild) в период создания процессора Clipper.
Майк О'Коннор - главный архитектор второго поколения ядра picoJava II и один из ведущих разработчиков ядра picoJava I. Его научные интересы связаны с архитектурой компьютеров, параллельной разработкой программных и аппаратных решений.
Электронную почту авторам статьи можно направлять по адресам harlan.mcghan@eng.sun.com и mike.oconnor@eng.sun.com.
После того, как значения помещены в регистровый файл, основанная на регистрах RISC-машина может выполнить сложения за одну команду (a), в качестве операндов которых указываются два регистра-слагаемых и регистр, в который помещается сумма. В отличие от этого сложение с использованием стека (b) выполняется за четыре команды. Сначала два слагаемых из локальных переменных заносятся в стек. Затем значения на вершине стека суммируются, а результат записывается в еще одну локальную переменную:
ADD R3, R2, R1 a) ILOAD_1 ILOAD_2 b) IADD ISTORE_3
Литература
[1.] C.Mangione, "Just In Time for Java vs. C++", NC World, Jan.1998; http://www.ncworldmag.com/ncworld/ncw-81-1998/ncw-01-jperf.html.
[2.] W.Wulf et al., The Design of an Optimizing Compiler, American Elsevier, New York, 1973.
[3.] D.Ungar, "Generation Scavenging: A Nondisruptive High Performance Storage Reclamation Algorithm", ACM SIGPLAN Notices, Apr. 1984, pp. 157-167.
[4.] R.Hudson and J.E.B.Moss, "Incremental Garbage Collection for Mature Objects", Proc. Int'l Workshop on Memory Management, Springer-Verlag, Amsterdam, 1992, pp.388-408.
[5.] S.Grarup and J.Seligmann, "Incremental Mature Garbage Collection", masters of science thesis, Computer Science Dept., Aarhus Univ., Aug.1993.