Co to jest LLVM? Potęga Swift, Rust, Clang i nie tylko

W rozwijającym się krajobrazie pojawiają się nowe języki i ulepszenia istniejących. Mozilla's Rust, Apple's Swift, Jetbrains's Kotlin i wiele innych języków zapewnia programistom nową gamę opcji dotyczących szybkości, bezpieczeństwa, wygody, przenośności i mocy.

Dlaczego teraz? Jednym z ważnych powodów są nowe narzędzia do budowania języków - w szczególności kompilatory. Głównym z nich jest LLVM, projekt open source, pierwotnie opracowany przez twórcę języka Swift Chrisa Lattnera jako projekt badawczy na University of Illinois.

LLVM ułatwia nie tylko tworzenie nowych języków, ale także usprawnia rozwój już istniejących. Zapewnia narzędzia do automatyzacji wielu najbardziej niewdzięcznych części zadania tworzenia języka: tworzenie kompilatora, przenoszenie kodu wyjściowego na wiele platform i architektur, generowanie optymalizacji specyficznych dla architektury, takich jak wektoryzacja, oraz pisanie kodu obsługującego popularne metafory językowe, takie jak wyjątki. Jego liberalne licencje oznaczają, że można go swobodnie wykorzystywać ponownie jako komponent oprogramowania lub wdrażać jako usługę.

Lista języków korzystających z LLVM ma wiele znanych nazw. Język Swift firmy Apple używa LLVM jako struktury kompilatora, a Rust używa LLVM jako podstawowego składnika łańcucha narzędzi. Ponadto wiele kompilatorów ma edycję LLVM, na przykład Clang, kompilator C / C ++ (to nazwa „C-lang”), sam projekt jest ściśle powiązany z LLVM. Mono, implementacja .NET, ma opcję kompilacji do kodu natywnego przy użyciu zaplecza LLVM. A Kotlin, nominalnie język JVM, opracowuje wersję języka o nazwie Kotlin Native, która używa LLVM do kompilacji do kodu maszynowego.

Zdefiniowano LLVM

W istocie LLVM jest biblioteką do programistycznego tworzenia kodu natywnego dla maszyn. Deweloper używa interfejsu API do generowania instrukcji w formacie zwanym reprezentacją pośrednią lub IR. LLVM może następnie skompilować IR do samodzielnego pliku binarnego lub wykonać kompilację JIT (just-in-time) na kodzie, aby uruchomić w kontekście innego programu, takiego jak interpreter lub środowisko uruchomieniowe języka.

Interfejsy API LLVM zapewniają prymitywy do tworzenia wielu typowych struktur i wzorców występujących w językach programowania. Na przykład, prawie każdy język ma koncepcję funkcji i zmiennej globalnej, a wiele z nich ma koreprogramy i interfejsy funkcji obcych języka C. LLVM ma funkcje i zmienne globalne jako standardowe elementy w swoim IR i ma metafory do tworzenia procedur i łączenia się z bibliotekami C.

Zamiast tracić czas i energię na wymyślanie na nowo tych konkretnych kół, możesz po prostu użyć implementacji LLVM i skupić się na tych częściach języka, które wymagają uwagi.

Przeczytaj więcej o Go, Kotlin, Pythonie i Rust 

Iść:

  • Dotknij możliwości języka Go Google
  • Najlepsze IDE i edytory języka Go

Kotlin:

  • Co to jest Kotlin? Wyjaśnienie alternatywy dla języka Java
  • Frameworki Kotlin: przegląd narzędzi programistycznych JVM

Pyton:

  • Co to jest Python? Wszystko co musisz wiedzieć
  • Samouczek: Jak rozpocząć pracę z Pythonem
  • 6 podstawowych bibliotek dla każdego programisty Pythona

Rdza:

  • Co to jest Rust? Sposób na bezpieczne, szybkie i łatwe tworzenie oprogramowania
  • Dowiedz się, jak zacząć korzystać z Rust 

