&Notepad

Will Crichton – September 9, 2018
Ik heb een grief met de uitdrukking “systeemprogrammering.” Voor mij leek het altijd onnodig twee ideeën te combineren: low-level programmeren (omgaan met implementatiedetails van de machine) en systeemontwerp (het creëren en beheren van een complexe set van interoperabele componenten). Waarom is dat zo? Hoe lang is dit al zo? En wat zouden we kunnen winnen bij een herdefiniëring van het idee van systemen?

Jaren ’70: Verbetering van de assemblage

Laten we eens teruggaan naar de oorsprong van moderne computersystemen om te begrijpen hoe de term is geëvolueerd. Ik weet niet wie de term oorspronkelijk bedacht heeft, maar mijn opzoekingen suggereren dat serieuze pogingen om “computersystemen” te definiëren begonnen rond het begin van de jaren 70. In Systems Programming Languages (Bergeron1 et al. 1972), zeggen de auteurs:

Een systeemprogramma is een geïntegreerde verzameling subprogramma’s, die samen een geheel vormen dat groter is dan de som van de delen, en dat een bepaalde drempel van grootte en/of complexiteit overschrijdt. Typische voorbeelden zijn systemen voor multiprogrammering, vertaling, simulatie, informatiebeheer en tijddeling. Hieronder volgt een gedeeltelijke reeks eigenschappen, waarvan sommige ook in niet-systemen worden aangetroffen, en die niet alle in een bepaald systeem aanwezig behoeven te zijn.

  1. Het op te lossen probleem is van brede aard en bestaat uit vele, en gewoonlijk zeer gevarieerde, deelproblemen.
  2. Het systeemprogramma zal waarschijnlijk worden gebruikt ter ondersteuning van andere software en toepassingsprogramma’s, maar kan ook zelf een volledig toepassingspakket zijn.
  3. Het is ontworpen voor voortdurend “produktie”-gebruik en niet zozeer voor een eenmalige oplossing voor een enkel toepassingsprobleem.
  4. Het is waarschijnlijk voortdurend in ontwikkeling wat betreft het aantal en de soorten functies die het ondersteunt.
  5. Een systeemprogramma vereist een bepaalde discipline of structuur, zowel binnen als tussen de modules (d.w.z. ,,communicatie”), en wordt gewoonlijk door meer dan één persoon ontworpen en uitgevoerd.

Deze definitie is redelijk acceptabel-computersystemen zijn grootschalig, langdurig in gebruik, en tijd-variabel. Maar terwijl deze definitie grotendeels beschrijvend is, is een belangrijk idee in het artikel voorschrijvend: het pleiten voor de scheiding van talen op laag niveau en systeemtalen (in die tijd, het contrasteren van assemblage met FORTRAN).

Het doel van een systeemprogrammeertaal is een taal te bieden die kan worden gebruikt zonder onnodige zorg voor “bit twiddling” overwegingen, maar toch code genereert die niet merkbaar slechter is dan die welke met de hand wordt gegenereerd. Zo’n taal moet de beknoptheid en leesbaarheid van hoog-niveau-talen combineren met de ruimte- en tijdsefficiëntie en de mogelijkheid om machine- en besturingssysteemfaciliteiten te “benaderen” die in assembler-taal kunnen worden verkregen. Ontwerp-, schrijf- en debugging-tijd moet worden geminimaliseerd zonder onnodige overhead op systeembronnen.

Tegzelfdertijd publiceerden onderzoekers van CMU BLISS: A Language for Systems Programming (Wulf et al. 1972), met de volgende beschrijving:

