&Notepad

Will Crichton – September 9, 2018
Ich habe ein Problem mit dem Begriff „Systemprogrammierung“. Mir schien es immer, als ob er unnötigerweise zwei Ideen kombiniert: Low-Level-Programmierung (Umgang mit Implementierungsdetails der Maschine) und Systemdesign (Erstellen und Verwalten eines komplexen Satzes von interagierenden Komponenten). Warum ist das der Fall? Wie lange ist das schon so? Und was könnten wir gewinnen, wenn wir das Konzept der Systeme neu definieren?

1970er Jahre: Verbesserung der Assemblierung

Lassen Sie uns zu den Ursprüngen der modernen Computersysteme zurückreisen, um zu verstehen, wie sich der Begriff entwickelt hat. Ich weiß nicht, wer den Begriff ursprünglich geprägt hat, aber meine Recherchen deuten darauf hin, dass ernsthafte Bemühungen zur Definition von „Computersystemen“ in den frühen 70er Jahren begannen. In Systems Programming Languages (Bergeron1 et al. 1972) sagen die Autoren:

Ein Systemprogramm ist ein integrierter Satz von Unterprogrammen, die zusammen ein Ganzes bilden, das größer ist als die Summe seiner Teile, und die eine bestimmte Schwelle von Größe und/oder Komplexität überschreiten. Typische Beispiele sind Systeme für Multiprogramming, Übersetzen, Simulieren, Informationsmanagement und Time-Sharing. Nachfolgend sind einige Eigenschaften aufgeführt, von denen einige auch in Nicht-Systemen zu finden sind, die aber nicht alle in einem bestimmten System vorhanden sein müssen.

  1. Das zu lösende Problem ist weit gefasst und besteht aus vielen, meist recht unterschiedlichen Teilproblemen.
  2. Das Systemprogramm wird wahrscheinlich verwendet, um andere Software- und Anwendungsprogramme zu unterstützen, kann aber auch selbst ein komplettes Anwendungspaket sein.
  3. Es ist eher für den kontinuierlichen „Produktionseinsatz“ als für die einmalige Lösung eines einzelnen Anwendungsproblems konzipiert.
  4. Es entwickelt sich wahrscheinlich ständig weiter, was die Anzahl und die Art der unterstützten Funktionen angeht.
  5. Ein Systemprogramm erfordert eine bestimmte Disziplin oder Struktur, sowohl innerhalb als auch zwischen den Modulen (d.h. „Kommunikation“), und wird in der Regel von mehr als einer Person entworfen und implementiert.

Diese Definition ist recht angenehm, denn Computersysteme sind groß, werden lange genutzt und sind zeitlich variabel. Während diese Definition jedoch weitgehend deskriptiv ist, ist ein Schlüsselgedanke des Papiers präskriptiv: Er befürwortet die Trennung von Low-Level-Sprachen von Systemsprachen (damals wurde Assembler mit FORTRAN verglichen).

Das Ziel einer Systemprogrammiersprache ist es, eine Sprache bereitzustellen, die ohne übermäßige Rücksicht auf „Bit-Twiddling“-Überlegungen verwendet werden kann und dennoch Code erzeugt, der nicht wesentlich schlechter ist als der von Hand erzeugte. Eine solche Sprache sollte die Prägnanz und Lesbarkeit von Hochsprachen mit der Raum- und Zeiteffizienz und der Fähigkeit, an Maschinen- und Betriebssystemeinrichtungen heranzukommen, die in Assemblersprache erhältlich sind, kombinieren. Die Entwurfs-, Schreib- und Debugging-Zeit sollte minimiert werden, ohne die Systemressourcen unnötig zu belasten.

Zur gleichen Zeit veröffentlichten Forscher der CMU BLISS: A Language for Systems Programming (Wulf et al. 1972) und beschrieben es als:

Wir bezeichnen BLISS als eine „Implementierungssprache“, obwohl wir zugeben, dass der Begriff etwas zweideutig ist, da vermutlich alle Computersprachen dazu benutzt werden, etwas zu implementieren. Für uns bedeutet der Ausdruck eine höhere Sprache für allgemeine Zwecke, bei der der Schwerpunkt auf einer bestimmten Anwendung liegt, nämlich dem Schreiben großer, produktiver Softwaresysteme für eine bestimmte Maschine. Spezialsprachen, wie Compiler-Compiler, fallen nicht in diese Kategorie, und wir gehen auch nicht unbedingt davon aus, dass diese Sprachen maschinenunabhängig sein müssen. Wir betonen das Wort „Implementierung“ in unserer Definition und haben Wörter wie „Design“ und „Dokumentation“ nicht verwendet. Wir gehen nicht unbedingt davon aus, dass eine Implementierungssprache ein geeignetes Mittel ist, um den ursprünglichen Entwurf eines großen Systems auszudrücken oder dieses System ausschließlich zu dokumentieren. Konzepte wie Maschinenunabhängigkeit, Ausdruck des Entwurfs und der Implementierung in derselben Notation, Selbstdokumentation und andere sind eindeutig wünschenswerte Ziele und Kriterien, nach denen wir verschiedene Sprachen bewertet haben.

Hier stellen die Autoren die Idee einer „Implementierungssprache“ gegenüber, die auf einer höheren Ebene als Assembler, aber auf einer niedrigeren Ebene als eine „Entwurfssprache“ liegt. Dies widerspricht der Definition im vorhergehenden Beitrag, der dafür plädiert, dass der Entwurf eines Systems und die Implementierung eines Systems getrennte Sprachen haben sollten.

Bei beiden Beiträgen handelt es sich um Forschungsartefakte oder Befürwortungen. Der letzte Beitrag (ebenfalls aus dem produktiven Jahr 1972!) ist Systems Programming (Donovan 1972), ein Lehrbuch zum Erlernen der Systemprogrammierung.

Was ist Systemprogrammierung? Man kann sich einen Computer als eine Art Tier vorstellen, das allen Befehlen gehorcht. Man hat gesagt, dass Computer im Grunde genommen Menschen aus Metall sind oder umgekehrt, dass Menschen Computer aus Fleisch und Blut sind. Wenn wir uns jedoch näher mit Computern befassen, stellen wir fest, dass sie im Grunde genommen Maschinen sind, die sehr spezifischen und primitiven Anweisungen folgen. In den Anfängen der Computer kommunizierten die Menschen mit ihnen über Ein- und Ausschalter, die primitive Anweisungen gaben. Bald wollten die Menschen komplexere Anweisungen geben. Man wollte zum Beispiel sagen können: X = 30 * Y; wenn Y = 10 ist, was ist dann X? Heutige Computer können eine solche Sprache ohne die Hilfe von Systemprogrammen nicht verstehen. Systemprogramme (z. B. Compiler, Lader, Makroprozessoren, Betriebssysteme) wurden entwickelt, um die Computer besser an die Bedürfnisse ihrer Benutzer anzupassen. Außerdem wünschten sich die Menschen mehr Unterstützung bei der Erstellung ihrer Programme.

Ich mag es, dass diese Definition uns daran erinnert, dass Systeme im Dienste der Menschen stehen, auch wenn sie nur eine Infrastruktur darstellen, die nicht direkt dem Endbenutzer ausgesetzt ist.

1990er Jahre: Der Aufstieg des Scripting

In den 70er und 80er Jahren sahen die meisten Forscher die Systemprogrammierung in der Regel als Gegensatz zur Assembler-Programmierung. Es gab einfach keine anderen guten Werkzeuge, um Systeme zu erstellen. (Ich bin mir nicht sicher, wo Lisp in all dem war? In keiner der Quellen, die ich gelesen habe, wurde Lisp erwähnt, obwohl ich vage weiß, dass es Lisp-Maschinen gab, wenn auch nur für kurze Zeit.)

Mitte der 90er Jahre kam es jedoch zu einer großen Veränderung bei den Programmiersprachen mit dem Aufkommen der dynamisch getippten Skriptsprachen. Ausgehend von früheren Shell-Skriptsystemen wie Bash, setzten sich Sprachen wie Perl (1987), Tcl (1988), Python (1990), Ruby (1995), PHP (1995) und Javascript (1995) durch. Dies gipfelte in dem einflussreichen Artikel „Scripting: Higher Level Programming for the 21st Century“ (Ousterhout 1998). Darin wird die „Ousterhout’sche Dichotomie“ zwischen „Systemprogrammiersprachen“ und „Skriptsprachen“ formuliert.