LLVM: zaprojektowany do przenoszenia

Aby zrozumieć LLVM, pomocne może być rozważenie analogii do języka programowania C: C jest czasami opisywany jako przenośny język asemblera wysokiego poziomu, ponieważ ma konstrukcje, które mogą być blisko sprzętu systemowego i został przeniesiony na prawie każda architektura systemu. Ale C jest użyteczny jako przenośny język asemblera tylko do pewnego momentu; nie został zaprojektowany do tego konkretnego celu.

Natomiast IR LLVM był od początku projektowany jako przenośny zestaw. Jednym ze sposobów osiągnięcia tej przenośności jest oferowanie elementów podstawowych niezależnych od określonej architektury maszyny. Na przykład typy liczb całkowitych nie są ograniczone do maksymalnej szerokości bitowej podstawowego sprzętu (na przykład 32 lub 64 bity). Można tworzyć pierwotne typy liczb całkowitych, używając dowolnej liczby bitów, na przykład 128-bitowej liczby całkowitej. Nie musisz też martwić się o tworzenie wyników dopasowanych do zestawu instrukcji konkretnego procesora; LLVM zadba o to również za Ciebie.

Neutralny pod względem architektury projekt LLVM ułatwia obsługę wszelkiego rodzaju sprzętu, obecnego i przyszłego. Na przykład IBM niedawno dostarczył kod do obsługi swojego systemu z / OS, Linux on Power (w tym obsługi biblioteki wektoryzacji MASS firmy IBM) i architektur AIX dla projektów LLVM w językach C, C ++ i Fortran. 

Jeśli chcesz zobaczyć rzeczywiste przykłady LLVM IR, przejdź do witryny projektu ELLCC i wypróbuj wersję demonstracyjną na żywo, która konwertuje kod C na LLVM IR bezpośrednio w przeglądarce.

Jak języki programowania używają LLVM

Najczęstszym przypadkiem użycia dla LLVM jest kompilator z wyprzedzeniem (AOT) dla języka. Na przykład projekt Clang z wyprzedzeniem kompiluje C i C ++ do natywnych plików binarnych. Ale LLVM umożliwia także inne rzeczy.

Kompilacja just-in-time z LLVM

Niektóre sytuacje wymagają, aby kod był generowany w locie w czasie wykonywania, a nie kompilowany z wyprzedzeniem. Na przykład język Julia JIT kompiluje swój kod, ponieważ musi działać szybko i współdziałać z użytkownikiem za pośrednictwem REPL (pętla odczytu-ewalu-wydruku) lub interaktywnej zachęty. 

Numba, pakiet przyspieszający matematykę dla Pythona, JIT-kompiluje wybrane funkcje Pythona do kodu maszynowego. Może również skompilować kod ozdobiony Numba z wyprzedzeniem, ale (podobnie jak Julia) Python oferuje szybki rozwój, będąc językiem interpretowanym. Używanie kompilacji JIT do tworzenia takiego kodu uzupełnia interaktywny przepływ pracy Pythona lepiej niż kompilacja z wyprzedzeniem.

Inni eksperymentują z nowymi sposobami wykorzystania LLVM jako JIT, takimi jak kompilowanie zapytań PostgreSQL, co daje nawet pięciokrotny wzrost wydajności.

Automatyczna optymalizacja kodu za pomocą LLVM

LLVM nie tylko kompiluje IR do natywnego kodu maszynowego. Możesz również programowo nakierować go na optymalizację kodu z wysokim stopniem szczegółowości, przez cały proces łączenia. Optymalizacje mogą być dość agresywne, w tym takie rzeczy, jak wstawianie funkcji, eliminowanie martwego kodu (w tym nieużywanych deklaracji typów i argumentów funkcji) i rozwijanie pętli.

