Suchen

MSVC, GCC und Clang/LLVM: Compiler-spezifische Vor- und Nachteile

Autor / Redakteur: Filipe Martins & Anna Kobylinska / Stephan Augsten

Ein Compiler ist Herz und Seele einer leistungsstarken Development Toolchain. Mit diesem zentralen Infrastrukturelement steht und fällt Entwickler-Produktivität. Die hochperformante Ausführung von Code braucht einen zuverlässigen Compiler.

Firmen zum Thema

Visual Studio unter Ubuntu Linux mit Einbindung des Compiler-unabhängigen Tools.
Visual Studio unter Ubuntu Linux mit Einbindung des Compiler-unabhängigen Tools.
(Bild: Bullet / Microsoft / Canonical)

Aktiv gepflegte Entwicklungssprachen gibt es mittlerweile in Hülle und Fülle für jede auch so spezialisierte Aufgabenstellung; das Aufkommen von verteilten Anwendungsarchitekturen mit KI/ML-Schnittstellen geht mit der sogenannten Polyglott-Programmierung Hand in Hand.

Relevante Compiler-Familien gibt es dennoch nur genau drei: MSVC, GCC und LLVC. Sie unterscheiden sich wie Tag und Nacht, während die Probleme, die sie lösen, in ihrer Komplexität immer stärker zunehmen.

Die wachsende Abstraktion zwischen der Hardwareschicht und Datenstrukturen auf der einen Seite und „menschentauglicher“ Logikebene auf der anderen Seite stellt Compiler vor eine ganze Reihe an Herausforderungen. Das Aufkommen von Polyglott-Programmierung, insbesondere im Rahmen von Microservices und von KI-gestützter Datenanalyse, erschwert sie noch zusätzlich.

Das Compiler-Dilemma

Moderne Compiler müssen nicht nur ausführbare Hochleistungsprogramme generieren, sondern auch selbst eine hohe Leistung aufweisen. Ein großes Softwareprojekt in C++ kann hunderte bis tausende einzelner Übersetzungseinheiten enthalten. Jede Übersetzungseinheit kann sich wiederum aus tausenden von Codezeilen zusammensetzen. C++ - Code kann zudem auch eine große Anzahl von vorlagenbasierten Programmiertechnologien verwenden.

Bei diesen Technologien muss der Compiler relevante Informationen mehrmals übertragen, um eine Zieldatei zu generieren. Die Kompilierung großer C++-Projekte kann mehrere Stunden dauern. Mehrere voneinander abhängige Änderungen müssen in den Code gleichzeitig einfließen, was die erneute Kompilierung der betreffenden Bibliotheken der Codebasis erzwingt.

Daher sind schnellere Compiler im Speziellen und Build-Tools im Allgemeinen entscheidend für eine hohe Entwickler-Produktivität, erst recht in großen Teams. Sie sind auch die wichtigste Voraussetzung dafür, moderne Chip-Architekturen auszureizen.

Moderne Prozessoren verfügen über superskalare, lange Pipelines sowie komplexe interne Strukturen und unterstützen Vektorerweiterungseinheiten sowohl in der CISC- als auch in der RISC-Architektur. Bei vielen Anwendungen können die Entwickler Vektorerweiterungsbefehle verwenden, um die Programmausführungsleistung erheblich zu verbessern.

Bei Matrix- und Vektoroperationen kommen beispielsweise kombinierte Multiplikations- und Additionsbefehle zum Einsatz, um die Leistung und Genauigkeit zu verbessern. Entwickler können sich etwa mit Bitmaskenbefehlen behelfen, um die Verzweigungsverarbeitung in Vektoroperationen zu beschleunigen.

Um jedoch die höchste Leistung zu erzielen, müssen Entwickler (und ihre Compiler) nach wie vor viel Aufwand treiben, um Aufgaben mit komplexen Speicherzugriffsmodi und nicht standardkonformen Kernels zu bewältigen. Die Aufgaben, die Compiler absolut zuverlässig lösen müssen, um die fehlerfreie Funktionsfähigkeit von Code zu gewährleisten, nehmen in ihrer Komplexität immer stärker zu.

MSVC

Microsofts Ökosystem von Lösungen rund um die Softwareentwicklung stützt sich auf MSVC, den sogenannten Microsoft Visual C++ Compiler, und MSBuild, Microsofts hauseigenes, quelloffenes Automatisierungswerkzeug (die sogenannte Build Engine). Dieses bildet das Herzstück von Visual Studio, obwohl es selbst interessanterweise auch ohne die IDE auskommt.

