&Notepad

Will Crichton – September 9, 2018
Jag har ett problem med uttrycket ”systemprogrammering”. För mig har det alltid verkat som om det onödigtvis kombinerar två idéer: programmering på låg nivå (hantera implementeringsdetaljer i maskinen) och systemdesign (skapa och hantera en komplex uppsättning samverkande komponenter). Varför är det så? Hur länge har detta varit sant? Och vad kan vi vinna på att omdefiniera idén om system?

1970-talet: Förbättring av sammansättning

Vi kan resa tillbaka till de moderna datorsystemens ursprung för att förstå hur begreppet har utvecklats. Jag vet inte vem som ursprungligen myntade uttrycket, men mina sökningar tyder på att seriösa ansträngningar för att definiera ”datorsystem” inleddes i början av 70-talet. I Systems Programming Languages (Bergeron1 et al. 1972) säger författarna:

Ett systemprogram är en integrerad uppsättning av underprogram som tillsammans bildar en helhet som är större än summan av sina delar och som överskrider ett visst tröskelvärde för storlek och/eller komplexitet. Typiska exempel är system för multiprogrammering, översättning, simulering, informationshantering och tidsdelning. Följande är en partiell uppsättning egenskaper, av vilka vissa återfinns i andra system än system och som inte alla behöver finnas i ett visst system.

  1. Problemet som skall lösas är av bred karaktär och består av många, och vanligen ganska varierande, delproblem.
  2. Systemprogrammet kommer sannolikt att användas som stöd för andra program- och tillämpningsprogram, men kan också vara ett komplett tillämpningspaket i sig självt.
  3. Det är utformat för kontinuerlig ”produktionsanvändning” snarare än som en engångslösning på ett enskilt tillämpningsproblem.
  4. Det kommer sannolikt att utvecklas kontinuerligt när det gäller antalet och typerna av funktioner som det stöder.
  5. Ett systemprogram kräver en viss disciplin eller struktur, både inom och mellan moduler (dvs. ”kommunikation”), och utformas och implementeras vanligen av mer än en person.

Denna definition är ganska samstämmig – datorsystem är storskaliga, långvarigt använda och tidsvarierande. Även om denna definition till stor del är beskrivande, är en viktig idé i dokumentet dock normativ: man förespråkar en åtskillnad mellan lågnivåspråk och systemspråk (på den tiden kontrasterade man assembler och FORTRAN).

Målet med ett systemprogrammeringsspråk är att tillhandahålla ett språk som kan användas utan att man behöver bry sig om ”bit twiddling”-överväganden, men som ändå kommer att generera en kod som inte är märkbart sämre än den som genereras för hand. Ett sådant språk bör kombinera högnivåspråkens koncisitet och läsbarhet med den effektivitet i fråga om utrymme och tid och förmågan att ”komma åt” maskinens och operativsystemets faciliteter som kan erhållas med hjälp av assemblerspråk. Tidsåtgången för design, skrivning och felsökning bör minimeras utan att onödig belastning läggs på systemresurser.

Till samma tid publicerade forskare från CMU BLISS: A Language for Systems Programming (Wulf et al. 1972) och beskrev det på följande sätt:

Vi hänvisar till BLISS som ett ”implementeringsspråk”, även om vi medger att termen är något tvetydig, eftersom alla datorspråk förmodligen används för att implementera något. För oss innebär uttrycket ett allmänt ändamål, ett språk på högre nivå där den primära tonvikten har lagts på en specifik tillämpning, nämligen skrivandet av stora produktionsprogramvarusystem för en specifik maskin. Språk för särskilda ändamål, t.ex. kompilatorer, hör inte till denna kategori, och vi antar inte heller nödvändigtvis att dessa språk behöver vara maskinoberoende. Vi betonar ordet ”implementation” i vår definition och har inte använt ord som ”design” och ”dokumentation”. Vi förväntar oss inte nödvändigtvis att ett implementeringsspråk kommer att vara ett lämpligt verktyg för att uttrycka den ursprungliga utformningen av ett stort system eller för den exklusiva dokumentationen av detta system. Begrepp som maskinoberoende, att uttrycka design och implementering i samma notation, självdokumentation och andra är klart önskvärda mål och är kriterier som vi utvärderat olika språk utifrån.