Ponownie, moc tkwi w tym, że nie musisz wdrażać tego wszystkiego samodzielnie. LLVM może obsłużyć je za Ciebie lub możesz polecić mu wyłączenie ich w razie potrzeby. Na przykład, jeśli chcesz mieć mniejsze pliki binarne kosztem pewnej wydajności, możesz mieć interfejs użytkownika kompilatora, który powie LLVM, aby wyłączyć rozwijanie pętli.

Języki specyficzne dla domeny z LLVM

LLVM był używany do tworzenia kompilatorów dla wielu języków ogólnego przeznaczenia, ale jest również przydatny do tworzenia języków, które są bardzo wertykalne lub wyłączne dla domeny problemowej. Pod pewnymi względami jest to miejsce, w którym LLVM świeci najjaśniej, ponieważ usuwa wiele uciążliwości przy tworzeniu takiego języka i sprawia, że ​​działa dobrze.

Na przykład projekt Emscripten pobiera kod LLVM IR i konwertuje go na JavaScript, teoretycznie umożliwiając każdemu językowi z zapleczem LLVM eksportowanie kodu, który można uruchomić w przeglądarce. Plan długoterminowy zakłada posiadanie zaplecza opartego na LLVM, który może generować WebAssembly, ale Emscripten jest dobrym przykładem tego, jak elastyczny może być LLVM.

Innym sposobem wykorzystania LLVM jest dodanie rozszerzeń specyficznych dla domeny do istniejącego języka. Nvidia użyła LLVM do stworzenia kompilatora Nvidia CUDA, który pozwala językom dodawać natywne wsparcie dla CUDA, które kompiluje się jako część generowanego kodu natywnego (szybciej), zamiast być wywoływanym przez bibliotekę dostarczoną z nim (wolniej).

Sukces LLVM z językami specyficznymi dla domeny pobudził nowe projekty w LLVM do rozwiązania problemów, które stwarzają. Największym problemem jest to, że niektóre DSL są trudne do przetłumaczenia na LLVM IR bez ciężkiej pracy na przednim końcu. Jednym z rozwiązań w pracach jest wielopoziomowa reprezentacja pośrednia, czyli projekt MLIR.

MLIR zapewnia wygodne sposoby reprezentowania złożonych struktur danych i operacji, które można następnie automatycznie przetłumaczyć na LLVM IR. Na przykład platforma uczenia maszynowego TensorFlow może mieć wiele złożonych operacji na wykresie przepływu danych, które można skutecznie skompilować do kodu natywnego za pomocą MLIR.

Praca z LLVM w różnych językach

Typowym sposobem pracy z LLVM jest użycie kodu w języku, w którym czujesz się komfortowo (i który oczywiście obsługuje biblioteki LLVM).

Dwie popularne opcje językowe to C i C ++. Wielu programistów LLVM domyślnie wybiera jeden z tych dwóch z kilku ważnych powodów: 

  • Sam LLVM jest napisany w C ++.
  • Interfejsy API LLVM są dostępne w wersjach C i C ++.
  • Znaczna część rozwoju języka ma miejsce z C / C ++ jako podstawą

Jednak te dwa języki to nie jedyne możliwości. Wiele języków może wywoływać natywnie biblioteki C, więc teoretycznie jest możliwe wykonanie programowania LLVM w dowolnym takim języku. Ale dobrze jest mieć rzeczywistą bibliotekę w języku, który elegancko otacza API LLVM. Na szczęście wiele języków i środowisk wykonawczych języków ma takie biblioteki, w tym C # / .NET / Mono, Rust, Haskell, OCAML, Node.js, Go i Python.

