С самого своего начала C++ анализатор PVS-Studio для Windows (тогда еще Viva64 версии 1.00 в 2006 году) собирался компилятором MSVC. С выходом новых релизов C++ ядро анализатора научилось работать на Linux и macOS, и структура проекта была переведена на использование CMake. Но под Windows сборка по-прежнему происходила с помощью компилятора MSVC. 29 апреля 2019 года разработчики Visual Studio объявили о включении в свою среду разработки набора утилит LLVM и компилятора Clang. И сейчас у нас наконец дошли руки, чтобы попробовать его в действии.
Тестирование производительности
В качестве бенчмарка воспользуемся нашей утилитой для регрессионного тестирования анализатора под названием SelfTester. Суть её работы заключается в анализе набора разных проектов и сравнении результатов анализа с эталонными. Например, если при каких-то правках в ядре анализатора появились ложные предупреждения или пропали правильные, значит появилась регрессия, которую надо исправить. Более подробно про SelfTester можно прочитать в статье "Лучшее – враг хорошего".
Среди тестовой базы – достаточно разнообразные по объёму кода проекты. Как правило, если рабочий компьютер или тестовый сервер не нагружен, то время тестирования SelfTester'ом на одной и той же версии ядра варьируется в пределах погрешности. В случае если производительность анализатора не уберегли, это значительно скажется на общем времени тестирования.
После того как сборка C++ ядра перешла на Clang, SelfTester стал проходить на 11 минут быстрее.
Выигрыш по производительности в 13% — это довольно заметно, учитывая, что достаточно просто поменять компилятор, не так ли?
Минусы тоже есть, но незначительные. Сборка дистрибутива замедлилась на 8 минут, а размер исполняемого файла подрос на 1,6 Мбайт (из них ~500 Кбайт из-за статической линковки рантайма).
Видимо, производительность достигается более долгим этапом LTO (большую часть сборки занимает именно линковка) и более агрессивным раскручиванием циклов и встраиванием функций.
Далее хочется поделиться подводными камнями, возникшими в процессе перехода.
Генерация сборки под Clang
Скрипты CMake позволяют нам собирать свой код всеми мейнстримными компиляторами под нужные операционные системы.
Прежде всего необходимо установить компоненты компилятора Clang через Visual Studio Installer.
Clang-cl — это так называемый "драйвер", который позволяет использовать clang с параметрами от cl.exe. Таким образом, он должен прозрачно взаимодействовать с MSBuild, практически как родной компилятор.
Также можно воспользоваться официальными сборками от проекта LLVM, которые можно найти на их репозитории GitHub. Однако для них нужно установить дополнительный плагин, чтобы Visual Studio смогла найти компиляторы. Имя toolset'а будет llvm, а не clangcl, как показано дальше в примерах.
Указываем toolchain в команде генерации solution для Visual Studio:
cmake -G "Visual Studio 16 2019" -Tclangcl <src>
Либо используем GUI:
Открываем получившийся проект, собираем. И, конечно же, получаем пачку ошибок.
Чиним сборку
Хоть сlang-cl внешне и ведет себя как CL, под капотом это совсем другой компилятор, со своими приколами.
Мы стараемся не игнорировать предупреждения компиляторов, поэтому используем флаги /W4 и /WX. Однако Clang может генерировать дополнительные предупреждения, которые сейчас мешают сборке. Пока выключим их:
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
....
if (WIN32)
add_compile_options(-Wno-error=deprecated-declarations
-Wno-error=reorder-ctor
-Wno-error=format-security
-Wno-error=macro-redefined
-Wno-error=bitwise-op-parentheses
-Wno-error=missing-field-initializers
-Wno-error=overloaded-virtual
-Wno-error=invalid-source-encoding
-Wno-error=multichar
-Wno-unused-local-typedef
-Wno-c++11-narrowing)
....
endif()
endif()
Немного получше.
Компиляторы GCC и Clang имеют встроенную поддержку типа int128, в отличие от MSVC под Windows. Поэтому в своё время была написана обертка с реализацией Int128 для Windows (на ассемблерных вставках и обернутая ifdef'ами, в лучших традициях C/C++). Поправим определения для препроцессора, заменив:
if (MSVC)
set(DEFAULT_INT128_ASM ON)
else ()
set(DEFAULT_INT128_ASM OFF)
endif ()
на
if (MSVC AND NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang")
set(DEFAULT_INT128_ASM ON)
else ()
set(DEFAULT_INT128_ASM OFF)
endif ()
Обычно библиотеку с builtin'ами линкеру (lld) передает драйвер компилятора, будь то clang.exe или clang-cl.exe. Но в данном случае линкером заправляет MSBuild напрямую, который не знает, что нужно её использовать. Соответственно, драйвер никак не может передать флаги линкеру, поэтому приходится разбираться самим.
if (CMAKE_GENERATOR MATCHES "Visual Studio")
link_libraries("$(LLVMInstallDir)\\lib\\clang\\${CMAKE_CXX_COMPILER_VERSION}\\lib\\windows\\clang_rt.builtins-x86_64.lib")
else()
link_libraries(clang_rt.builtins-x86_64)
endif()
Ура! Сборка заработала. Однако дальше при запуске тестов нас ждала куча ошибок сегментации:
Отладчик при этом показывает какое-то странное значение в IntegerInterval, а на самом деле проблема находится немного дальше:
В разных структурах для Dataflow-механизма активно применяется ранее упомянутый тип Int128, а для работы с ним используются SIMD-инструкции. И падение вызвано невыровненным адресом:
Инструкция MOVAPS перемещает из памяти набор чисел с плавающей запятой в регистры для SIMD-операций. Адрес при этом обязан быть выровнен, в его конце должен стоять 0, а оказалась 8. Придется помочь компилятору, задав правильное выравнивание:
class alignas(16) Int128
Порядок.
Последняя проблема вылезла из-за Docker-контейнеров:
Сборка под MSVC всегда делалась со статической линковкой рантайма, а для экспериментов с Clang рантайм переключили на динамический. Оказалось, что в образах с Windows по умолчанию не установлены Microsoft Visual C++ Redistributable. Решили вернуть статическую линковку, чтобы у пользователей не возникало таких же неприятностей.
Заключение
Несмотря на то, что пришлось немного повозиться с подготовкой проекта, мы остались довольны ростом производительности анализатора более чем на 10%.
Последующие релизы PVS-Studio на Windows будут собираться с помощью компилятора Clang.
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Alexey Govorov, Sergey Larin. PVS-Studio Team: Switching to Clang Improved PVS-Studio C++ Analyzer's Performance.
klirichek
Ох, намаялись мы с этой статической линковкой рантайма…
Поставили очень давно, когда проект был небольшой а cmake не такой продвинутый. Сама постановка уже была очень костыльная (ещё до проекта cmake подсунуть ему файлы с "правильными" flag_overrides), и рулить такой сущностью, что теперь во флагах компилятора вместо одной буковки стоит другая казалось ненужным.
Последние увлекательные приключения были из-за этого с boost. Ему можно сказать, какой конфиг мы хотим использовать. Но вот про static runtime (если он уже стоит в основном проекте) нужно говорить обязательно; молчать нельзя! И нужно говорить правильно (да, нужен!). Иначе сборки не будет. По итогу, подумав, решили таки теперь вернуться к "традиционному" динамическому рантайму. Особых настроек стало гораздо меньше и все дела проще. А для копирования (редистрибуции) нужных либ в cmake есть спец. модуль InstallRequiredSystemLibraries. Если особого резона в использовании именно статика нет, то включение этого модуля решает те же проблемы, что и включение статика (получается кросс-виндовый бинарь, хоть и со "свитой"), и при этом избавляет от множества скрытых эффектов, обусловленных статиком.
Сейчас (в нынешнем cmake) это хоть и можно стало ставить более цивилизованно
в смысле, что теперь при желании узнать рантайм не надо парсить строку опций компилятора, а достаточно прочитать property проекта — но это всё равно не работает с тем же бустом )