Här kontrasterar författarna idén om ett ”implementeringsspråk” som högre nivå än assembler, men lägre nivå än ett ”designspråk”. Detta motsätter sig definitionen i det föregående dokumentet, där man förespråkar att utformning av ett system och implementering av ett system bör ha separata språk.

Båda dessa dokument är forskningsartefakter eller förespråkanden. Den sista posten att beakta (också från 1972, ett produktivt år!) är Systems Programming (Donovan 1972), en pedagogisk text för att lära sig systemprogrammering.

Vad är systemprogrammering? Du kanske föreställer dig en dator som ett slags odjur som lyder alla kommandon. Det har sagts att datorer i grund och botten är människor gjorda av metall eller omvänt att människor är datorer gjorda av kött och blod. Men när vi kommer nära datorerna ser vi att de i grunden är maskiner som följer mycket specifika och primitiva instruktioner. I datorernas tidiga dagar kommunicerade människor med dem genom att använda på- och avknappar som betecknade primitiva instruktioner. Snart ville man ge mer komplexa instruktioner. Man ville till exempel kunna säga X = 30 * Y. Om Y = 10, vad är då X? Dagens datorer kan inte förstå ett sådant språk utan hjälp av systemprogram. Systemprogram (t.ex. kompilatorer, inläsare, makroprocessorer, operativsystem) utvecklades för att göra datorerna bättre anpassade till användarnas behov. Vidare ville människor ha mer hjälp med mekaniken för att förbereda sina program.

Jag gillar att den här definitionen påminner oss om att systemen är till för människor, även om de bara är infrastruktur som inte är direkt utsatt för slutanvändaren.

1990-talet: På 70- och 80-talen verkar det som om de flesta forskare såg systemprogrammering oftast som en kontrast till assemblerprogrammering. Det fanns helt enkelt inga andra bra verktyg för att bygga system. (Jag är inte säker på var Lisp befann sig i allt detta? Ingen av de resurser jag läste citerade Lisp, även om jag är vagt medveten om att Lisp-maskiner existerade, om än kortvarigt.)

I mitten av 90-talet skedde emellertid en stor förändring inom programmeringsspråken i och med framväxten av dynamiskt typade skriptspråk. Språk som Perl (1987), Tcl (1988), Python (1990), Ruby (1995), PHP (1995) och Javascript (1995) förbättrade tidigare skalskriptsystem som Bash och arbetade sig in i huvudfåran. Detta kulminerade i den inflytelserika artikeln ”Scripting: Higher Level Programming for the 21st Century” (Ousterhout 1998). I denna artikulerades ”Ousterhouts dikotomi” mellan ”systemprogrammeringsspråk” och ”skriptspråk”

Skriptspråk är utformade för andra uppgifter än systemprogrammeringsspråk, och detta leder till grundläggande skillnader mellan språken. Systemprogrammeringsspråk har utformats för att bygga datastrukturer och algoritmer från grunden, med utgångspunkt i de mest primitiva datorelementen, t.ex. minnesord. Däremot är skriptspråk utformade för att limma ihop: de förutsätter att det finns en uppsättning kraftfulla komponenter och är i första hand avsedda för att koppla ihop komponenterna. Systemprogrammeringsspråk är starkt typade för att hjälpa till att hantera komplexiteten, medan skriptspråk är typfria för att förenkla kopplingar mellan komponenter och möjliggöra snabb utveckling av tillämpningar. Flera trender på senare tid, t.ex. snabbare maskiner, bättre skriptspråk, den ökande betydelsen av grafiska användargränssnitt och komponentarkitekturer samt Internets tillväxt, har kraftigt ökat skriptspråkens användbarhet.

På teknisk nivå kontrasterade Ousterhout skriptspråk kontra system längs axlarna typsäkerhet och instruktioner per angiven uppgift, enligt vad som visas ovan. På designnivå karakteriserade han de nya rollerna för varje språkklass: systemprogrammering är till för att skapa komponenter och scripting är till för att limma ihop dem.

Omkring den här tiden började också statiskt typade men skräpinsamlade språk att vinna i popularitet. Java (1995) och C# (2000) förvandlades till de titaner vi känner till idag. Även om dessa två inte traditionellt sett betraktas som ”systemprogrammeringsspråk” har de använts för att utforma många av världens största programvarusystem. Ousterhout nämnde till och med uttryckligen ”i den Internetvärld som håller på att ta form nu används Java för systemprogrammering.”

