К.В. КОНОНЕНКО Московский инженерно–физический институт (государственный университет) ИССЛЕДОВАНИЕ ГЛОБАЛЬНОГО РАСПРЕДЕНИЯ РЕГИСТРОВ В ПРОГРАММАХ НА MICROSOFT COMMON INTERMEDIATE LANGUAGE C ПОМОЩЬЮ АЛГОРИТМА ЛИНЕЙНОГО СКАНИРОВАНИЯ И ВИРТУАЛЬНОЙ МАШИНЫ БИБЛИОТЕКИ LIBJIT Представлены результаты исследования алгоритма линейного сканирования для программ виртуальной машины Microsoft Сommon Intermediate Language. Разработан новый генератор объектного кода для виртуальной машины библиотеки libJIT. В основе исходных кодов [1] настоящей работы использованы исходные коды библиотеки LibJIT [2], первоначально созданной Ризом Везерли для Фонда Свободного Программного Обеспечения. Дизайн библиотеки LibJIT содержит обширный набор средств, которые заботятся о процессе компиляции во время выполнения программы, не связывая программиста с языком или специфическими особенностями байт-кода. В отличие от других систем, таких как JVM, .NET, Parrot и LLVM, LibJIT – это фундамент для создания большого числа виртуальных машин, динамических скриптовых языков. Большая часть работы над компилятором на лету касается арифметики, преобразования типов, записи и чтение из памяти, циклов, проведения анализа графа потока данных, распределения регистров, и генерации выполняемого машинного кода. Только очень маленькая часть работы касается языковых специфических особенностей. Цель проекта LibJIT состоит в том, чтобы обеспечить набор средства компиляции на лету, не связывая программиста с языковыми специфическими особенностями. В работе [3] было произведена разработка поддержки базового алгоритма линейного сканирования [4] для распределения регистров в LibJIT. В настоящей работе представлено дальнейшее исследование и разработка данного алгоритма для применения в библиотеке LibJIT для процессора IA-32. В настоящей работе произведены дальнейшие исследования и разработка различных вариантов быстрых алгоритмов распределения регистров в программах для виртуальной машины CIL. Библиотека LibJIT поддерживает множество платформенно независимых функций с префиксом jit_insn_*. Последовательность вызванных функций генерирует тело программы во множестве инструкций в SSA представлении. Например, при синтаксическом анализе исходной программы для некоторой виртуальной машины вызывается последовательность функций LibJIT, отображающая исходную программу на байт-коде во внутреннее представление LibJIT. Данное представление компилируется и исполняется при вызове функции jit_function_apply или если функция установлена для компиляции во время еѐ вызова из другой функции. На первом этапе происходит анализ промежутков жизни переменных (liveness analysis). В самом простом случае использования быстрого алгоритма распределения регистров если в функции имеется обработчик исключений, то устанавливается максимальный размер промежутка жизни всех переменных попавших на пересечение функции _setjmp. Быстрый алгоритм заключается в том, что сначала для всех переменных программы в SSA представлении промежуток жизни устанавливается с момента первого использования до последнего использования. Затем происходит итеративный процесс увеличения промежутка жизни переменных с использованием графа управления до момента, когда промежутки жизни всех переменных больше не изменяются. Например, если в середине жизни переменной существует условный переход в некоторую точку программы, то промежуток жизни переменной расширяется данный точки направления перехода. В конце анализа получается множество “критических точек”, в которых сохранены два списка: один с множеством создаваемых переменных и второй с множеством переменных используемых в последний раз. Распределение регистров происходит в два этапа: глобальное и локальное. Алгоритм линейного сканирования производит глобальное распределение регистров в функции на основе промежутков жизни переменных. Данный алгоритм работает до момента генерации кода. Основы его работы показаны в работе [1]. Однако внесены некоторые изменения. Процессоры регистров общего назначения используются для типов размерности менее 32 бита, для типов данных размерности 64 бита используются пары регистров, для 32 битных и 64 битных чисел с пла- вающей точкой используются регистры XMM расширения процессора SSE. Программа анализируется сначала до конца за один проход. Например, если встречается 32 битное значение, то для него выделяется свободный регистр, при недостатке регистров в память стека сохраняется переменная с наиболее дальним окончанием жизни от текущей инструкции. В конце анализа оказывается, что для некоторых переменных выделены регистры, для других будет использоваться место в локальном стеке. Если в программе инструкция берѐтся адрес некоторой переменной, то для данной переменной всегда используется место в стеке. Некоторые инструкции процессора IA-32 предполагают, использование некоторых регистров или разрушают их содержимое. Самым простым вариантом является сохранение содержимое данных регистров вокруг таких инструкций, однако более элегантным решением оказывается использование понятие “дырок” в жизни регистров. Если для генерации машинного кода некоторого опкода может использоваться некий регистр или разрушается его содержимое, то в данном опкоде для регистра появляется “дырка”. Если в промежутке жизни переменной попадает “дырка” в жизни регистра, то такой регистр не может использоваться для данной переменной. Таким образом, не приходится сохранять такие регистры. Эксперимент также показывает, что данный подход также даѐт лучшее качество кода и лучшую скорость выполнения получаемого объектного кода. Разработанный новый алгоритм распределения регистров в среде Portable.NET Just-In-Time compiler 0.8.0 Алгоритм распределения регистров в среде Portable.NET Just-In-Time compiler 0.8.0 Sieve (решение задачи «решета») 20439 17499 Loop(циклы) 28311 24976 Logic(булева логика) 57311 55647 String(работа со строками) 16586 16651 Float (работа с 2642 числами с плавающей точкой) 1940 Method (вызов методов) 32426 30401 PnetMark (средний показатель) 19005 16970 Таблица 1. Сравнение производительности алгоритмов распределения регистров в тесте PnetMark (измерения в числе вызовов метода за единицу времени) В критических точках происходит обновление информации о выделенных для переменных регистрах и свободном месте в стеке. Далее вызывается обработчик операционного кода, в котором происходит работа алгоритма локального распределения регистров и генерация объектного кода. Алгоритм локального распределения регистров поддерживает несколько типов рас- пределения регистров: распределение регистров для входных и выходных переменных опкода, если для них на данный момент используется память в стеке; распределение регистров для механизма алиасинга; распределение временных регистров необходимых для генерации машинного кода. Функция распределения регистров поддерживает возможность задавать условия найденным регистрам. Данная функция работает в четыре этапа, каждый из которых следует, если предыдущий этап не смог найти свободный регистр: 1. ищется свободный регистр неиспользованный ни глобальным, ни локальным алгоритмом распределения регистров 2. ищется свободный регистр неиспользованный глобальным алгоритмом распределения регистров 3. ищется регистр сохранѐнный локально 4. ищется регистр, не сохранѐнный пока локально (кроме механизма алиасинга). При этом выделяется свободное место в стеке и временно сохраняется содержимое регистра. Содержимое регистра восстанавливается, если переменная, для которой глобально выделен данный регистр, используется в качестве входной переменной опкода. Если переменная используется в качестве выходного значения, то представление переменной в стеке синхронизируется. Если текущий операционный код выполняет операцию условного/безусловного перехода, то содержимое всех временно сохранѐнных переменных восстанавливается. Однако информация об использованных локальных регистрах не разрушается для использования. Информация об использованных регистрах разрушается в конце данного блока операционных кодов, если вход в следующий блок операционных кодов не происходит из предыдущего блока или происходит из другого блока. Механизм поддержки “дырок” в жизни регистров является действительно важным для синхронизации механизма локального распределения регистров и глобального механизма распределения регистров. Если промежуток жизни переменной попадает в “дырку” некоторого регистра, то данный регистр не может быть выделен глобально для данной переменной. Содержимое регистра восстанавливается, если при генерации машинного кода для данного опкода будет использоваться данный регистр, например, при операции деления разрушается информация о локально выделенных регистрах EAX, EDX. Приведѐм примеры использования локального распределения регистров. Например, найдено при генерации машинного кода опкодов LOAD/STORE_RELATIVE_* и LOAD/STORE_ELEMENT_* всегда используются регистры для входных и выходных переменных. Данные опкоды выполняют операции доступа и записи в память. Для реализации возможности индексированного перехода (конструкция JUMP_TABLE) найдено целесообразным введением “дырки” для регистра EAX. Таким образом, EAX можно всегда использовать для хранения входного значения регистр. Другой интересный случай для функции _sigsetjmp, для неѐ устанавливаются дырки для регистров EAX, EDX, ECX. Тесты производительности разработанного генератора кода и старого генератора кода, который использует только локальное распределение регистров, показывают, что изменение теста PnetMark достигают в среднем 12%. СПИСОК ЛИТЕРАТУРЫ 1. LibJIT Linear Scan Register Allocator // http://code.google.com/p/libjit-linear-scan-register-allocator/ 2. Страница о библиотеке libJIT в энциклопедии Wikipedia // http://en.wikipedia.org/wiki/LibJIT 3. Реализация алгоритма линейного сканирования с использованием расширений SIMD процессора IA-32 в библиотеке LibJIT; Научная сессия МИФИ-2008. XI Московская международная телекоммуникационная конференция студентов и молодых ученых. 4. Linear Scan Register Allocation, M. Poletto and V. Sarkar; ACM Transaction Programming Languages & Systems, Vol. 21, No. 5, September 1999, Pages 895-913, ISSN:0164-0925; ACM Press