ISSN 0321-2653 IZVESTIYA VUZOV. SEVERO-KAVKAZSKIIREGION. TECHNICAL SCIENCE. 2017. No 3
УДК 519.685.3+ DOI: 10.17213/0321-2653-2017-3-64-69
ПОВЫШЕНИЕ ЭФФЕКТИВНОСТИ ОПТИМИЗАЦИИ КОМПИЛЯТОРА ПО УПЛОТНЕНИЮ КОДА
© 2017 г. И.Р. Скапенко, Д.В. Дубров
Южный федеральный университет, г. Ростов-на-Дону, Россия
INCREASING THE EFFICIENCY OF THE COMPILER OPTIMIZATION ON CODE COMPACTION
I.R. Skapenko, D.V. Dubrov
Southern Federal University, Rostov-on-Don, Russia
Скапенко Илья Русланович - магистрант, Институт математики, механики и компьютерных наук имени И.И. Воровича, Южный федеральный университет, г. Ростов-на-Дону, Россия. E-mail: skapenko@sfedu.ru
Дубров Денис Владимирович - канд. физ.-мат. наук, доцент, кафедра «Информатика и вычислительный эксперимент», Институт математики, механики и компьютерных наук имени И.И. Воровича, Южный федеральный университет, г. Ростов-на-Дону, Россия. E-mail: dubrov@sfedu.ru
Skapenko Ilya Ruslanovich - Master student, I.I. Vorovich Institute of Mathematics, Mechanics, and Computer Science, Southern Federal University, Rostov-on-Don, Russia. E-mail: skapenko@sfedu.ru
Dubrov Denis Vladimirovich - Candidate of Physical and Mathematical Sciences, Associate Professor, department «Informatics and Computational Experiment», I.I. Vorovich Institute of Mathematics, Mechanics, and Computer Science, Southern Federal University, Rostov-on-Don, Russia. E-mail: dubrov@sfedu.ru
В настоящее время существует множество вычислительных систем с ограниченным количеством памяти. Автоматическое уменьшение размера программного кода может избавить разработчиков программного обеспечения от большого количества рутинной работы и необходимости реализовывать программы на языках низкого уровня. Целью настоящей работы является реализация межпроцедурных оптимизаций уменьшения размера кода на основе библиотеки LLVM, а также изучение их эффективности. В работе рассматривается межпроцедурная оптимизация вынесения одинаковых базовых блоков в отдельную функцию, которая реализуется на уровне промежуточного представления LLVM.
Ключевые слова: оптимизирующий компилятор; уплотнение кода; LLVM; SSA-форма; межпроцедурные оптимизации; слияние функций.
Currently, there exist many computational systems with a limited amount of RAM. Automatic program code reduction could save the developers from large amounts of routine work and the necessity to implement programs in low-level languages. The goal of the current work is implementing inter-procedural optimizations of code size reduction based on LLVM library and studying their efficiency. An inter-procedural optimization factoring out identical basic blocks into separate functions, which is implemented on the LLVM intermediate internal representation level, is considered in the work.
Keywords: optimizing compiler; code compaction; LLVM; SSA form; inter-procedural optimizations; function merging.
Введение
В настоящее время наблюдается значительное увеличение количества устройств, основанных на встраиваемых системах, таких как предметы носимой электроники, бытовые, промышленные устройства. По прогнозам Gartner, общее количество устройств Интернета вещей во всём мире к 2020 г. должно увеличиться втрое по сравнению с 2016 г., достигнув 20 млрд штук [1]. Одной из главных технологических проблем во
внедрении таких устройств считается сложность разработки программного обеспечения [2]. Код, создаваемый компиляторами языков высокого уровня, часто бывает практически непригоден для использования во встраиваемых системах из-за его избыточности. Это заставляет программистов переходить на использование языков низкого уровня, что усложняет процесс разработки. В некоторых ситуациях уменьшение размера программы может оказаться необходимым также для систем с большим объёмом оперативной
ISSN 0321-2653 IZVESTIYA VUZOV. SEVERO-KAVKAZSKIIREGION.
TECHNICAL SCIENCE. 2017. No 3
памяти по причинам оптимизации скорости: код меньшего размера может целиком поместиться в кэше процессора, что позволит ускорить доступ к памяти. Либо код может занять небольшое количество страниц виртуальной памяти, при этом все требуемые физические адреса переходов могут целиком уместиться в TLB-кэше процессора, что ускорит работу блока управления памятью и избавит от необходимости загрузки лишних страниц. В современных компиляторах реализованы основные методы оптимизации программ, направленные на умень-шение размера кода. К сожалению, такие преобразования являются, как правило, машинно-зависимыми [3], поэтому не могут быть легко переносимыми на другие платформы. Некоторые стандартные преобразования кода, направленные на устранение избыточных вычислений (например, удаление общих подвыражений и «подъём кода» [4, 5]), также могут привести к уменьшению конечного размера программы, однако их эффективность для этой цели на практике невысока.
Целью настоящей работы является реализация оптимизирующего преобразования компилятора, уменьшающего размер кода и при этом максимально не зависимого от конечной архитектуры. В качестве основы для выполняемой работы была использована библиотека для построения компиляторов LLVM [6]. Она была выбрана ввиду открытости её исходных кодов, наличия большого количества инструментов на её основе (компилятор clang и другие), наличия документации и других преимуществ. Необходимость уменьшения размера кода в семействе компиляторов LLVM появилась с момента создания кодогенератора для архитектур ARM, MIPS, AVR и других, которые активно используются во встраиваемых системах. Благодаря грамотной архитектуре LLVM (рис. 1), имеется возможность писать оптимизации, не зависящие от языка программирования и конечной архитектуры на уровне промежуточного представления (LLVM IR). Также возможно написание низкоуровневых архитектурно-зависимых оптимизаций на уровне машинных команд (LLVM MIR). Таким образом, требуемое преобразование должно быть реализовано в виде прохода LLVM. В качестве метода уменьшения размера программы в данной работе было выбрано уплотнение кода путём слияния одинаковых базовых блоков и вынесения их в отдельные функции (так называемая процедурная абстракция базовых блоков) [7]. Исходный код данной оптими-
зации для ШУМ 4.0 выложен авторами в открытый доступ на ресурсе ОйНиЬ [8].
Clang C/C++/ ObjC Fronted
LLVM X86 Backend
Fortran
Haskell
llvm-gcc LLVM LLVM PowerPC Backend
Fronted Optimizer
GHC Fronted
LLVM ARM Backend
■ X86
-PowerPC
• ARM
Рис. 1. Структура семейства компиляторов LLVM / Fig. 1. Structure of LLVM compiler family
Процедурная абстракция базовых блоков
В LLVM существуют различные виды проходов, но для задач межпроцедурных оптимизаций на уровне LLVM IR подходит тип прохода ModulePass [9], так как только из данного типа имеется возможность просматривать и изменять содержимое функций во внутреннем представлении.
Особенностью промежуточного кода LLVM IR является его представление в виде SSA-формы [5], которая предполагает разбиение программы на линейные участки - «базовые блоки», каждый из который заканчивается одной из завершающих инструкций - «Terminator-инструкция», а начинаться может несколькими Phi-инструкциями. Из-за этого в процессе преобразования невозможно заменить базовые блоки исходной программы целиком, поскольку Phi-инструкции используют значения других базовых блоков, доступ к которым возможен только внутри функции. Terminator-инструкции обязательно должны находиться в конце корректно сформированных базовых блоков. Поэтому в реализованном преобразовании Phi- и Terminator-инструкции не рассматриваются при работе с базовыми блоками, и при вынесении базовых блоков эти инструкции не заменяются. В SSA-форме также невозможна реализация напрямую других преобразований по уменьшению размера кода, например, tail call [4].
Пример применения оптимизации абстрагирования базовых блоков на уровне LLVM IR с помощью прохода типа ModulePass представлен на рис. 2. Здесь в исходной программе имеются два «эквивалентных» в некотором смысле (см. далее) базовых блока. Из тела этих блоков создаётся новая функция (@common), затем тела обоих базовых блоков заменяются на вызов этой функции с подстановкой соответствующих аргументов.
ISSN 0321-2653 IZVESTIYA VUZOV. SEVERO-KAVKAZSKIIREGION.
TECHNICAL SCIENCE. 2017. No 3
Исходная программа
If.then:
%1 = load ,dddr
%2 - load K1
%Ъ = load Kresult
Xadd - add nsw i32 *3. %2
store i32 %add. Krasult
br label ftif.end
if.then2:
- load ttb.addr
S6 - load
%7 = load ^result
5iadd3 - add nsw 132 %7, %6
store 132 Kaddä, Xresult
br label %if.end4
Конечная программа
if.then:
tail call void @common(%a,addr, %result} br label %if.end
define void ©commonC%a.addr, %result) -C entry:
%1 - load %a.addr %2 = load %1
- load ^result %add - add nsw i32 %3, %2 store i32 %add, %result ret void
>
if.then2:
tail call void @common(9tb. addr, %result} br label %if.end4
Рис. 2. Пример работы преобразования по уплотнению кода / Fig. 2. Example of executing the code compaction transformation
Алгоритм слияния базовых блоков
Реализованный в настоящей работе алгоритм можно разделить на два основных шага:
1. Сравнение базовых блоков.
2. Слияние базовых блоков.
Как уже было сказано, при работе с базовыми блоками не учитываются Termination- и Phi-node-инструкции, так как их вынесение в отдельную функцию невозможно.
Рассмотрим подробнее этап сравнения базовых блоков. На данном этапе составляются множества эквивалентных блоков. Базовые блоки считаются эквивалентными, если:
- все инструкции имеют одинаковый порядок. i-я инструкция одного базового блока должна совпадать с i-й инструкцией остальных базовых блоков;
- при первом вхождении локальной переменной она ассоциируется с локальными переменными других базовых блоков на той же позиции. Вхождения данной переменной должны совпадать со вхождениями ассоциируемой переменной;
- типы всех ассоциируемых переменных должны быть преобразуемы друг в друга без потерь;
- глобальные переменные не ассоциируются и используются в одинаковых инструкциях на одинаковых позициях;
- функции базовых блоков должны иметь одинаковые атрибуты двух типов: во-первых, у обеих функций должна совпадать или отсутствовать стратегия сборки мусора, во-вторых, они должны находиться в одинаковых секциях кода. Остальные атрибуты функций при сравнении не учитываются.
Основная часть алгоритма сравнения базовых блоков была адаптирована из LLVM-прохода MergeFunctions [10], который, в отличие от данной работы, предназначен для поиска одинакового кода на уровне целых функций. Одной из модификаций оригинального алгоритма, реализованной в настоящей работе, является добавление свойства коммутативности у бинарных операций сложения и умножения. Например, в оригинальной версии инструкции умножения следующего вида считались различными:
%mul = mul nsw i32 %k, 3 %mul = mul nsw i32 3, %k
Следующей модификацией является исключение некоторых атрибутов при сравнении функций базовых блоков (см. выше). Исключаются атрибуты, не влияющие на генерацию кода. Остальные изменения были произведены из-за особенностей сравнения базовых блоков, таких как пропуск Termination- и Phi-инструкций.
Рассмотрим подробнее этап слияния базовых блоков. На данном этапе поочерёдно рассматриваются множества эквивалентных базовых блоков, найденные на предыдущем этапе:
1. Для каждого базового блока находится список его входных и выходных параметров.
2. Количество и типы входных параметров базовых блоков должны совпадать (иначе базовые блоки не эквивалентны). Список выходных аргументов может отличаться, поэтому находится объединение множеств всех выходных переменных.
3. Проверяется наличие подходящей функции среди множества эквивалентных базовых блоков. Если функция была найдена, то переход к шагу 6.
ISSN 0321-2653 IZVESTIYA VUZOV. SEVERO-KAVKAZSKIIREGION.
4. Определяется, произойдёт ли уменьшение размера машинного кода при создании функции и замене базовых блоков на её вызов.
5. Создаётся функция, основанная на одном из базовых блоков множества. Аргументами функции являются входные параметры и указатели на каждый из выходных параметров. При создании функции после каждого создания выходной переменной она помещается в соответствующий выходной аргумент функции. Также устанавливаются атрибуты функции, уменьшающие её конечный размер.
6. Далее происходит замена каждого базового блока на вызов функции с использованием оператора bitcast для типобезопасности. Для каждой выходной переменной резервируется место на стеке, а после вызова функции переменные загружаются из стека, если она используется далее.
В первоначальной версии алгоритма шаг 4 (оценка, уменьшится ли размер кода при создании новой функции) отсутствовал. Однако на практике это приводило к увеличению размера конечного кода вместо его уменьшения (см. далее). В связи с этим были реализованы следующие дополнения к алгоритму:
- уменьшение количества выходных параметров, дублируемых или перемещаемых с помощью «бесплатных» инструкций (таких как bitcast, getelementptr, lifetime.start);
- оценка размера скомпилированных версий базовых блоков для пропуска блоков слишком малого размера в процессе работы алгоритма слияния. Были использованы как оценочные значения, возвращаемые интерфейсом LLVM TargetTransformInfo, так и собственные эвристические оценки, учитывающие особенности архитектур X86-64 и ARM.
Указанные меры привели к желаемому результату: после них преобразование действительно стало уменьшать выходной код.
Тестирование преобразования
Для тестирования реализованного преобразования были использованы исходные коды следующих открытых проектов:
-TinyXML2:
https://github.com/leethomason/tinyxml2
- curl:
https://github.com/curl/curl
- FatFs:
TECHNICAL SCIENCE. 2017. No 3
http://www.elm-chan.org/fsw/ff/ 00index_e.html
Каждый из этих проектов был скомпилирован при помощи clang с оптимизацией кода по размеру (ключ -Oz) с генерацией результата в LLVM IR (проект TinyXML2 дополнительно компилировался в версии без поддержки исключений: ключи -fno-exceptions -fno-unwind-tables). Далее получаемое внутреннее представление подавалось на вход разработанному преобразованию, после чего обе версии (до и после преобразования) обрабатывались компоновщиком LLVM для получения объектных модулей. Затем сравнивался размер полученных объектных модулей. В качестве целевых архитектур были использованы X86-64 и ARM. Для создания кода X86 были применены следующие команды вызова инструментов LLVM (аналогично — для ARM, с именем триплета архитектуры arm- linux-gnueabih):
> clang++ tinyxml2.cpp -emit-llvm -S -Oz -
target x8 6_64-linux-gnu -o tinyxml2.ll
> opt tinyxml2.ll-load
libIRFactoringTransform.so -bbfactor
-S -o tinyxml2_bbf.ll
> llc tinyxml2.ll -filetype=obj -march=x8 6-64
-o tinyxml2.o
> llc tinyxml2bbf.ll -filetype=obj -
march=x86-64 -o tinyxml2bbf.o
Здесь libIRFactoringTransform.so-загружаемый модуль компилятора с реализованным преобразованием уплотнения кода, включение преобразования выполняется при помощи ключа -bbfactor.
Кроме сравнения размеров были реализованы тесты корректности преобразования при помощи автоматизированного анализа LLVM IR.
Результаты экспериментов представлены в табл. 1 (архитектура X86-64) и 2 (ARM). Здесь столбцы с заголовками «TXML 1» и «TXML 2» содержат результаты компиляции библиотеки TinyXML2 с, соответственно, включённой и отключённой поддержкой исключений.
Все значения размеров в таблицах указаны в байтах. В первой строке каждой таблицы («Исх. размер») представлены размеры скомпилированных модулей без преобразования уплотнения. Вторая строка («Опт. размер 1») содержит размеры модулей с преобразованием уплотнения без оценки целесообразности замены базового блока (шаг 4 алгоритма слияния базовых блоков). Две следующих строки («Разность» и «Разн., %») содержат, соответственно, абсолютные и относительные разности исходного и оптимизированного размера. Отрицательные величины в этих строках говорят о том, что в
ISSN 0321-2653 IZVESTIYA VUZOV. SEVERO-KAVKAZSKIIREGION.
результате преобразования код, наоборот, увеличился. Следующая строка («Опт. размер 2») содержит размеры объектных модулей, скомпилированных с преобразованием уплотнения, в котором на шаге 4 алгоритма была использована упрощённая процедура оценки эффективности замены базового блока. А именно, базовый блок считался неподходящим для замены, если его размер составлял три или менее инструкций LLVM. Следующие две строки, аналогично предыдущим с такими же названиями, содержат разности исходного и оптимизированного кода при помощи второго вида оптимизации. Наконец, строки «Опт. размер 3» содержат размеры модулей, скомпилированных с участием преобразования уплотнения, в котором на шаге 4 алгоритма слияния была использована процедура оценки размера кода при помощи интерфейса TargetTransformlnfo, а также собственная эвристическая процедура. Также были использованы методы, направленные на уменьшение количества выходных параметров функций, описанные ранее.
Таблица 1 / Table 1
Результаты экспериментов компиляции, X86 / Results of compilation experiments, X86
Проект TXML 1 TXML 2 libcurl FatFS
Исходи. размер 29311 19213 299045 9998
Опт. размер 1 32019 21367 342227 12202
Разность -2708 -2154 -43182 -2204
Разн., % -9,24 -11,21 -14,44 -22,04
Опт. размер 2 30305 19914 309827 11211
Разность -994 -701 -10782 -1213
Разн., % -3,39 -3,65 -3,61 -12,13
Опт. размер 3 29156 19013 299002 9998
Разность 155 200 43 0
Разн., % 0,53 1,04 0,01 0
Таблица 2 / Table 2 Результаты экспериментов компиляции, ARM / Results of compilation experiments, ARM
Проект TXML 1 TXML 2 libcurl FatFS
Исходн. размер 27110 23214 311010 11628
Опт. размер 1 30034 25814 353428 14820
Разность -2924 -2600 -42418 -3192
Разн., % -10,79 -11,2 -13,64 -27,45
Опт. размер 2 28198 24198 335573 13208
Разность -1088 -984 -24563 -1580
Разн., % -4,01 -4,24 -7,9 -13,59
Опт. размер 3 26914 22994 310922 11628
Разность 196 220 88 0
Разн., % 0,72 0,95 0,03 0
TECHNICAL SCIENCE. 2017. No 3
Данный эксперимент показал, что выполнение преобразования без учёта размера заменяемых базовых блоков, наоборот, приводит к ощутимому увеличению размеров кода. Также видно, что качество оценки размера машинного кода, получаемого из каждого базового блока, существенно влияет на качество преобразования. Так, простая процедура оценки в терминах инструкций LLVM привела к увеличению размера кода. Наконец, положительный результат показало использование более сложной оценки размера кода. И даже в этом случае уменьшение размера кода в результате работы преобразования для всех проектов составило незначительные величины: от 0 до 1%. Наибольший эффект от преобразования был достигнут на проекте TinyXML2 с отключённой поддержкой исключений (1 %), наименьший - на FatFs, где преобразование оставило код без изменений.
Заключение
Из-за неоднородного преобразования промежуточного кода в машинный одной из главных трудностей алгоритмов оптимизации размера кода на уровне промежуточного представления является определение того, как изменится конечный размер кода. Для определения как можно более точного размера кода необходим доступ к функциональности кодогенератора. В противном случае для получения приемлемого результата нужно производить проходы понижения уровня кода самостоятельно и для разных кодогенераторов использовать разную логику определения размера кода, что является лишней и ненужной работой.
В настоящее время в LLVM не существует удовлетворительных средств, определяющих конечный размер кода инструкции или базового блока. Вместо этого LLVM предоставляет доступ к интерфейсу кодогенератора TargetTransformlnfo. Однако он возвращает величину, являющуюся чем-то средним между размером инструкции и её временем выполнения. Также эта величина является эвристикой, и результаты, полученные исключительно с её помощью, оказались отрицательными, т.е. конечный размер программы увеличивался.
Таким образом, оптимизация размера кода на уровне LLVM IR имеет смысл только для крупных участков кода, таких как базовый блок, регион, функция. Для повышения качества преобразования следует использовать более точные методики оценки размера машинного кода (для реализованной в данный момент эвристической
ISSN 0321-2653 IZVESTIYA VUZOV. SEVERO-KAVKAZSKIIREGION.
процедуры имеется потенциал для улучшения). Также видится перспективным изменение традиционной «линейной» архитектуры компиляторов, когда из исходного кода строится последовательно сначала высокоуровневое внутреннее представление (абстрактное синтаксическое дерево), затем оно понижается до промежуточного уровня, затем до представления машинного кода, и так далее вплоть до выходного двоичного файла. В целях получения более точной оценки итогового размера функции был бы полезным «древовидный» алгоритм, позволяющий полностью скомпилировать отдельную функцию до уровня бинарного кода ещё на этапе обработки промежуточного представления.
Литература
1. Gartner Says 6.4 Billion Connected «Things» Will Be in Use in 2016, Up 30 Percent From 2015 // Gartner, 10 Nov. 2015, URL: http://www.gartner.com/newsroom/id/3165317 (дата обращения: 24.04.2017).
TECHNICAL SCIENCE. 2017. No 3
2. Mattern F., Flörkemeier C. Vom Internet der Computer zum Internet der Dinge // Informatik Spektrum. 2010. Vol. 33, № 2. 107-121. DOI 10.1007/s00287-010-0417-7.
3. De Sutter B., Put L. van, Chanet D., De Bus B., De Bosschere K. Link-time compaction and optimization of ARM executables // ACM Transactions on Embedded Computing Systems. 2007. Feb. Vol. 1, № 3. DOI 10.1145/1210268.1210273.
4. Muchnick S.S. Advanced Compiler Design and Implementation. San Francisco, CA, USA.: Morgan Kaufmann Publishers Inc., 1997. xxx+856 pp. ISBN
1-55860-320-4.
5. Ахо А.В., Лам М С., Сети Р., Ульман Дж.Д. Компиляторы: принципы, технологии и инструментарий,
2-е изд.: пер. с англ. М.: ООО «И.Д. Вильямс», 2008. 1184 с. ISBN 978-5-8459-1349-4.
6. Lopes B.C., Auler R. Getting Started with LLVM Core Libraries. Packt Publishing, 2014. 314 p. ISBN 978-1-78216-692-4.
7. Debray S.K., Evans W. Compiler Techniques for Code Compaction // ACM Transactions on Programming Languages and Systems. 2000. Mar. Vol. 22, № 2. P. 378 - 415. DOI 10.1145/349214.349233.
8. Code compaction. URL: https://github.com/skapix/ codeCompaction (дата обращения: 24.04.2017).
9. Writing an LLVM Pass. URL: http://llvm.org/docs/ WritingAnLLVMPass.html (дата обращения: 24.04.2017).
10. MergeFunctions pass. URL: http://llvm.org/docs/ MergeFunctions.html (дата обращения: 24.04.2017).
References
1. Gartner Says 6.4 Billion Connected "Things" Will Be in Use in 2016, Up 30 Percent From 2015 // Gartner, 10 Nov. 2015. Available at: http://www.gartner.com/newsroom/id/3165317 (accessed 24.04.2017).
2. Mattem F., Flörkemeier, C. Vom Internet der Computer zum Internet der Dinge // Informatik Spektrum. 2010. Vol. 33, No 2. 107-121. DOI 10.1007/s00287-010-0417-7.
3. De Sutter B., van Put L., Chanet D., De Bus B., De Bosschere K. Link-time compaction and optimization of ARM executables // ACM Transactions on Embedded Computing Systems. 2007. Feb. Vol. 1, No 3. DOI 10.1145/1210268.1210273.
4. Muchnick S.S. Advanced Compiler Design and Implementation. San Francisco, CA, USA. : Morgan Kaufmann Publishers Inc., 1997. xxx+856 pp. ISBN 1-55860-320-4.
5. Aho A.V., Lam M.S., Sethi R., Ullman J.D. Kompilyatory: printsipy, tekhnologii i instrumentarii [Compilers: Principles, Techniques, and Tools]. Moscow, 2006. xxiv+1009 pp. ISBN 978-0-32148-681-3.
6. Lopes B.C., Auler R. Getting Started with LLVM Core Libraries. Packt Publishing, 2014. 314 pp. ISBN 978-1-78216-692-4.
7. Debray S.K., Evans W. Compiler Techniques for Code Compaction // ACM Transactions on Programming Languages and Systems. 2000. Mar. Vol. 22, No 2. pp. 378-415. DOI 10.1145/349214.349233.
8. Code compaction. Available at: https://github.com/skapix/codeCompaction (accessed 24.04.2017).
9. Writing an LLVM Pass. Available at: http://llvm.org/docs/WritingAnLLVMPass.html (accessed 24.04.2017).
10. MergeFunctions pass. Available at: http://llvm.org/docs/MergeFunctions.html (accessed 24.04.2017).
Поступила в редакцию /Received_03 мая 2017 г. /May 03, 2017