2010: Under det senaste decenniet har gränsen mellan skriptspråk och systemprogrammeringsspråk börjat suddas ut. Företag som Dropbox kunde bygga förvånansvärt stora och skalbara system på bara Python. Javascript används för att återge komplexa användargränssnitt i realtid på miljarder webbsidor. Graduell typning har fått stor betydelse i Python, Javascript och andra skriptspråk, vilket gör det möjligt att övergå från prototypkod till produktionskod genom att stegvis lägga till statisk typinformation.

Till samma tid har massiva tekniska resurser lagts på JIT-kompilatorer för både statiska språk (t.ex. Javas HotSpot) och dynamiska språk (t.ex. Luas LuaJIT, Javascript V8, Pythons PyPy), vilket har gjort att deras prestanda kan konkurrera med traditionella systemprogrammeringsspråk (C, C++). Storskaliga distribuerade system som Spark är skrivna i Scala2. Nya programmeringsspråk som Julia, Swift och Go fortsätter att flytta prestandagränserna för skräpprogrammerade språk.

En panel med namnet Systems Programming in 2014 and Beyond innehöll de största hjärnorna bakom dagens självidentifierade systemspråk: Bjarne Stroustrup (skapare av C++), Rob Pike (skapare av Go), Andrei Alexandrescu (D-utvecklare) och Niko Matsakis (Rust-utvecklare). På frågan ”vad är ett systemspråk 2014” svarade de (redigerad transkription):

  • Niko Matsakis: Att skriva applikationer på klientsidan. Den raka motsatsen till vad Go är utformat för. I dessa tillämpningar har du höga latensbehov, höga säkerhetskrav, en massa krav som inte dyker upp på serversidan.
  • Bjarne Stroustrup: Systemprogrammering kom från det område där man var tvungen att hantera hårdvara, och sedan blev tillämpningarna mer komplicerade. Man måste hantera komplexitet. Om du har problem med betydande resursbegränsningar är du på systemprogrammeringsområdet. Om man behöver finkornigare kontroll är det också inom systemprogrammeringens område. Det är begränsningarna som avgör om det är systemprogrammering. Har du slut på minne? Har du ont om tid?
  • Rob Pike: När vi först tillkännagav Go kallade vi det för ett systemprogrammeringsspråk, och jag ångrar det lite eftersom många antog att det var ett språk för att skriva operativsystem. Vad vi borde ha kallat det är ett språk för serverskrivande, vilket är vad vi egentligen tänkte att det skulle vara. Nu förstår jag att vi har ett språk för molninfrastruktur. En annan definition av systemprogrammering är saker som körs i molnet.
  • Andrei Alexandrescu: Jag har ett par lackmustester för att kontrollera om något är ett systemprogrammeringsspråk. Ett systemprogrammeringsspråk måste kunna låta dig skriva din egen minnesallokator i det. Man ska kunna förfalskat ett nummer till en pekare, eftersom det är så hårdvaran fungerar.

Har systemprogrammering med hög prestanda att göra då? Resursbegränsningar? Hårdvarukontroll? Molninfrastruktur? Det verkar i stort sett som om språk i kategorin C, C++, Rust och D skiljer sig åt när det gäller deras abstraktionsnivå från maskinen. Dessa språk avslöjar detaljer i den underliggande hårdvaran som minnesallokering/layout och finkornig resurshantering.

Ett annat sätt att tänka på det: När du har ett effektivitetsproblem, hur mycket frihet har du att lösa det? Det underbara med programmeringsspråk på låg nivå är att när du identifierar en ineffektivitet ligger det inom din makt att eliminera flaskhalsen genom noggrann kontroll av maskindetaljerna. Vektorisera den här instruktionen, ändra storleken på den datastrukturen för att behålla den i cacheminnet, och så vidare. På samma sätt som statiska typer ger större säkerhet3 som ”de här två sakerna som jag försöker lägga till är definitivt heltal”, ger lågnivåspråk större säkerhet om att ”den här koden kommer att köras på maskinen så som jag har angett.”

Däremot är optimering av tolkade språk en absolut djungel. Det är otroligt svårt att veta om körtiden konsekvent kommer att köra din kod på det sätt som du förväntar dig. Detta är exakt samma problem som med kompilatorer med automatisk parallellisering – ”auto-vektorisering är inte en programmeringsmodell” (se The story of ispc). Det är som att skriva ett gränssnitt i Python och tänka ”jag hoppas verkligen att den som anropar den här funktionen ger mig ett int.”