Doch Microsoft tanzt gerne auf allen Hochzeiten. Wer sich mit dem Standard-Combo aus MSVC und MSBuild nicht anfreunden kann, dem steht es unter Windows frei, auf ein Compiler-agnostisches Werkzeug aus der Linux-Welt namens CMake auszuweichen. Linux-Entwickler auf der Windows-Plattform machen aus dieser Möglichkeit reichlich Gebrauch.

Die Compiler GCC und LLVM finden so auch in Microsoft-zentrische Toolchains Einzug und sind damit auf allen führenden Entwicklungsplattformen verfügbar. Offenbar zieht Microsoft die Maximierung der Entwicklerproduktivität auf der eigenen Plattform anderen Überlegungen vor und ist mit diesem Ansatz in der letzten Zeit recht gut gefahren.

Microsofts IDEs Visual Studio Code (mit 57 Prozent) und Visual Studio (31,5 Prozent) konnten in der aktuellen Umfrage von StackOverflow (Edition 2019) die beiden begehrten Spitzenplätze als die beliebtesten Entwicklungsumgebungen besetzen. Auf Windows als Betriebssystem trifft die Aussage aber schon mal nicht mehr zu. Diesen Titel konnte Linux einheimsen. 83,1 Prozent aller Entwickler, die mit diesem quelloffenen Betriebssystem arbeiten, wollen es beibehalten; unter den Windows-Nutzern bekennen sich zu einer ähnlichen Loyalität lediglich 64,2%. (Rund jeder Zweite der Befragten arbeitet unter Windows, ein Viertel unter macOS und ein weiteres unter Linux.)

GNU Compiler Collection

Der Name GCC steht für die GNU Compiler Collection, eine quelloffene Sammlung von Tools für GNU und Linux. Dieses erstmals im Jahre 1987 veröffentlichte Toolset entstammte der Feder von Richard Stallman, einem der frühen Verfechter von Open Source, dem Gründer und ehemaligen Vorsitzenden der Free Software Foundation.

Auch heute nach all den Jahren bildet GCC den Grundpfeiler von Linux. Der Linux-Kernel lässt sich nämlich derzeit nach wie vor ohne Modifikationen nur mit GCC kompilieren. Zwischendurch hatte sich die GCC-Gemeinde der Kernentwickler über grundlegende Meinungsunterschiede aufgesplittet und so entstand ein GCC-Fork namens EGCS. Der Fork konnte sich bald bewähren und als zukunftsträchtig erwiesen.

Stallman krönte ihn in der dritten Generation zum neuen Hauptzweig und übergab die Kontrolle über das Projekt an das GCC Industrial Committee. So ließ sich das Projekt vorerst aufs Neue beflügeln. Dennoch zog sich der Weg zur nachfolgenden vierten Generation von GCC wie ein Kaugummi in die Länge bis die Entwicklergemeinde des nicht enden wollenden Wartens überdrüssig wurde.

Als sich der eine oder andere GCC-Nutzer händeringend nach Alternativen umschaute, war mit LLVM bereits im Dezember 2003 eine reizvolle Alternative entstanden. Inzwischen liegt GCC in der Version 9.3 (stable) vor; GCC 10 steht auch bereits in den Startlöchern. Interessanterweise hat auch LLVM die Versionsnummer 10 erreicht. Zwischen GCC und LLVM hat sich im Laufe der Zeit ein durchaus gesunder Konkurrenzkampf entwickelt, der vor allem den Entwicklern zu Gute kommt.

Clang/LLVM

Bei LLVM handelt es sich um eine Sammlung modularer, wiederverwendbarer Compiler- und Toolchain-Technologien, die aus einem Forschungsprojekt der Universität zu Illinois hervorging. LLVM verfolgt das Ziel, eine universelle Schnittstelle zum Kompilieren beliebiger Sprachen zu erschaffen.

Das Compiler-Infrastrukturprojekt LLVM besteht aus einer Reihe von Compiler- und Toolchain-Technologien, mit denen Entwickler ein Front-End für jede Programmiersprache und ein Back-End für jede Befehlssatzarchitektur zusammenstellen können. LLVM basiert auf einer sprachunabhängigen Zwischendarstellung, die als tragbare Assemblersprache auf hoher Ebene dient und mit einer Vielzahl von Transformationen über mehrere Durchgänge optimiert werden kann.

Clang steht für „C language family frontend for LLVM“. Die Aufgabe von Clang besteht darin, den Code in ein Interimsformat zu wandeln, damit der LLVM-Code-Generation-Layer daraus den finalen Maschinencode generieren kann. Clang unterstützt u.a. C, C++, Objective C/C++, OpenCL, CUDA und RenderScript. Es ist als ein Drop-In-Ersatz für die GNU Compiler Collection (GCC) gedacht und unterstützt die meisten Kompilierungsflags und inoffizielle Spracherweiterungen.