Skriptsprachen sind für andere Aufgaben als Systemprogrammiersprachen konzipiert, was zu grundlegenden Unterschieden in den Sprachen führt. Systemprogrammiersprachen wurden entwickelt, um Datenstrukturen und Algorithmen von Grund auf neu zu erstellen, ausgehend von den primitivsten Computerelementen wie z. B. Worten im Speicher. Im Gegensatz dazu sind Skriptsprachen auf das Kleben ausgelegt: Sie setzen die Existenz einer Reihe leistungsfähiger Komponenten voraus und sind in erster Linie dazu gedacht, Komponenten miteinander zu verbinden. Systemprogrammiersprachen sind stark typisiert, um die Komplexität zu beherrschen, während Skriptsprachen typfrei sind, um die Verbindungen zwischen Komponenten zu vereinfachen und eine schnelle Anwendungsentwicklung zu ermöglichen. Mehrere aktuelle Trends wie schnellere Maschinen, bessere Skriptsprachen, die zunehmende Bedeutung von grafischen Benutzeroberflächen und Komponentenarchitekturen sowie das Wachstum des Internets haben die Anwendbarkeit von Skriptsprachen stark erhöht.

Auf technischer Ebene stellte Ousterhout Skriptsprachen und Systeme entlang der Achsen der Typsicherheit und der Anweisungen pro Anweisung gegenüber, wie oben gezeigt. Auf der Entwurfsebene charakterisierte er die neuen Rollen für jede Sprachklasse: Systemprogrammierung dient der Erstellung von Komponenten, Skripting dem Zusammenkleben.

Etwa zu dieser Zeit begannen auch statisch typisierte, aber garbage collected Sprachen an Popularität zu gewinnen. Java (1995) und C# (2000) entwickelten sich zu den Titanen, die wir heute kennen. Obwohl diese beiden Sprachen traditionell nicht als „Systemprogrammiersprachen“ gelten, wurden sie für die Entwicklung vieler der größten Softwaresysteme der Welt verwendet. Ousterhout erwähnte sogar ausdrücklich, dass „in der Internetwelt, die jetzt Gestalt annimmt, Java für die Systemprogrammierung verwendet wird.“

2010er Jahre: Grenzen verschwimmen

Im letzten Jahrzehnt hat die Grenze zwischen Skriptsprachen und Systemprogrammiersprachen zu verschwimmen begonnen. Unternehmen wie Dropbox waren in der Lage, erstaunlich große und skalierbare Systeme nur mit Python aufzubauen. Javascript wird zum Rendern komplexer Benutzeroberflächen in Echtzeit auf Milliarden von Webseiten verwendet. Die schrittweise Typisierung hat in Python, Javascript und anderen Skriptsprachen an Bedeutung gewonnen und ermöglicht den Übergang von „Prototyp“-Code zu „Produktions“-Code durch schrittweises Hinzufügen statischer Typinformationen.

Gleichzeitig wurden enorme technische Ressourcen in JIT-Compiler sowohl für statische Sprachen (z. B. HotSpot von Java) als auch für dynamische Sprachen (z. B. LuaJIT von Lua, V8 von Javascript, PyPy von Python) gesteckt, so dass deren Leistung mit der traditioneller Systemprogrammiersprachen (C, C++) konkurrieren kann. Groß angelegte verteilte Systeme wie Spark sind in Scala2 geschrieben. Neue Programmiersprachen wie Julia, Swift und Go verschieben weiterhin die Leistungsgrenzen von Garbage-Collected-Sprachen.

