Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
На прошлой неделе, после нескольких месяцев разработки, вышла очередная версия языка программирования NewLang. Одной из технических особенностей данного релиза является переход на использования компилятора clang вместо gcc.
Данная статья описывает причины смены компилятора, некоторые особенности этого процесса, проблемы, которые приходилось решать и итоговые выводы.
Автор надеется, что эта информация может оказаться полезной и позволит сэкономить кучу времени, если заранее знать некоторые подводные камни, а так же положительные стороны от перехода на clang.
Предпосылки
Прежде чем вдаваться в технические подробности, следует рассказать о предпосылках для данной задачи, а именно, о языке программирования NewLang и особенностях его реализации.Первые эксперименты с синтаксисом языка были на Python. Через некоторое время разработка продолжилась на C++, а так как все это происходило под Linux, то выбор компилятора gcc был вполне естественным.
А когда вышла первая публичная версия NewLang, то пришлось задуматься о поддержке ОС Windows и самое первое и естественное решение — настроить кросскомпиляцию с помощью gcc и msys или cygwin. Но когда потребовалось линковаться с библиотекой libtorch, которая используется для работы с тензорами, то возникла серьезная проблема. Libtorch не поддерживает сборку кросскомпилятором gcc под Windows!
Это значит, что сборка всего проекта с помощью gcc под «винду» отпадает и пришлось использовать Visual Studio с нативным компилятором. Собственно, сперва и пришлось идти этой дорогой — gcc под Linux и Visual Studio под Windows, ведь особо выбирать было не из чего. И в итоге получил все прелести поддержки исходников для нескольких систем с многочисленными #ifdef, написанием оберток под различающиеся интерфейсы операционных системы и прочими болезням данного решения.
Конечно, нет худа без добра, и в результате этой работы удалось значительно почистить кодовую базу проекта, но итоговое решение было очень тяжелым в поддержке. И его почти удалось победить и оставалось сделать только вызовы нативных функций через libffi, но «почти» вылилось в несколько недель работы, а тесты так и не получилось нормально запустить без косяков (возникали ошибки в библиотечных вызовах внутри msys-2.0.dll или cygwin1.dll), непонятные косяки с памятью и прочая мелочевка, а еще в довесок нужно было таскать кучу дополнительных dll.
Некоторое время назад я уже тестировал возможности JIT компиляции и даже была предпринята первая попытка перейти на clang. Но тогда пришлось пробираться через C++ классы по исходным текстам примеров, написанных под старые версии LLVM, и в итоге так ничего и не вышло (кроме статьи для Хабра Динамическая JIT компиляция С/С++ в LLVM с помощью Clang). Хотя в итоге оказалось, что результат этой работы мне очень сильно помог.
Поэтому, несмотря на имеющийся негативный опыт использования clang и LLVM, пришлось принять волевое решение сделать еще одну попытку перевести сборку на clang. Ведь в будущем это и так планировалось сделать, но изначально это были очень далекие планы, которые неожиданно стали насущной необходимостью.
И, как ни странно, все оказалось значительно проще, чем виделось изначально. Я взял за основу старые эксперименты для JIT компиляции С++ кода и все работы по переезду на компилятор clang заняли от силу пару дней (это в противовес нескольким неделям безрезультатных попыток использовать gcc под Linux и нативного компилятора под Windows).
Грабли clang и LLVM
Конечно, не удалось избежать и нескольких граблей. Под Linux при работе приложения (точнее при завершении его работы) всегда возникал Segmentation fault из-за двойного освобождения памяти, которого не было при сборке с помощью gcc. Полазив под отладчиком стало понятно, что проблема действительно специфическая для clang, т. к. LLVM и libtorch, который внутри себя тоже использует LLVM, при завершении приложения освобождают одну и туже статическую переменную.
Трассировка
#0 __GI___libc_free (mem=0x1) at malloc.c:3102и
#1 0x00007fffe3d0c113 in llvm::cl::Option::~Option() () from ../contrib/libtorch/lib/libtorch_cpu.so
#2 0x00007fffd93eafde in __cxa_finalize (d=0x7ffff6c74000) at cxa_finalize.c:83
#3 0x00007fffe0b80723 in __do_global_dtors_aux () from ../contrib/libtorch/lib/libtorch_cpu.so
#4 0x00007fffffffdd80 in ?? ()
#5 0x00007ffff7fe0f6b in _dl_fini () at dl-fini.c:138
#0 0x00000000c0200000 in ?? ()
#1 0x00007fffda7b844f in ?? () from /lib/x86_64-linux-gnu/libLLVM-13.so.1
#2 0x00007fffd95ecfde in __cxa_finalize (d=0x7fffdf9a0ba0) at cxa_finalize.c:83
#3 0x00007fffda743cd7 in ?? () from /lib/x86_64-linux-gnu/libLLVM-13.so.1
#4 0x00007fffffffdd80 in ?? ()
#5 0x00007ffff7fe0f6b in _dl_fini () at dl-fini.c:138
Обошел этот coredump тем, что вместо нормального выхода из main вызываю _exit, чтобы все остальные функции освобождения памяти не вызывались в самом процессе, оставив это на совести операционной системы. Примерно вот так _exit(RUN_ALL_TESTS());
Остальные проблемы возникли уже под Windows.
Если clang можно установить и использовать с Visual Studio в виде бинарной сборки, то вот библиотеки LLVM в бинарном виде не релизятся и их нужно собираться вручную (или искать предсобранные на сторонних сайтах).
Первые несколько попыток собрать LLVM под Windows провалились. Во время сборки виртуальной машине не хватало ОЗУ, и нативный компилятор от Microsoft вылетал из-за нехватки памяти.
Причем после увеличения доступной памяти для виртуалки с виндой втрое с 4 до 12 Гб не помогло, просто стало валится чуть позже в другом месте. А так как все это собирается ну очень долго, по несколько часов (и это на 16-ти ядерном процессоре!), то пришлось на время реквизировать десктопный компьютер у дочери с 32Г ОЗУ и запускать сборку на нем на всю ночь.
Ура, под утро все собралось! Но размер библиотек вместе с отладочной информацией получился несколько сотен мегабайт! И тут я понял, что что-то делаю не так. Пришлось разбираться в настройках сборки LLVM (до этого я просто запускал — собрать все), и о чудо, даже на самом сайте проекта написано для таких как я (кто читает документацию не до, а после).
The default Visual Studio configuration is Debug which is slow and generates a huge amount of debug information on disk.
После этого оставил для сборки только x86 платформу в релизной конфигурации и как итог — сборка LLVM заняла менее часа на виртуалке и больше никаких проблем не возникала. Вот что документация животворящая делает!!!
Все остальное заняло совсем немного времени, все собралось clang под Windows без проблем и … вылезли новые проблемы:
При выполнении юнит тестов в некоторых местах JIT компилятор падает с сообщением об ошибке:
MCJIT::runFunction does not support full-featured argument passing!!!
В результате пришлось делать временную заглушку и ловить эту ошибку, чтобы она не прерывала выполнение остальных тестов. Итог получился вполне удовлетворительный, из всех тестов не проходят только три из-за косяка в LLVM.
И это единственная проблема, которая так и осталась не побежденной. Но я решил сейчас с ней вообще не разбираться, хотя из-за этого и пришлось отложить поддержку NewLang под Windows до следующего релиза.
Надеюсь, что в будущих версиях LLVM либо это пофиксят, т.к. я сейчас использую LLVM 13, а на подходе уже 15, или верну вызов нативных функций опять с помощью библиотеки libffi.
Итоговые вкусности
Для меня плюсы от перехода с gcc на clang оказались значительно более весомыми, чем оставшиеся не решенными проблемы.
В первую очередь очень сильно радует значительно упростившаяся кросс платформенная разработка. Уже ненужно разруливать с помощью #ifdef разные версии компиляторов и платформ, разный синтаксис для подавления предупреждений и прочие индивидуальные особенности каждого используемого компилятора, ведь clang он в любой системе clang.
Вторым очень приятным моментом стал нормальный C-style интерфейс к библиотеке LLVM-C, в которую входят в том числе и функции для работы с динамическими библиотеками, поиск экспортируемых символов в исполняемом и библиотечных файлах и прочие плюшки, которые значительно упростили трудоемкость кроссплатформенной разработки и отпала необходимость в написании собственных оберток для GetProcAddress под Windows и dlsym под Linux.
Еще одной очень нужной фичей clang, у которой просто нет альтернативы в gcc, является интерфейс для работы с декорированными C++ именами и возможность работы в виде прилинкованной библиотеки. Это потребуется в следующем релизе NewLang для импорта нативных C++ функций с поддержкой проверки типов аргументов и для создания C++ классов в рантайме.
Ну и конечно, JIT компиляция, которую я еще пока не успел оценить по достоинству, так как в NewLang она еще полноценно не используется, но обязательно потребуется в будущем.
Как итог, прощай gcc и здравствуй clang!
Без вариантов!