Das LLVM-Projekt distanziert sich von der gängigen Aufschlüsselung als eine „Low Level Virtual Machine“, um unliebsame Verwechslungen mit VMs zu vermeiden. Zu den Stärken von Clang/LLVM zählen die fortgeschrittene Modularität dieser Infrastrukturlösung und die einfache Integration der verschiedenen Bausteine in IDEs.

Anders als die LLVM-Toolchain arbeitet GCC in einem sequenziellen Compile-Link-Debug-Workflow und verweigert die direkte Zusammenarbeit mit IDEs. Features wie die Syntax-Hervorhebung oder die automatische Code-Vervollständigung lassen sich mit GCC nur über Umwege umsetzen. Das Duo aus Clang und LLVM bildet im Übrigen den Unterbau von Apples Xcode. Doch die bei weitem interessanteste Implementierung ist der JetBrains-Compiler.

Kotlin/Native mit LLVM

Kotlin/Native ist ein LLVM-Backend für den Kotlin-Compiler, die Laufzeitimplementierung und die native Codegenerierungsfunktion auf der Basis des LLVM-Toolchains. Kotlin/Native soll in erster Linie die Kompilierung für Plattformen ermöglichen, auf denen virtuelle Maschinen nicht erwünscht oder nicht möglich sind (z. B. iOS oder Embedded) oder auf denen ein Entwickler bereit ist, ein eigenständiges Programm in angemessener Größe ohne separate Runtime zu erstellen

Die Arbeitsweise des JetBrains-Compilers ähnelt im Großen und Ganzen einer Pipeline, welche Quelldateien aufnimmt und diese schrittweise in ausführbaren Code umwandelt. Der erste große Schritt in dieser Pipeline wird umgangssprachlich als Front-End des Compilers bezeichnet. Diese Lösung analysiert den Code, löst Namen auf, führt Typprüfungen und dergleichen andere Aufgaben durch.

Dieser Teil des Compilers funktioniert u.a. auch in der IDE, wenn er Fehler hervorhebt, zu Definitionen navigiert und nach Symbolverwendungen in dem betreffenden Projekt sucht. Dies ist der Schritt, mit dem beispielsweise Kotlin heutzutage die meiste Zeit verbringt. JetBrains will eben diese Phase in künftigen Versionen um ein Vielfaches beschleunigen.

Nachdem das Front-End des Compilers den Code analysiert hat, generiert sein Back-End die ausführbaren Dateien. JetBrains hat beispielsweise drei Backends: Kotlin/JVM, Kotlin/JS und Kotlin/Native. Die ersten beiden wurden historisch unabhängig voneinander geschrieben und teilten nicht viel Code. Kotlin/Native setzt auf einer neuen Infrastruktur auf, die auf einer internen Darstellung (IR) für Kotlin-Code basiert und eine Funktion erfüllt, die dem Bytecode in virtuellen Maschinen ähnelt.

JetBrains ist gerade dabei, die beiden anderen Backends auf dieselbe IR zu migrieren. Infolge dessen werde der Compiler einen Großteil der Back-End-Logik gemeinsam nutzen und über eine einheitliche Pipeline verfügen, damit die meisten Funktionen, Optimierungen und Bugfixes für alle Ziele nur einmal ausgeführt werden müssen. Die Umstellung auf die neuen Backends erfolgt schrittweise, beginnend in Kotlin 1.4. Die aktuelle Implementierung des neuen Compilers ist noch nicht abgeschlossen und wird zur Veröffentlichung von Kotlin 1.4 nicht fertig sein.

Wer den neuen Algorithmus für die Typinferenz auf die Probefahrt genommen haben sollte, hatte bereits Gelegenheit, sich von den inneren Werten des neuen Compilers selbst ein Bild zu machen. Der Ansatz für andere Teile der Umgebung werde demnach der Gleiche sein; beide Versionen des Compilers sollen für einige Zeit parallel zueinander fortbestehen bis der neue Compiler die nötige Stabilität erreicht haben sollte.

Fazit

Die Modularität der LLVM-Infrastruktur fördert Agilität in bisher ungekanntem Maße. Clang mit LLVM kann nicht „nur“ den vollständigen GCC-Stack ersetzen, sondern lässt sich viel einfacher mit anderen Elementen der gewünschten Toolchain integrieren und erweitern. So wird Entwickler-Produktivität groß geschrieben.

(ID:46646528)