Wij noemen BLISS een “implementatietaal”, hoewel we toegeven dat de term enigszins ambigu is, omdat vermoedelijk alle computertalen worden gebruikt om iets te implementeren. Voor ons betekent de uitdrukking een taal voor algemene doeleinden, op hoger niveau, waarin de primaire nadruk is gelegd op een specifieke toepassing, namelijk het schrijven van grote, produktie-softwaresystemen voor een specifieke machine. Talen voor speciale doeleinden, zoals compiler-compilers, vallen niet in deze categorie, en we gaan er ook niet noodzakelijk van uit dat deze talen machine-onafhankelijk hoeven te zijn. Wij benadrukken het woord “implementatie” in onze definitie en hebben woorden als “ontwerp” en “documentatie” niet gebruikt. Wij verwachten niet noodzakelijkerwijs dat een implementatietaal een geschikt middel zal zijn om het aanvankelijke ontwerp van een groot systeem uit te drukken, noch voor de exclusieve documentatie van dat systeem. Concepten als machine-onafhankelijkheid, het uitdrukken van het ontwerp en de implementatie in dezelfde notatie, zelf-documentatie, en andere, zijn duidelijk wenselijke doelen en zijn criteria aan de hand waarvan we verschillende talen hebben geëvalueerd.

Hier stellen de auteurs het idee tegenover van een “implementatietaal” als zijnde van een hoger niveau dan assemblage, maar van een lager niveau dan een “ontwerptaal”. Dit staat haaks op de definitie in het voorgaande paper, waarin wordt bepleit dat het ontwerpen van een systeem en het implementeren van een systeem gescheiden talen zouden moeten hebben.

Beide van deze papers zijn onderzoekshandelingen of pleidooien. Het laatste artikel (ook uit 1972, een productief jaar!) is Systems Programming (Donovan 1972), een educatieve tekst voor het leren van systeemprogrammeren.

Wat is systeemprogrammeren? Je kunt je een computer voorstellen als een soort beest dat alle commando’s gehoorzaamt. Er wordt wel gezegd dat computers in feite mensen van metaal zijn of, omgekeerd, dat mensen computers van vlees en bloed zijn. Zodra we echter computers van dichtbij bekijken, zien we dat het in feite machines zijn die zeer specifieke en primitieve instructies volgen. In de begindagen van de computers communiceerden de mensen met hen door aan- en uitschakelaars die primitieve instructies aanduidden. Al snel wilden de mensen meer complexe instructies geven. Zij wilden bijvoorbeeld kunnen zeggen X = 30 * Y; gegeven dat Y = 10, wat is X? De huidige computers kunnen een dergelijke taal niet begrijpen zonder de hulp van systeemprogramma’s. Systeemprogramma’s (b.v. compilers, loaders, macroprocessors, besturingssystemen) werden ontwikkeld om computers beter af te stemmen op de behoeften van hun gebruikers. Verder wilden mensen meer hulp bij de mechanica van het voorbereiden van hun programma’s.

Ik vind het leuk dat deze definitie ons eraan herinnert dat systemen in dienst staan van mensen, ook al is het alleen maar infrastructuur die niet direct aan de eindgebruiker wordt blootgesteld.

Jaren ’90: De opkomst van scripting

In de jaren ’70 en ’80 leken de meeste onderzoekers systeemprogrammering meestal te zien als een tegenstelling tot assemblageprogrammering. Er waren gewoon geen andere goede gereedschappen om systemen te bouwen. (Ik weet niet zeker waar Lisp in dit alles stond? Geen van de bronnen die ik heb gelezen noemde Lisp, hoewel ik me vaag bewust ben dat Lisp machines bestonden, hoe kort ook.)

Midden jaren ’90 vond er echter een grote omslag plaats in programmeertalen met de opkomst van dynamisch getypeerde scripttalen. Verbeterend op shell scripting systemen als Bash, vonden talen als Perl (1987), Tcl (1988), Python (1990), Ruby (1995), PHP (1995), en Javascript (1995) hun weg naar de mainstream. Dit culmineerde in het invloedrijke artikel “Scripting: Hoger Programmeren voor de 21ste Eeuw” (Ousterhout 1998). Hierin wordt “Ousterhout’s dichotomie” tussen “systeem programmeertalen” en “scripting talen” verwoord.”

