Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

LLVM

Глава 11 из книги "Архитектура приложений с открытым исходным кодом", том 1.

Оригинал: LLVM
Автор: Chris Lattner
Перевод: А.Панин

11.6. Интересные возможности, предоставляемые модульной архитектурой

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

11.6.1. Выбор момента и порядка выполнения каждой из фаз

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

Оптимизация времени связывания (Link-Time Optimization) решает проблему традиционной обработки компилятором одной единицы трансляции в каждый момент времени (т.е. файла исходного кода .c со всеми заголовками) и невозможности проведения оптимизаций (таких, как обработка inline-функций) в рамках нескольких файлов одновременно. Компиляторы на основе LLVM, такие, как Clang, поддерживают эту возможность с помощью аргументов командной строки -flto или -O4. Эти аргументы сообщают компилятору о том, что нужно сформировать биткод LLVM и записать его в файл с расширением .o вместо записи объектного файла для данной архитектуры и задерживают генерацию кода до момента связывания, как показано на Рисунке 11.6.

Оптимизация времени связывания
Рисунок 11.6: Оптимизация времени связывания

Подробное описание процесса данной оптимизации зависит от используемой операционной системы, но важным моментом является то, что компоновщик определяет, находится ли в файлах с расширением .o биткод LLVM или эти файлы являются объектными файлами для данной архитектуры. Когда установлено наличие биткода, компоновщик считывает содержимое файлов в память, производит связывание, после чего использует по отношению к коду оптимизатор LLVM. Так как теперь оптимизатор может обрабатывать гораздо больший объем кода, у него есть возможность преобразовывать inline-функии, устанавливать константы, проводить более агрессивное удаление фрагментов неиспользуемого кода и проводить другие оптимизации в рамках множества файлов. Хотя многие современные компиляторы поддерживают оптимизации времени связывания, большинство из них (т.е. GCC, Open64, компилятор Intel, и.т.д.) выполняют эти оптимизации с использованием ресурсоемкого и медленного процесса преобразования кода. В LLVM оптимизация времени связывания является естественным методом использования архитектуры системы и работает с различными исходными языками программирования (в отличие от множества других компиляторов), так как представление LLVM IR на самом деле не зависит от исходного языка программирования.

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

Оптимизация времени установки
Рисунок 11.7: Оптимизация времени установки

11.6.2. Тестирование элементов оптимизатора

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

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

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

; RUN: opt < %s -constprop -S | FileCheck %s
define i32 @test() {
  %A = add i32 4, 5
  ret i32 %A
  ; CHECK: @test()
  ; CHECK: ret i32 9
}

Строка с директивой RUN задает команду для выполнения: в данном случае используются инструменты с интерфейсом командной строки opt и FileCheck. Программа opt является всего лишь оберткой над менеджером фаз оптимизации LLVM, которая связана со всеми стандартными фазами (и может динамически подгружать дополнения с другими алгоритмами фаз) и позволяет использовать их с помощью интерфейса командной строки. Инструмент FileCheck проверяет, соответствуют ли данные, поступившие на стандартный ввод, серии описаний с использованием директив CHECK. В данном случае простейший тест проверяет корректность оптимизации операции сложения констант 4 и 5 с помощью инструкции add и получения в итоге значения 9 с помощью фазы оптимизации constprop.

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

11.6.3. Автоматическое тестирование с помощью BugPoint

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

Инструмент BugPoint из состава LLVM7 использует преобразование в представление LLVM IR и модульную архитектуру для автоматизации этого процесса. Например, при передаче исходного файла с расширением .ll или .bc вместе со списком фаз оптимизации, при использовании которых происходит крах оптимизатора, BugPoint сокращает объем кода до небольшого тестового примера и определяет, при выполнении какой из фаз оптимизации происходит крах. После этого он выводит сокращенный пример кода для тестирования и параметры командной строки для программы opt, позволяющие воспроизвести ошибку. Формирование результата путем сокращения объема кода и количества фаз оптимизации происходит с помощью техник, аналогичных отладке при помощи изменений ("delta debugging"). Так как BugPoint работает со структурой представления LLVM IR, не происходит траты времени впустую на генерацию некорректного представления и передачу его оптимизатору, как в случае использования стандартного инструмента "delta" с интерфейсом командной строки.

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

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

Сноски


Далее: 11.7. Взгляд в прошлое и направления развития в будущем