I dag: …så vad är systemprogrammering?

Detta för mig tillbaka till mitt ursprungliga klagomål. Det som många kallar systemprogrammering ser jag bara som programmering på låg nivå – att avslöja detaljer i maskinen. Men hur är det med system då? Minns vår definition från 1972:

  1. Problemet som skall lösas är av bred karaktär och består av många, och vanligtvis ganska varierande, delproblem.
  2. Systemprogrammet kommer troligen att användas som stöd för andra program- och tillämpningsprogram, men det kan också vara ett komplett tillämpningspaket i sig självt.
  3. Det är utformat för kontinuerlig användning i ”produktion” snarare än för en engångslösning på ett enda tillämpningsproblem.
  4. Det kommer sannolikt att utvecklas kontinuerligt när det gäller antalet och typerna av funktioner som det stöder.
  5. Ett systemprogram kräver en viss disciplin eller struktur, både inom och mellan moduler (dvs. ”kommunikation”), och det utformas och implementeras vanligen av mer än en person.

Det här verkar mer likna programvarutekniska frågor (modularitet, återanvändning, utveckling av koden) än prestandafrågor på låg nivå. Vilket innebär att varje programmeringsspråk som prioriterar att ta itu med dessa problem är ett systemprogrammeringsspråk! Det betyder fortfarande inte att varje språk är ett systemprogrammeringsspråk. Dynamiska programmeringsspråk är utan tvekan fortfarande långt ifrån systemspråk, eftersom dynamiska typer och idiom som ”be om förlåtelse, inte om lov” inte bidrar till god kodkvalitet.

Vad ger oss denna definition då? Här är en heta kommentar: funktionella språk som OCaml och Haskell är mycket mer systemorienterade än lågnivåspråk som C eller C++. När vi lär ut systemprogrammering till studenter bör vi inkludera principer för funktionell programmering, t.ex. värdet av oföränderlighet, effekten av rika typsystem för att förbättra gränssnittsdesignen och nyttan av funktioner av högre ordning. Skolor bör lära ut både systemprogrammering och programmering på låg nivå.

Som förespråkas, finns det en skillnad mellan systemprogrammering och god programvaruteknik? Egentligen inte, men ett problem här är att programvaruteknik och programmering på låg nivå ofta lärs ut isolerat. Även om de flesta kurser i programvaruteknik vanligtvis är Java-centrerade ”skriv bra gränssnitt och tester”, bör vi också lära studenterna hur man utformar system som har betydande resursbegränsningar. Kanske kallar vi programmering på låg nivå för ”system” eftersom många av de mest intressanta programvarusystemen är på låg nivå (t.ex. databaser, nätverk, operativsystem osv.). Eftersom system på låg nivå har många begränsningar kräver de att dess konstruktörer tänker kreativt.

En annan inramning är att programmerare på låg nivå bör försöka förstå vilka idéer inom systemkonstruktion som skulle kunna anpassas för att hantera den moderna maskinvarans verklighet. Jag tycker att Rust-gemenskapen har varit oerhört innovativ i detta avseende, genom att titta på hur goda principer för programvarudesign/funktionell programmering kan tillämpas på problem på låg nivå (t.ex. futures, felhantering eller naturligtvis minnessäkerhet).

För att sammanfatta: det vi kallar ”systemprogrammering” tycker jag att man borde kalla det för ”lågnivåprogrammering”. Design av datorsystem som område är för viktigt för att inte ha ett eget namn. Att tydligt separera dessa två idéer ger en större begreppsmässig klarhet om området för programmeringsspråksdesign, och det öppnar också dörren för att dela insikter mellan de två områdena: hur kan vi designa systemet runt maskinen och vice versa?

Vänligen skicka kommentarer till min inkorg på [email protected] eller Hacker News.

  1. Skojigt faktum här: två av författarna till den här uppsatsen, R. Bergeron och Andy Van Dam, är grundare av grafikgemenskapen och SIGGRAPH-konferensen. Det är en del av ett fortsatt mönster där grafikforskare sätter trenden inom systemdesign, se t.ex. ursprunget till GPGPU.

  2. Obligatorisk länk till Scalability! Men till vilket pris?

  3. Idealt är statiska typer en 100-procentig garanti (eller pengarna tillbaka), men i praktiken tillåter de flesta språk ett visst mått av Obj.magic.

Lämna ett svar

Din e-postadress kommer inte publiceras.