Scripting talen zijn ontworpen voor andere taken dan systeem programmeertalen, en dit leidt tot fundamentele verschillen in de talen. Systeemprogrammeertalen zijn ontworpen om gegevensstructuren en algoritmen vanaf nul op te bouwen, uitgaande van de meest primitieve computerelementen zoals woorden geheugen. Scripttalen daarentegen zijn ontworpen om te lijmen: zij gaan uit van het bestaan van een reeks krachtige componenten en zijn in de eerste plaats bedoeld om componenten met elkaar te verbinden. Systeemprogrammeertalen zijn sterk getypeerd om de complexiteit te helpen beheersen, terwijl scripttalen typeloos zijn om de verbindingen tussen componenten te vereenvoudigen en een snelle ontwikkeling van toepassingen mogelijk te maken. Verscheidene recente trends, zoals snellere machines, betere scripttalen, het toenemende belang van grafische gebruikersinterfaces en componentarchitecturen, en de groei van het Internet, hebben de toepasbaarheid van scripttalen sterk vergroot.

Op technisch niveau stelde Ousterhout scripting tegenover systemen langs de assen type-veiligheid en instructies-per-statement, zoals hierboven getoond. Op ontwerpniveau karakteriseerde hij de nieuwe rollen voor elke taalklasse: systeemprogrammeren is voor het maken van componenten, en scripting is voor het aan elkaar lijmen daarvan.

Omstreeks deze tijd begonnen statisch getypeerde maar garbage collected talen ook aan populariteit te winnen. Java (1995) en C# (2000) werden de titanen die we vandaag kennen. Hoewel deze twee talen traditioneel niet als “systeemprogrammeertalen” worden beschouwd, zijn ze gebruikt om veel van ’s werelds grootste softwaresystemen te ontwerpen. Ousterhout vermeldde zelfs expliciet “in de internetwereld die nu vorm krijgt, wordt Java gebruikt voor systeemprogrammering.”

2010s: Grenzen vervagen

In het afgelopen decennium is de grens tussen scripttalen en systeemprogrammeertalen gaan vervagen. Bedrijven als Dropbox waren in staat om verrassend grote en schaalbare systemen te bouwen op alleen Python. Javascript wordt gebruikt om real-time, complexe UI’s in miljarden webpagina’s te renderen. Geleidelijke typering heeft aan kracht gewonnen in Python, Javascript en andere scripttalen, waardoor een overgang van “prototype” code naar “productie” code mogelijk wordt door incrementeel statische type-informatie toe te voegen.

Tegelijkertijd hebben enorme technische middelen gestoken in JIT-compilers voor zowel statische talen (bijv. Java’s HotSpot) als dynamische talen (bijv. Lua’s LuaJIT, Javascript’s V8, Python’s PyPy) hun prestaties concurrerend gemaakt met traditionele systeemprogrammeertalen (C, C++). Grootschalige gedistribueerde systemen zoals Spark zijn geschreven in Scala2. Nieuwe programmeertalen als Julia, Swift en Go blijven de prestatiegrenzen van garbage-collected talen verleggen.

