Введение
Большинство разработчиков однозначно слышали о довольно значимых open-source разработках таких, как система LLVM и компилятор clang. Однако LLVM сейчас не только непосредственно сама система для создания компиляторов, но уже и большая экосистема, включающая в себя множество проектов для решения различных задач, возникающих в процессе любого этапа создания компилятора (обычно у каждого такого проекта существует свой отдельный репозиторий). Часть инфраструктуры естественно включает в себя средства для тестирования и бенчмаркинга, т.к. при разработке компилятора его эффективность является очень важным показателем. Одним из таких отдельных проектов тестовой инфраструктуры LLVM является test-suite (официальная документация).
LLVM test-suite
При первом беглом взгляде на репозиторий test-suite кажется, что это просто набор бенчмарков на C/C++, но это не совсем так. Помимо исходного кода программ, на которых будут производиться измерения производительности, test-suite включает гибкую инфраструктуру для их построения, запуска и сбора метрик. По умолчанию он собирает следующие метрики: время компиляции, время исполнения, время линковки, размер кода (по секциям).
Test-suite естественно полезен при тестировании и бенчмаркинге компиляторов, но также он может быть использован для некоторых других исследовательских задач, где необходима некоторая база кода на C/C++. Те, кто когда-то предпринимал попытки сделать что-то в области анализа данных, думаю, сталкивались с проблемой недостатка и разрозненности исходных данных. А test-suite хоть и не состоит из огромного количества приложений, но имеет унифицированный механизм сбора данных. Добавлять собственные приложения в набор, собирать метрики, необходимые именно Вашей задаче, очень просто. Поэтому, на мой взгляд, test-suite (помимо основной задачи тестирования и бенчмаркинга) — хороший вариант для базового проекта, на основе которого можно строить свой сбор данных для задач, где нужно анализировать некоторые особенности программного кода или некоторые характеристики программ.
Структура LLVM test-suite
test-suite
|----CMakeLists.txt // Базовый CMake файл, проводящий первичные настройки, добавляющий
| // модули и т.д.
|
|---- cmake
| |---- .modules // Файлы с описанием макросов и функций общего назначения,
| // фактически являющихся API для интеграции тестов
|
|---- litsupport // Модуль Python, описывающий собственный формат теста для test-suite,
| // распознаваемый средством тестирования lit (входит в LLVM)
|
|---- tools // Содержит дополнительные инструменты: для сравнения результатов
| // работы программ с ожидаемым выходом(с настройками для точности
| // проверки), измерения времени и т.д.
|
| // Остальные директории содержат бенчмарки
|
|---- SingleSource // Содержит тестовые программы, состоящие из одного файла с исходным
| // кодом. В одной директории может находиться много разных тестов.
|
|---- MultiSource // Содержит тестовые программы, состоящие из множества файлов с
| // исходным кодом. В одной директории обычно находятся файлы для
| // одного приложения.
|
|---- MicroBenchmarks // Программы, использующие библиотеку google-benchmark. В них
| // определяются функции, которые выполняются несколько раз, пока
| // результаты измерений не будут статистически значимыми
|
|---- External // Содержит описание для программ, которые не входят в test-suite, а
| // именно, исходные коды программ находятся (или могут находиться)
| // где-то в другом месте
Структура простая и понятная.
Принцип работы
Как видно за всю работу по описанию сборки, запуска и сбора метрик отвечают CMake и специальный формат lit-тестов.
Если рассматривать очень абстрактно, понятно, что процесс бенчмаркинга с помощью это системы выглядит просто и весьма предсказуемо:
Как же это выглядит более детально? В данной статье хотелось бы остановится именно на том, какую роль во всей системе играет CMake и что из себя представляет единственный файл, который Вы должны написать, если хотите что-то добавить в данную систему.
1. Построение тестовых приложений.
В качестве билд-системы используется ставший уже фактически стандартом для программ на C/C++ CMake. CMake производит конфигурацию проекта и генерирует в зависимости от предпочтений пользователя файлы make, ninja и т.д. для непосредственного построения.
Однако в test-suite CMake генерирует не только правила, как собрать приложения, но и производит конфигурацию самих тестов.
После запуска CMake в build директорию будут записаны еще файлы (с расширением .test) с описанием того, как приложение должно выполняться и проверяться на корректность.
Пример наиболее стандартного файла .test
RUN: cd <some_path_to_build_directory>/MultiSource/Benchmarks/Prolangs-C/football ; <some_path_to_build_directory>/MultiSource/Benchmarks/Prolangs-C/football/football
VERIFY: cd <some_path_to_build_directory>/MultiSource/Benchmarks/Prolangs-C/football ; <some_path_to_build_directory>/tools/fpcmp %o football.reference_output
В файле с расширением .test могут быть следующие секции:
- PREPARE — описывает любые действия, которые должны быть сделаны до запуска приложения, очень похоже на метод Before существующий в разных фреймворках для unit-тестирования;
- RUN — описывает как запустить приложение;
- VERIFY — описывает как проверить корректность работы приложения;
- METRIC — описывает метрики, которые нужно собрать дополнительно в стандартным.
Любая из данных секций может быть опущена.
Но так как данный файл генерируется автоматически, то именно в файле CMake для бенчмарка описывается: как получить объектные файлы, как их собрать в приложение, а потом еще и что с этим приложением нужно делать.
Для лучшего понимания поведения по умолчанию и того, как же это описывается, рассмотрим пример некоторого CMakeLists.txt
list(APPEND CFLAGS -DBREAK_HANDLER -DUNICODE-pthread) # необходимые для приложения флаги компиляции (остальные более общие флаги компиляции вроде уровня оптимизации и т.п. лучше указывать при вызове CMakе, чтобы иметь возможность в дальнейшем ставить эксперименты)
list(APPEND LDFLAGS -lstdc++ -pthread) # необходимые для приложения флаги для линкера
Флаги могут устанавливаться в зависимости от платформы, в test-suite cmake modules входит файл DetectArchitecture, который определяет целевую платформу, на которой запускаются бенчмарки, поэтому просто можно использовать уже собранные данные. Также доступны и другие данные: операционная система, порядок байтов и т.д.
if(TARGET_OS STREQUAL "Linux")
list(APPEND CPPFLAGS -DC_LINUX)
endif()
if(NOT ARCH STREQUAL "ARM")
if(ENDIAN STREQUAL "little")
list(APPEND CPPFLAGS -DFPU_WORDS_BIGENDIAN=0)
endif()
if(ENDIAN STREQUAL "big")
list(APPEND CPPFLAGS -DFPU_WORDS_BIGENDIAN=1)
endif()
endif()
В принципе, в данной части не должно быть ничего нового для людей, которые хотя бы когда-то видели или писали простой CMake файл. Естественно, Вы можете использовать библиотеки, строить их сами, в общем, использовать любые средства, предоставляемые CMake для того, чтобы описать процесс сборки Вашего приложения.
А дальше нужно обеспечить генерацию .test файла. Какие средства предоставляет интерфейс tets-suite для этого?
Есть 2 базовых макроса llvm_multisource и llvm_singlesource, которых для большинства тривиальных случаев хватает.
- llvm_multisource используется, если приложение состоит из нескольких файлов. Если Вы не передадите файлы с исходным кодом как параметры при вызове этого макроса в своем CMake, то будут использованы все файлы исходного кода, находящиеся в текущей директории, в качестве базы для построения. На самом деле, сейчас происходят изменения в интерфейсе данного макроса в test-suite, и описанный способ передачи исходных файлов в виде параметров макроса – это текущая версия, находящаяся в master ветке. Раньше была другая система: файлы с исходным кодом должны были быть записаны в переменную Source (так было еще в релизе 7.0), а макрос не принимал никаких параметров. Но основная логика реализации осталась прежней.
- llvm_singlesource считает, что каждый .c/.cpp файл – это отдельный бенчмарк и для каждого собирает отдельный исполняемый файл.
По умолчанию, оба описанных выше макроса для запуска построенного приложения генерируют команду, которая просто вызывает это приложение. А проверка корректности происходит за счет сравнения с ожидаемым выходом, находящимся в файле с расширением .reference_output (также с возможными суффиксами .reference_output.little-endian, .reference_output.big-endian).
Если Вас это устраивает, это просто замечательно, Вам хватит одной дополнительной строчки (вызова llvm_multisource или llvm_singlesource), чтобы запустить приложение и получить следующие метрики: размер кода (по секциям), время компиляции, время линковки, время исполнения.
Но, естественно, редко бывает все так гладко. Вам может потребоваться изменить одну или несколько стадий. И это возможно тоже с помощью простых действий. Единственное, нужно помнить, что, если переопределяете некоторую стадию, нужно описать и все остальные (даже если алгоритм их работы по умолчанию устраивает, что, конечно, немного огорчает).
В API существуют макросы для описания действий на каждой стадии.
Про макрос llvm_test_prepare для подготовительной стадии особенно писать нечего, туда просто в качестве параметра передаются команды, которые нужно выполнить.
Что может понадобиться в секции запуска? Самый предсказуемый случай – приложение принимает некоторые аргументы, входные файлы. Для этого есть макрос llvm_test_run, который принимает только аргументы запуска приложения (без имени исполняемого файла) в качестве параметров.
llvm_test_run(--fixed 400 --cpu 1 --num 200000 --seed 1158818515 run.hmm)
Для изменения действий на этапе проверки корректности используется макрос llvm_test_verify, который принимает любые команды в качестве параметров. Конечно, для проверки корректности лучше использовать инструменты, включенные в папку tools. Они предоставляют неплохие возможности для сравнения сгенерированного выхода с ожидаемым (существует отдельная обработка для сравнения вещественных чисел с некоторой погрешностью и т.п.). Но можно где-то и просто проверить, что приложение завершилось успешно и т.д.
llvm_test_verify("cat %o | grep -q 'exit 0'") # %o - это специальный placeholder для выходного файла, который понимает lit. Так как дальше эти команды будут выполняться с помощью lit, то можно использовать все, что он способен распознать. Подробной информации о lit (инструмент для тестирования, входящий в LLVM) в данном материале не будет (можно ознакомиться с <a href="https://llvm.org/docs/CommandGuide/lit.html">официальной документацией</a>)
А что, если есть необходимость собирать некоторые дополнительные метрики? Для этого существует макрос llvm_test_metric.
llvm_test_metric(METRIC <имя метрики> <команда, позволяющая получить значение>)
Например, для dhrystone можно получить специфичную для него метрику.
llvm_test_metric(METRIC dhry_score grep 'Dhrystones per Second' %o | awk '{print $4}')
Конечно, если нужно собрать для всех тестов дополнительные метрики данный способ несколько неудобен. Нужно либо добавлять вызов llvm_test_metric в высокоуровневые макросы, предоставляемые интерфейсом, либо можно использовать TEST_SUITE_RUN_UNDER (переменную CMake) и специфичный скрипт для сбора метрик. Переменная TEST_SUITE_RUN_UNDER довольна полезна, и может быть использована, например, для запуска на симуляторах и т.п. По сути в нее записывается команда, которая примет на вход приложение с его аргументами.
В итоге же получаем некоторый CMakeLists.txt вида
# У нас нет специфичных флагов компиляции
llvm_test_run(--fixed 400 --cpu 1 --num 200000 --seed 1158818515 run.hmm)
llvm_test_verify("cat %o | grep -q 'exit 0'")
llvm_test_metric(METRIC score grep 'Score' %o | awk '{print $4}')
llvm_multisource() # llvm_multisource(my_application) в новой версии
Интеграция не требует дополнительных усилий, если приложение уже собирается с помощью CMake, то в CMakeList.txt в test-suite можно включить уже существующий CMake для сборки и дописать несколько простых вызовов макросов.
2. Запуск тестов
В результате своей работы CMake сгенерировал специальный тестовый файл по заданному описанию. Но как этот файл выполняется?
lit всегда использует некоторый конфигурационный файл lit.cfg, который, соответственно, существует и в test-suite. В данном конфигурационном файле указываются различные настройки для запуска тестов, в том числе задается формат исполняемых тестов. Test-suite использует свой формат, который находится в папке litsupport.
config.test_format = litsupport.test.TestSuiteTest()
Данный формат описан в виде класса теста, унаследованного от стандартного lit-теста и переопределяющий основной метод интерфейса execute. Также важными компонентами litsupport является класс с описанием плана выполнения теста TestPlan, который хранит все команды, которые должны быть выполнены на разных стадиях и знает порядок стадий. Для предоставления необходимой гибкости в архитектуру также внесены модули, которые должны предоставлять метод mutatePlan, внутри которого они могут изменять план тестирования, как раз и внося описание сбора нужных метрик, добавляя дополнительные команды для измерения времени к запуску приложения и т.д. За счет подобного решения архитектура хорошо расширяется.
Примерная схема работы test-suite теста (за исключением деталей в виде классов TestContext, различных конфигураций lit и самих тестов и т.д.) представлена ниже.
Lit вызывает выполнение указанного в конфигурационном файле типа теста. TestSuiteTest парсит сгенерированный CMake тестовый файл, получая описание основных стадий. Затем вызываются все найденные модули для изменения текущего плана тестирования, запуск инструментируется. Потом полученные тестовый план исполняется: выполняются по порядку стадии подготовки, запуска, проверки корректности. При наличии необходимости может быть выполнено профилирование (добавляемое одним из модулей, если при конфигурации устанавливалась переменная, показывающая необходимость профилирования). Следующим шагом собираются метрики, функции для сбора которых были добавлены стандартными модулями в поле metric_collectors в TestPlan, а затем происходит сбор дополнительных метрик, описанных пользователем в CMake.
3. Запуск test-suite
Запуск test-suite возможен двумя способами:
- Ручным, т.е. последовательным вызовом команд.
cmake -DCMAKE_CXX_COMPILER:FILEPATH=clang++ -DCMAKE_C_COMPILER:FILEPATH=clang test-suite # конфигурация make # непосредственно построение llvm-lit . -o <output> # запуск тестов
- с помощью LNT (еще одна система из экосистемы LLVM, которая позволяет запускать бенчмарки, сохранять результаты в БД, анализировать результаты в веб-интерфейсе). LNT внутри своей команды запуска тестов выполняет те же шаги, что и в предыдущем пункте.
lnt runtest test-suite --sandbox SANDBOX --cc clang --cxx clang++ --test-suite test-suite
Результат для каждого теста выводится в виде
PASS: test-suite :: MultiSource/Benchmarks/Prolangs-C/football/football.test (m of n)
********** TEST 'test-suite :: MultiSource/Benchmarks/Prolangs-C/football/football.test' RESULTS **********
compile_time: 1.1120
exec_time: 0.0014
hash: "38254c7947642d1adb9d2f1200dbddf7"
link_time: 0.0240
size: 59784
size..bss: 99800
…
size..text: 37778
**********
Результаты от разных запусков можно сравнить и без LNT (хотя данный фреймворк предоставляет большие возможности для анализа информации с помощью разных инструментов, но он нуждается в отдельном обзоре), воспользовавшись скриптом, входящим в test-suite
test-suite/utils/compare.py results_a.json results_b.json
Пример сравнения размера кода одного и того бенчмарка от двух запусков: с флагами -O3 и -Os
test-suite/utils/compare.py -m size SANDBOX1/build/O3.json SANDBOX/build/Os.json
Tests: 1
Metric: size
Program O3 Os diff
test-suite...langs-C/football/football.test 59784 47496 -20.6%
Заключение
Инфраструктура для описания и запуска бенчмарков, реализованная в test-suite проста в использовании и поддержке, хорошо масштабируется, и в принципе, по моему мнению, использует достаточно элегантные решения в своей архитектуре, что, конечно, делает test-suite очень полезным инструментом для разработчиков компиляторов, а также данная система может быть доработана для использования в некоторых задачах анализа данных.