Auf einer Podiumsdiskussion mit dem Titel „Systems Programming in 2014 and Beyond“ (Systemprogrammierung im Jahr 2014 und darüber hinaus) traten die größten Köpfe hinter den heutigen selbsternannten Systemsprachen auf: Bjarne Stroustrup (Erfinder von C++), Rob Pike (Erfinder von Go), Andrei Alexandrescu (D-Entwickler) und Niko Matsakis (Rust-Entwickler). Auf die Frage „Was ist eine Systemprogrammiersprache im Jahr 2014?“, antworteten sie (bearbeitete Transkription):

  • Niko Matsakis: Das Schreiben von clientseitigen Anwendungen. Das polare Gegenteil von dem, wofür Go konzipiert ist. Bei diesen Anwendungen gibt es hohe Latenzanforderungen, hohe Sicherheitsanforderungen, eine Menge Anforderungen, die auf der Serverseite nicht auftauchen.
  • Bjarne Stroustrup: Die Systemprogrammierung kam aus dem Bereich, in dem man sich mit der Hardware befassen musste, und dann wurden die Anwendungen komplizierter. Man muss mit der Komplexität umgehen können. Wenn man Probleme mit erheblichen Ressourcenbeschränkungen hat, ist man im Bereich der Systemprogrammierung. Wenn Sie eine feinkörnigere Steuerung benötigen, sind Sie ebenfalls im Bereich der Systemprogrammierung angesiedelt. Es sind die Einschränkungen, die bestimmen, ob es sich um Systemprogrammierung handelt. Geht Ihnen der Speicher aus? Geht Ihnen die Zeit aus?
  • Rob Pike: Als wir Go zum ersten Mal ankündigten, nannten wir es eine Systemprogrammiersprache, und ich bedaure das ein wenig, weil viele Leute annahmen, es sei eine Programmiersprache für Betriebssysteme. Wir hätten es eine Sprache zum Schreiben von Servern nennen sollen, denn das ist es, was wir wirklich im Sinn hatten. Jetzt weiß ich, dass es sich um eine Sprache für die Cloud-Infrastruktur handelt. Eine andere Definition von Systemprogrammierung ist das, was in der Cloud läuft.
  • Andrei Alexandrescu: Ich habe ein paar Lackmustests, um zu prüfen, ob etwas eine Systemprogrammiersprache ist. Eine Systemprogrammiersprache muss die Möglichkeit bieten, einen eigenen Speicherallokator zu schreiben. Man sollte in der Lage sein, eine Zahl in einen Zeiger zu fälschen, denn so funktioniert die Hardware.

Geht es bei der Systemprogrammierung also um hohe Leistung? Ressourcenbeschränkungen? Hardware-Kontrolle? Cloud-Infrastruktur? Im Großen und Ganzen scheint es so zu sein, dass sich Sprachen der Kategorie C, C++, Rust und D durch ihre Abstraktionsebene von der Maschine unterscheiden. Diese Sprachen legen Details der zugrundeliegenden Hardware offen, wie z.B. Speicherzuweisung/Layout und feinkörniges Ressourcenmanagement.

Eine andere Möglichkeit, darüber nachzudenken: Wenn Sie ein Effizienzproblem haben, wie viel Freiheit haben Sie, um es zu lösen? Das Wunderbare an Low-Level-Programmiersprachen ist, dass es in Ihrer Macht steht, den Engpass durch sorgfältige Kontrolle der Maschinendetails zu beseitigen, wenn Sie eine Ineffizienz erkennen. Vektorisieren Sie diese Anweisung, ändern Sie die Größe dieser Datenstruktur, um sie im Cache zu halten, und so weiter. Genauso wie statische Typen die Gewissheit3 geben, dass die beiden Dinge, die ich zu addieren versuche, auf jeden Fall ganze Zahlen sind, geben Low-Level-Sprachen die Gewissheit, dass dieser Code auf der Maschine so ausgeführt wird, wie ich es spezifiziert habe.“

Im Gegensatz dazu ist die Optimierung von interpretierten Sprachen ein absoluter Dschungel. Es ist unglaublich schwer zu wissen, ob die Laufzeitumgebung Ihren Code konsistent so ausführen wird, wie Sie es erwarten. Das ist genau das gleiche Problem wie bei der automatischen Parallelisierung von Compilern – „Auto-Vektorisierung ist kein Programmiermodell“ (siehe Die Geschichte von ispc). Es ist, als würde man eine Schnittstelle in Python schreiben und denken: „Nun, ich hoffe natürlich, dass derjenige, der diese Funktion aufruft, mir einen int gibt.“

Heute: …was ist denn nun Systemprogrammierung?

Damit bin ich wieder bei meinem ursprünglichen Anliegen angelangt. Was viele Leute als Systemprogrammierung bezeichnen, sehe ich als Low-Level-Programmierung an, die Details der Maschine aufdeckt. Aber was ist dann mit Systemen? Erinnern wir uns an unsere Definition von 1972:

  1. Das zu lösende Problem ist breit gefächert und besteht aus vielen, meist recht unterschiedlichen Teilproblemen.
  2. Das Systemprogramm wird wahrscheinlich zur Unterstützung anderer Software- und Anwendungsprogramme eingesetzt, kann aber auch selbst ein komplettes Anwendungspaket sein.
  3. Es ist eher für den kontinuierlichen „Produktionseinsatz“ als für die einmalige Lösung eines einzelnen Anwendungsproblems konzipiert.
  4. Es wird wahrscheinlich ständig weiterentwickelt, was die Anzahl und die Art der unterstützten Funktionen angeht.
  5. Ein Systemprogramm erfordert eine bestimmte Disziplin oder Struktur, sowohl innerhalb als auch zwischen den Modulen (d.h. „Kommunikation“), und wird normalerweise von mehr als einer Person entworfen und implementiert.