In een panel met de titel Systems Programming in 2014 and Beyond kwamen de grootste geesten achter de huidige zelfbenoemde systeemtalen aan het woord: Bjarne Stroustrup (bedenker van C++), Rob Pike (bedenker van Go), Andrei Alexandrescu (D-ontwikkelaar), en Niko Matsakis (Rust-ontwikkelaar). Op de vraag “wat is een systeemprogrammeertaal anno 2014?” zeiden ze (bewerkte transcriptie):

  • Niko Matsakis: Het schrijven van client-side applicaties. Het tegengestelde van waar Go voor ontworpen is. In deze toepassingen heb je hoge latency behoeften, hoge beveiligingseisen, een heleboel eisen die niet aan de serverkant naar voren komen.
  • Bjarne Stroustrup: Systeemprogrammering kwam voort uit het veld waar je te maken had met hardware, en toen werden de toepassingen ingewikkelder. Je moet omgaan met complexiteit. Als je te maken hebt met aanzienlijke beperkingen van middelen, dan zit je in het domein van systeemprogrammering. Als je fijnkorreligere controle nodig hebt, dan zit je ook in het domein van systeemprogrammering. Het zijn de beperkingen die bepalen of het een systeemprogrammering is. Heb je bijna geen geheugen meer? Heb je te weinig tijd?
  • Rob Pike: Toen we Go voor het eerst aankondigden, noemden we het een systeem programmeertaal, en ik heb daar een beetje spijt van, omdat veel mensen aannamen dat het een operating systems schrijftaal was. Wat we het hadden moeten noemen is een server schrijftaal, dat is wat we er echt van dachten. Nu begrijp ik dat wat we hebben een cloud infrastructuur taal is. Een andere definitie van systeemprogrammering is het spul dat draait in de cloud.
  • Andrei Alexandrescu: Ik heb een paar lakmoes-tests om te controleren of iets een systeemprogrammeertaal is. Een systeemprogrammeertaal moet je in staat stellen er je eigen geheugentoewijzingsprogramma in te schrijven. Je moet in staat zijn om een getal in een pointer te vervalsen, want dat is hoe hardware werkt.

Gaat systeemprogrammeren dan over hoge prestaties? Beperkingen van de middelen? Hardware controle? Cloud-infrastructuur? Het lijkt erop, in grote lijnen, dat talen in de categorie van C, C++, Rust, en D worden onderscheiden in termen van hun niveau van abstractie van de machine. Deze talen leggen details bloot van de onderliggende hardware, zoals geheugentoewijzing/layout en fijnkorrelig resource management.

Een andere manier om erover na te denken: als je een efficiency-probleem hebt, hoeveel vrijheid heb je dan om het op te lossen? Het mooie van low-level programmeertalen is dat wanneer je een inefficiëntie identificeert, het binnen je macht ligt om het knelpunt te elimineren door zorgvuldige controle over machinedetails. Vectorize deze instructie, resize die data structuur om het in cache te houden, enzovoort. Op dezelfde manier als statische types meer vertrouwen3 geven, zoals “deze twee dingen die ik probeer toe te voegen zijn zeker gehele getallen,” geven low-level talen meer vertrouwen dat “deze code zal worden uitgevoerd op de machine zoals ik heb gespecificeerd.”

Het optimaliseren van geïnterpreteerde talen is daarentegen een absolute jungle. Het is ongelooflijk moeilijk om te weten of de runtime je code consequent zal uitvoeren op de manier die je verwacht. Dit is precies hetzelfde probleem met auto-parallelizing compilers-“auto-vectorization is not a programming model” (zie The story of ispc). Het is alsof je een interface in Python schrijft en denkt: “Ik hoop maar dat degene die deze functie aanroept me een int geeft.”

Dag: …dus wat is systeemprogrammeren?

Dit brengt me terug bij mijn oorspronkelijke grief. Wat veel mensen systeemprogrammering noemen, beschouw ik als low-level programmeren, waarbij details van de machine worden blootgelegd. Maar hoe zit het dan met systemen? Denk aan onze definitie uit 1972:

  1. Het op te lossen probleem is van brede aard en bestaat uit vele, en meestal zeer gevarieerde, sub-problemen.
  2. Het systeemprogramma zal waarschijnlijk worden gebruikt ter ondersteuning van andere software en toepassingsprogramma’s, maar kan ook zelf een compleet toepassingspakket zijn.
  3. Het is ontworpen voor voortdurend “produktie”-gebruik in plaats van een eenmalige oplossing voor een enkel toepassingsprobleem.
  4. Het is waarschijnlijk voortdurend in ontwikkeling wat betreft het aantal en de soorten functies die het ondersteunt.
  5. Een systeemprogramma vereist een bepaalde discipline of structuur, zowel binnen als tussen modules (d.w.z. , “communicatie”), en wordt gewoonlijk door meer dan één persoon ontworpen en uitgevoerd.