Jedynym zastrzeżeniem jest to, że niektóre powiązania języka z LLVM mogą być mniej kompletne niż inne. Na przykład w Pythonie istnieje wiele możliwości wyboru, ale każdy różni się pod względem kompletności i użyteczności:

  • llvmlite, opracowany przez zespół, który tworzył Numba, pojawił się jako obecny pretendent do pracy z LLVM w Pythonie. Implementuje tylko podzbiór funkcjonalności LLVM, zgodnie z potrzebami projektu Numba. Ale ten podzbiór zapewnia zdecydowaną większość tego, czego potrzebują użytkownicy LLVM. (llvmlite to ogólnie najlepszy wybór do pracy z LLVM w Pythonie).
  • Projekt LLVM utrzymuje własny zestaw powiązań z interfejsem API języka C LLVM, ale obecnie nie są one obsługiwane.
  • llvmpy, pierwsze popularne powiązanie Pythona dla LLVM, wypadło z konserwacji w 2015 roku. Złe dla każdego projektu oprogramowania, ale gorsze podczas pracy z LLVM, biorąc pod uwagę liczbę zmian, które pojawiają się w każdej edycji LLVM.
  • llvmcpy ma na celu zaktualizowanie powiązań Pythona dla biblioteki C, zapewnienie ich aktualizacji w sposób zautomatyzowany i udostępnienie ich za pomocą natywnych idiomów Pythona. llvmcpy jest wciąż na wczesnym etapie, ale już może wykonać pewne podstawowe prace z interfejsami API LLVM.

Jeśli jesteś ciekawy, jak używać bibliotek LLVM do tworzenia języka, twórcy LLVM mają samouczek, używając C ++ lub OCAML, który prowadzi Cię przez tworzenie prostego języka o nazwie Kaleidoscope. Od tego czasu został przeniesiony na inne języki:

  • Haskell:  bezpośredni port oryginalnego samouczka.
  • Python: jeden taki port jest ściśle zgodny z samouczkiem, podczas gdy drugi to bardziej ambitna przeróbka z interaktywną linią poleceń. Oba używają llvmlite jako powiązań z LLVM.
  • Rust Swift: Wydawało się nieuniknione, że przeniesiemy samouczek na dwa języki, które LLVM pomogło stworzyć.

Wreszcie samouczek jest również dostępny w  językach ludzi . Został przetłumaczony na język chiński przy użyciu oryginalnego C ++ i Pythona.

Czego LLVM nie robi

Biorąc pod uwagę wszystko, co zapewnia LLVM, warto również wiedzieć, czego nie robi.

Na przykład LLVM nie analizuje gramatyki języka. Wiele narzędzi już to wykonuje, takich jak lex / yacc, flex / bison, Lark i ANTLR. Parsowanie i tak ma być oddzielone od kompilacji, więc nie jest zaskakujące, że LLVM nie próbuje tego rozwiązać.

LLVM również nie odnosi się bezpośrednio do szerszej kultury oprogramowania w danym języku. Instalowanie plików binarnych kompilatora, zarządzanie pakietami podczas instalacji i aktualizowanie łańcucha narzędzi - musisz to zrobić samodzielnie.

Wreszcie, co najważniejsze, nadal istnieją wspólne części języków, dla których LLVM nie zapewnia prymitywów. W wielu językach istnieje pewien sposób zarządzania pamięcią zbieraną bezużytkami, jako główny sposób zarządzania pamięcią lub jako dodatek do strategii takich jak RAII (których używają C ++ i Rust). LLVM nie zapewnia mechanizmu wyrzucania elementów bezużytecznych, ale zapewnia narzędzia do implementacji wyrzucania elementów bezużytecznych, umożliwiając oznaczanie kodu metadanymi, co ułatwia pisanie elementów odśmiecających.

Jednak nic z tego nie wyklucza możliwości, że LLVM może w końcu dodać natywne mechanizmy do implementacji czyszczenia pamięci. LLVM rozwija się szybko, a główne wydanie pojawia się mniej więcej co sześć miesięcy. Tempo rozwoju prawdopodobnie wzrośnie tylko dzięki temu, że wiele obecnych języków umieściło LLVM w centrum ich procesu rozwoju.