Diese Punkte scheinen eher Software-Engineering-Probleme zu sein (Modularität, Wiederverwendung, Code-Evolution) als Low-Level-Leistungsprobleme. Das bedeutet, dass jede Programmiersprache, die sich vorrangig mit diesen Problemen befasst, eine Systemprogrammiersprache ist! Das heißt aber noch nicht, dass jede Sprache eine Systemprogrammiersprache ist. Dynamische Programmiersprachen sind wohl noch weit von Systemsprachen entfernt, da dynamische Typen und Redewendungen wie „Bitte um Verzeihung, nicht um Erlaubnis“ einer guten Codequalität nicht förderlich sind.

Was bringt uns diese Definition dann? Hier ist ein heißer Tipp: Funktionale Sprachen wie OCaml und Haskell sind viel systemorientierter als Low-Level-Sprachen wie C oder C++. Wenn wir Studenten die Systemprogrammierung lehren, sollten wir die Prinzipien der funktionalen Programmierung einbeziehen, wie z.B. den Wert der Unveränderlichkeit, die Auswirkung umfangreicher Typensysteme zur Verbesserung der Schnittstellengestaltung und den Nutzen von Funktionen höherer Ordnung. Schulen sollten sowohl Systemprogrammierung als auch Low-Level-Programmierung lehren.

Gibt es, wie befürchtet, einen Unterschied zwischen Systemprogrammierung und guter Softwareentwicklung? Nicht wirklich, aber ein Problem dabei ist, dass Software-Engineering und Low-Level-Programmierung oft isoliert gelehrt werden. Während die meisten Software-Engineering-Kurse in der Regel Java-zentriert sind – „schreibe gute Schnittstellen und Tests“ -, sollten wir den Studenten auch beibringen, wie man Systeme entwirft, die erheblichen Ressourcenbeschränkungen unterliegen. Vielleicht nennen wir Low-Level-Programmierung „Systeme“, weil viele der interessantesten Softwaresysteme Low-Level-Systeme sind (z. B. Datenbanken, Netzwerke, Betriebssysteme usw.). Da Low-Level-Systeme viele Einschränkungen haben, erfordern sie von ihren Entwicklern kreatives Denken.

Eine andere Sichtweise ist, dass Low-Level-Programmierer versuchen sollten zu verstehen, welche Ideen im Systemdesign angepasst werden könnten, um mit der Realität moderner Hardware umzugehen. Ich denke, dass die Rust-Gemeinschaft in dieser Hinsicht äußerst innovativ war, indem sie untersucht hat, wie gute Software-Design-/Funktionsprogrammier-Prinzipien auf Low-Level-Probleme angewendet werden können (z.B. Futures, Fehlerbehandlung oder natürlich Speichersicherheit).

Zusammenfassend sollte das, was wir „Systemprogrammierung“ nennen, meiner Meinung nach „Low-Level-Programmierung“ genannt werden. Das Gebiet des Computersystemdesigns ist zu wichtig, um nicht seinen eigenen Namen zu haben. Die klare Trennung dieser beiden Ideen schafft eine größere konzeptionelle Klarheit im Bereich des Programmiersprachenentwurfs und öffnet auch die Tür für den Austausch von Erkenntnissen zwischen den beiden Bereichen: Wie können wir das System um die Maschine herum entwerfen und umgekehrt?

Bitte senden Sie Kommentare an meinen Posteingang unter [email protected] oder Hacker News.

  1. Cooler Fakt: Zwei der Autoren dieses Papiers, R. Bergeron und Andy Van Dam, sind Gründungsmitglieder der Grafikgemeinschaft und der SIGGRAPH-Konferenz. Dies ist Teil eines fortlaufenden Musters, bei dem Grafikforscher den Trend im Systemdesign setzen, z.B. der Ursprung von GPGPU.

  2. Pflichtverweis auf Skalierbarkeit!

  3. Eigentlich sind statische Typen eine 100%ige Garantie (oder Sie bekommen Ihr Geld zurück), aber in der Praxis erlauben die meisten Sprachen ein gewisses Maß an Obj.magic.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.