Deze lijken veel meer op software engineering-kwesties (modulariteit, hergebruik, code-evolutie) dan op low-level performance-kwesties. Dat betekent dat elke programmeertaal die prioriteit geeft aan het aanpakken van deze problemen een systeemprogrammeertaal is! Dat betekent nog steeds niet dat elke taal een systeemprogrammeertaal is. Dynamische programmeertalen zijn aantoonbaar nog steeds verre van systeemtalen, omdat dynamische types en idiomen als “vraag vergeving, geen toestemming” niet bevorderlijk zijn voor een goede kwaliteit van de code.

Wat levert deze definitie ons dan op? Hier is een snelle conclusie: functionele talen zoals OCaml en Haskell zijn veel meer systeemgeoriënteerd dan talen op laag niveau zoals C of C++. Wanneer we systeemprogrammeren onderwijzen aan studenten, zouden we functionele programmeerprincipes moeten opnemen, zoals de waarde van onveranderlijkheid, de impact van rijke typesystemen op het verbeteren van interface-ontwerp, en het nut van hogere-orde functies. Scholen zouden zowel systeemprogrammering als programmeren op laag niveau moeten onderwijzen.

Zoals bepleit, is er een onderscheid tussen systeemprogrammering en goede software engineering? Niet echt, maar een probleem hier is dat software engineering en programmeren op laag niveau vaak los van elkaar worden onderwezen. Terwijl de meeste software engineering lessen meestal Java-centrisch zijn “schrijf goede interfaces en tests”, zouden we studenten ook moeten onderwijzen over hoe systemen te ontwerpen die aanzienlijke beperkingen hebben wat betreft middelen. Misschien noemen we low-level programmeren “systemen” omdat veel van de meest interessante software systemen low-level zijn (b.v. databases, netwerken, besturingssystemen, etc.). Omdat low-level systemen veel beperkingen hebben, moeten de ontwerpers ervan creatief denken.

Een ander frame is dat low-level programmeurs zouden moeten proberen te begrijpen welke ideeën in systeemontwerp kunnen worden aangepast om om te gaan met de realiteit van moderne hardware. Ik denk dat de Rust-gemeenschap in dit opzicht buitengewoon innovatief is geweest, door te kijken hoe goede softwareontwerpen/functionele programmeerprincipes kunnen worden toegepast op problemen op laag niveau (bijv. futures, foutafhandeling, of natuurlijk geheugenveiligheid).

Om samen te vatten: wat wij “systeemprogrammering” noemen, zou volgens mij “programmeren op laag niveau” moeten heten. Het ontwerpen van computersystemen als vakgebied is te belangrijk om geen eigen naam te hebben. Het duidelijk scheiden van deze twee ideeën geeft een grotere conceptuele helderheid over de ruimte van programmeertaalontwerp, en het opent ook de deur naar het delen van inzichten over de twee ruimtes: hoe kunnen we het systeem rond de machine ontwerpen, en vice versa?

Gelieve opmerkingen te sturen naar mijn inbox op [email protected] of Hacker News.

  1. Cool feit hier: twee van de auteurs van dit artikel, R. Bergeron en Andy Van Dam, zijn stichtende leden van de grafische gemeenschap en de SIGGRAPH conferentie. Onderdeel van een doorlopend patroon waarbij grafische onderzoekers de trend zetten in systeemontwerp, cf. de oorsprong van GPGPU.

  2. Verplichte link naar Schaalbaarheid! Maar tegen welke prijs?

  3. Eindelijk zijn statische typen 100% garantie (of je geld terug), maar in de praktijk staan de meeste talen een zekere mate van Obj.magic toe.

Geef een antwoord

Het e-mailadres wordt niet gepubliceerd.