На прошлой неделе, после нескольких месяцев разработки, вышла очередная версия языка программирования 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!
Без вариантов!

Комментарии (3)


  1. zorn_v
    15.08.2022 21:48
    -6

    Как мы использовали кучу других языков и компиляторов, чтобы создать ЯЗЫК #1

    Название то какое клёвое...
    https://www.youtube.com/watch?v=Km5XQxRrQvw&t=45s

    На КДПВ GCC из яйца вылупился ? Серьезно ?


  1. sergio_nsk
    16.08.2022 05:56
    +3

    После этого оставил для сборки только x86 платформу в релизной конфигурации и как итог — сборка LLVM заняла менее часа на виртуалке и больше никаких проблем не возникала.

    Ваши с дочерью компьютеры на жёстких дисках что ли? LLVM собирается достаточно быстро, и обычно Debug-сборка делается быстрее, чем Release с оптимизациями. Если размер получаемых файлов так фатально влияет на скорость сборки, то что-то не так с железом.

    Segmentation fault из-за двойного освобождения памяти

    Я не думаю, что проблема в LLVM. C++ runtime указывали? libtorch_cpu.so скорее всего собран со статическим LLVM libc++, а вы собирались и линковались с гнутым libsdc++ .


    1. rsashka Автор
      16.08.2022 08:41

      Влияет не размер файлов, а количество вариантов сборки для разных типов целевых процессоров. После того, как оставил только x64 стало собираться очень шустро.

      Насчет статической линковки, вы скорее всего правы, т.к. libtorch_cpu на 100% слинкован статически, а я линкуюсь динамически, поэтому и вылез косяк.