&Notepad

Will Crichton – 9 septembrie 2018
Am o nemulțumire cu sintagma „programare de sisteme”. Pentru mine, mi s-a părut întotdeauna că combină în mod inutil două idei: programarea de nivel scăzut (care se ocupă de detaliile de implementare a mașinii) și proiectarea sistemelor (crearea și gestionarea unui set complex de componente interoperabile). De ce se întâmplă acest lucru? De cât timp este adevărat acest lucru? Și ce am putea câștiga din redefinirea ideii de sisteme?

Anii ’70: Îmbunătățirea asamblării

Să călătorim înapoi la originile sistemelor informatice moderne pentru a înțelege cum a evoluat termenul. Nu știu cine a inventat expresia inițial, dar căutările mele sugerează că efortul serios de definire a „sistemelor de calculatoare” a început pe la începutul anilor ’70. În Limbaje de programare a sistemelor (Bergeron1 et al. 1972), autorii spun:

Un program de sistem este un set integrat de subprograme, care formează împreună un întreg mai mare decât suma părților sale și care depășește un anumit prag de dimensiune și/sau complexitate. Exemple tipice sunt sistemele de multiprogramare, de traducere, de simulare, de gestionare a informațiilor și de partajare a timpului. În continuare este prezentat un set parțial de proprietăți,dintre care unele se regăsesc în non-sisteme, nu este necesar ca toate să fie prezente într-un anumit sistem.

  1. Problema care trebuie rezolvată este de natură largă și constă din multe subprobleme, de obicei destul de variate.
  2. Programul de sistem este susceptibil de a fi utilizat pentru a sprijini alte programe software și programe de aplicații, dar poate fi, de asemenea, un pachet complet de aplicații în sine.
  3. Este conceput pentru o utilizare continuă de „producție”, mai degrabă decât pentru o soluție unică la o singură problemă de aplicații.
  4. Este posibil să fie în continuă evoluție în ceea ce privește numărul și tipurile de caracteristici pe care le suportă.
  5. Un program de sistem necesită o anumită disciplină sau structură, atât în interiorul modulelor, cât și între module (de exemplu, „comunicare”), și este de obicei proiectat și implementat de mai multe persoane.

Această definiție este destul de agreabilă – sistemele de calculatoare sunt la scară largă, sunt utilizate pe termen lung și variază în timp. Cu toate acestea, în timp ce această definiție este în mare parte descriptivă, o idee cheie din lucrare este prescriptivă: pledează pentru separarea limbajelor de nivel scăzut de limbajele de sistem (la vremea respectivă, contrastând asamblarea cu FORTRAN).

Obiectivul unui limbaj de programare de sistem este de a oferi un limbaj care poate fi utilizat fără a se preocupa în mod nejustificat de considerații legate de „învârteala de biți”, dar care va genera cod care nu este sensibil mai rău decât cel generat manual. Un astfel de limbaj ar trebui să combine concizia ș i lizibilitatea limbajelor de nivel înalt cu eficiența spațială ș i temporală ș i capacitatea de a „accesa” facilitățile maș inii ș i ale sistemului de operare care pot fi obținute în limbajul de asamblare. Timpul de proiectare, de scriere și de depanare ar trebui să fie minimizat fără a impune o supraîncărcare inutilă a resurselor sistemelor.

În același timp, cercetătorii de la CMU au publicat BLISS: A Language for Systems Programming (Wulf et al. 1972), descriindu-l astfel:

Noi ne referim la BLISS ca la un „limbaj de implementare”, deși admitem că termenul este oarecum ambiguu deoarece, probabil, toate limbajele de calculator sunt folosite pentru a implementa ceva. Pentru noi, expresia conturează un limbaj de nivel superior, de uz general, în care accentul principal a fost pus pe o aplicație specifică, și anume scrierea de sisteme software mari, de producție, pentru o anumită mașină. Limbajele cu scop special, cum ar fi compilatoarele-compilatoare, nu intră în această categorie și nici nu presupunem neapărat că aceste limbaje trebuie să fie independente de mașină. Subliniem cuvântul „implementare” în definiția noastră și nu am folosit cuvinte precum „proiectare” și „documentație”. Nu ne așteptăm neapărat ca un limbaj de implementare să fie un vehicul adecvat pentru exprimarea proiectării inițiale a unui sistem mare și nici pentru documentarea exclusivă a acelui sistem. Concepte cum ar fi independența față de mașină, exprimarea proiectării și implementării în aceeași notație, autodocumentarea și altele, sunt în mod clar obiective dezirabile și sunt criteriile după care am evaluat diverse limbaje.

Aici, autorii contrastează ideea unui „limbaj de implementare” ca fiind de nivel superior față de asamblare, dar de nivel inferior față de un „limbaj de proiectare”. Acest lucru se opune definiției din lucrarea precedentă, susținând că proiectarea unui sistem și implementarea unui sistem ar trebui să aibă limbaje separate.

Ambele lucrări sunt artefacte de cercetare sau pledoarii. Ultima intrare de luat în considerare (tot din 1972, un an productiv!) este Systems Programming (Donovan 1972), un text educațional pentru învățarea programării sistemelor.

Ce este programarea sistemelor? Este posibil să vizualizați un computer ca pe un fel de bestie care ascultă de toate comenzile. S-a spus că, în esență, computerele sunt oameni făcuți din metal sau, invers, oamenii sunt computere făcute din carne și oase. Cu toate acestea, odată ce ne apropiem de computere, vedem că acestea sunt, în esență, mașini care urmează instrucțiuni foarte specifice și primitive. La începuturile calculatoarelor, oamenii comunicau cu ele prin intermediul unor întrerupătoare de pornire și oprire care denotă instrucțiuni primitive. În curând, oamenii au dorit să dea instrucțiuni mai complexe. De exemplu, au vrut să poată spune X = 30 * Y; având în vedere că Y = 10, cât este X? Calculatoarele actuale nu pot înțelege un astfel de limbaj fără ajutorul unor programe de sistem. Programele de sistem (de exemplu, compilatoare, încărcătoare, procesoare de macrocomenzi, sisteme de operare) au fost dezvoltate pentru a face computerele mai bine adaptate la nevoile utilizatorilor lor. Mai mult, oamenii doreau mai multă asistență în mecanica pregătirii programelor lor.

Îmi place că această definiție ne reamintește că sistemele sunt în slujba oamenilor, chiar dacă sunt doar o infrastructură care nu este expusă direct utilizatorului final.

Anii ’90: Ascensiunea scripturilor

În anii ’70 și ’80, se pare că majoritatea cercetătorilor vedeau programarea sistemelor de obicei ca pe un contrast cu programarea în asamblare. Pur și simplu nu existau alte instrumente bune pentru a construi sisteme. (Nu sunt sigur unde era Lisp în toate acestea? Niciuna dintre resursele pe care le-am citit nu a citat Lisp, deși sunt vag conștient că mașinile Lisp au existat totuși pentru scurt timp.)

Cu toate acestea, la mijlocul anilor ’90, a avut loc o schimbare majoră în limbajele de programare, odată cu apariția limbajelor de scripting cu tastare dinamică. Îmbunătățind sistemele anterioare de scripting de tip shell, cum ar fi Bash, limbaje precum Perl (1987), Tcl (1988), Python (1990), Ruby (1995), PHP (1995) și Javascript (1995) și-au croit drum în mainstream. Acest lucru a culminat cu influentul articol „Scripting: Programare de nivel superior pentru secolul XXI” (Ousterhout 1998). Acesta a articulat „dihotomia lui Ousterhout” între „limbajele de programare de sistem” și „limbajele de scripting.”

Limbajele de scripting sunt concepute pentru sarcini diferite față de limbajele de programare de sistem, iar acest lucru duce la diferențe fundamentale între limbaje. Limbajele de programare de sistem au fost concepute pentru a construi structuri de date și algoritmi de la zero, pornind de la cele mai primitive elemente de calculator, cum ar fi cuvintele de memorie. În schimb, limbajele de scripting sunt concepute pentru lipire: ele presupun existența unui set de componente puternice și sunt destinate în primul rând conectării componentelor între ele. Limbajele de programare de sistem sunt puternic tipizate pentru a ajuta la gestionarea complexității, în timp ce limbajele de scripting sunt lipsite de tip pentru a simplifica conexiunile dintre componente și pentru a asigura dezvoltarea rapidă a aplicațiilor. Mai multe tendințe recente, cum ar fi mașinile mai rapide, limbajele de scripting mai bune, importanța crescândă a interfețelor grafice cu utilizatorul și a arhitecturilor de componente, precum și creșterea internetului, au sporit foarte mult aplicabilitatea limbajelor de scripting.

La nivel tehnic, Ousterhout a pus în contrast scriptingul față de sisteme de-a lungul axelor de siguranță a tipurilor și instrucțiuni pe declarație, așa cum se arată mai sus. La nivel de proiectare, el a caracterizat noile roluri pentru fiecare clasă de limbaj: programarea de sisteme este pentru crearea de componente, iar scriptingul este pentru lipirea acestora.

În această perioadă, limbajele cu tipare statică, dar cu colectare de gunoi, au început, de asemenea, să câștige popularitate. Java (1995) și C# (2000) s-au transformat în titanii pe care îi cunoaștem astăzi. Deși aceste două nu sunt considerate în mod tradițional „limbaje de programare a sistemelor”, ele au fost folosite pentru a proiecta multe dintre cele mai mari sisteme software din lume. Ousterhout chiar a menționat în mod explicit că „în lumea internetului care se conturează acum, Java este folosit pentru programarea sistemelor.”

Anii 2010: Granițele se estompează

În ultimul deceniu, linia de demarcație dintre limbajele de scripting și limbajele de programare a sistemelor a început să se estompeze. Companii precum Dropbox au fost capabile să construiască sisteme surprinzător de mari și scalabile doar pe Python. Javascript este folosit pentru a reda în timp real interfețe complexe în miliarde de pagini web. Tastarea graduală a câștigat avânt în Python, Javascript și alte limbaje de scripting, permițând o tranziție de la codul „prototip” la codul „de producție” prin adăugarea incrementală de informații statice de tip.

În același timp, resursele inginerești masive investite în compilatoarele JIT atât pentru limbajele statice (de exemplu, Java’s HotSpot), cât și pentru cele dinamice (de exemplu, LuaJIT de la Lua, V8 de la Javascript, PyPy de la Python) au făcut ca performanțele acestora să fie competitive cu cele ale limbajelor tradiționale de programare a sistemelor (C, C++). Sistemele distribuite la scară largă, precum Spark, sunt scrise în Scala2. Noile limbaje de programare, cum ar fi Julia, Swift și Go, continuă să împingă limitele de performanță ale limbajelor de tip garbage-collected.

Un panel intitulat Systems Programming in 2014 and Beyond (Programarea sistemelor în 2014 și mai departe) a prezentat cele mai mari minți din spatele limbajelor de sisteme autoidentificate din prezent: Bjarne Stroustrup (creatorul lui C++), Rob Pike (creatorul lui Go), Andrei Alexandrescu (dezvoltator D) și Niko Matsakis (dezvoltator Rust). La întrebarea „ce este un limbaj de programare de sistem în 2014”, aceștia au răspuns (transcriere editată):

  • Niko Matsakis: Scrierea de aplicații pe partea de client. Opusul polar al scopului pentru care este proiectat Go. În aceste aplicații, aveți nevoi mari de latență, cerințe mari de securitate, o mulțime de cerințe care nu apar pe partea de server.
  • Bjarne Stroustrup: Programarea de sisteme a ieșit din domeniul în care trebuia să te ocupi de hardware, iar apoi aplicațiile au devenit mai complicate. Trebuie să te ocupi de complexitate. Dacă aveți probleme legate de constrângeri semnificative de resurse, vă aflați în domeniul programării de sisteme. Dacă aveți nevoie de un control mai fin, vă aflați, de asemenea, în domeniul programării sistemelor. Constrângerile sunt cele care determină dacă este vorba de programare de sisteme. Rămâneți fără memorie? Sunteți în criză de timp?
  • Rob Pike: Când am anunțat pentru prima dată Go, l-am numit un limbaj de programare a sistemelor și regret puțin acest lucru, deoarece mulți oameni au presupus că este un limbaj de scriere a sistemelor de operare. Ceea ce ar fi trebuit să numim este un limbaj de scriere pentru servere, care este ceea ce ne-am gândit cu adevărat că este. Acum înțeleg că ceea ce avem este un limbaj pentru infrastructura cloud. O altă definiție a programării sistemelor este chestia care rulează în cloud.
  • Andrei Alexandrescu: Am câteva teste de turnesol pentru a verifica dacă ceva este un limbaj de programare a sistemelor. Un limbaj de programare de sisteme trebuie să fie capabil să îți permită să îți scrii propriul alocator de memorie în el. Trebuie să poți falsifica un număr într-un pointer, pentru că așa funcționează hardware-ul.

Atunci programarea de sisteme înseamnă performanță ridicată? Constrângerile de resurse? Controlul hardware? Infrastructura cloud? Se pare că, în linii mari, limbajele din categoria C, C++, Rust și D se disting în funcție de nivelul de abstractizare față de mașină. Aceste limbaje expun detalii ale hardware-ului subiacent, cum ar fi alocarea/dispunerea memoriei și gestionarea fină a resurselor.

Un alt mod de a gândi la asta: când aveți o problemă de eficiență, câtă libertate aveți pentru a o rezolva? Partea minunată a limbajelor de programare de nivel scăzut este că, atunci când identificați o ineficiență, vă stă în putere să eliminați gâtul de îmbulzeală printr-un control atent asupra detaliilor mașinii. Vectorizați această instrucțiune, redimensionați această structură de date pentru a o păstra în memoria cache, și așa mai departe. În același mod în care tipurile statice oferă mai multă încredere3 cum ar fi „aceste două lucruri pe care încerc să le adaug sunt cu siguranță numere întregi”, limbajele de nivel scăzut oferă mai multă încredere că „acest cod se va executa pe mașină așa cum am specificat.”

În schimb, optimizarea limbajelor interpretate este o adevărată junglă. Este incredibil de greu de știut dacă timpul de execuție va executa în mod constant codul dumneavoastră în modul în care vă așteptați. Aceasta este exact aceeași problemă cu compilatoarele cu autoparalelizare – „autovectorizarea nu este un model de programare” (a se vedea The story of ispc). Este ca și cum ai scrie o interfață în Python, gândindu-te „ei bine, cu siguranță sper ca oricine apelează această funcție să-mi dea un int.”

Astăzi: …deci ce este programarea sistemelor?

Acest lucru mă aduce înapoi la nemulțumirea mea inițială. Ceea ce mulți oameni numesc programare de sisteme, eu mă gândesc doar la programarea de nivel scăzut – expunând detalii ale mașinii. Dar ce se întâmplă atunci cu sistemele? Reamintiți-vă definiția noastră din 1972:

  1. Problema care trebuie rezolvată este de natură largă, constând din mai multe subprobleme și, de obicei, destul de variate.
  2. Programul de sistem este probabil să fie folosit pentru a sprijini alte programe software și programe de aplicații, dar poate fi, de asemenea, un pachet complet de aplicații în sine.
  3. Este proiectat pentru o utilizare continuă în „producție”, mai degrabă decât pentru o soluție unică la o singură problemă de aplicații.
  4. Este posibil să fie în continuă evoluție în ceea ce privește numărul și tipurile de caracteristici pe care le suportă.
  5. Un program de sistem necesită o anumită disciplină sau structură, atât în interiorul modulelor, cât și între module (de exemplu, „comunicare”) și este, de obicei, proiectat și implementat de mai multe persoane.

Acestea par a fi mult mai mult probleme de inginerie software (modularitate, reutilizare, evoluția codului) decât probleme de performanță de nivel scăzut. Ceea ce înseamnă că orice limbaj de programare care prioritizează abordarea acestor probleme este un limbaj de programare de sisteme! Asta nu înseamnă totuși că orice limbaj este un limbaj de programare de sisteme. Se poate spune că limbajele de programare dinamice sunt încă departe de limbajele de sisteme, deoarece tipurile dinamice și idiomurile de genul „cere iertare, nu permisiune” nu sunt favorabile unei bune calități a codului.

Ce ne aduce această definiție, atunci? Iată o idee fierbinte: limbajele funcționale precum OCaml și Haskell sunt mult mai orientate către sisteme decât limbajele de nivel scăzut precum C sau C++. Atunci când predăm programarea de sisteme studenților, ar trebui să includem principii de programare funcțională, cum ar fi valoarea imutabilității, impactul sistemelor de tipuri bogate în îmbunătățirea proiectării interfețelor și utilitatea funcțiilor de ordin superior. Școlile ar trebui să predea atât programarea sistemelor, cât și programarea de nivel scăzut.

Așa cum se susține, există o distincție între programarea sistemelor și o bună inginerie software? Nu chiar, dar o problemă aici este că ingineria software și programarea de nivel scăzut sunt adesea predate în mod izolat. În timp ce majoritatea cursurilor de inginerie software sunt, de obicei, centrate pe Java „scrieți interfețe și teste bune”, ar trebui, de asemenea, să-i învățăm pe studenți despre cum să proiecteze sisteme care au constrângeri semnificative de resurse. Poate că numim programarea de nivel scăzut „sisteme” deoarece multe dintre cele mai interesante sisteme software sunt de nivel scăzut (de exemplu, baze de date, rețele, sisteme de operare etc.). Deoarece sistemele de nivel scăzut au multe constrângeri, ele cer proiectanților săi să gândească creativ.

O altă încadrare este că programatorii de nivel scăzut ar trebui să caute să înțeleagă ce idei de proiectare a sistemelor ar putea fi adaptate pentru a face față realității hardware-ului modern. Cred că comunitatea Rust a fost extrem de inovatoare în această privință, analizând modul în care bunele principii de proiectare software/programare funcțională pot fi aplicate la probleme de nivel scăzut (de exemplu, futures, gestionarea erorilor sau, bineînțeles, siguranța memoriei).

Pentru a rezuma, ceea ce noi numim „programare de sisteme” cred că ar trebui să se numească „programare de nivel scăzut”. Proiectarea sistemelor de calculatoare ca domeniu este prea importantă pentru a nu avea un nume propriu. Separarea clară a acestor două idei oferă o mai mare claritate conceptuală asupra spațiului de proiectare a limbajelor de programare și, de asemenea, deschide ușa pentru schimbul de informații între cele două spații: cum putem proiecta sistemul în jurul mașinii și viceversa?

Vă rog să trimiteți comentariile în căsuța mea poștală la [email protected] sau Hacker News.

  1. Un fapt interesant aici: doi dintre autorii acestui articol, R. Bergeron și Andy Van Dam, sunt membri fondatori ai comunității grafice și ai conferinței SIGGRAPH. Parte a unui model continuu în care cercetătorii din domeniul graficii stabilesc tendința în proiectarea sistemelor, c.f. originea GPGPU.

  2. Legătura obligatorie către Scalability! Dar cu ce COST?.

  3. În mod cert, tipurile statice sunt 100% garantate (sau banii înapoi), dar în practică majoritatea limbajelor permit o anumită cantitate de magie Obj.magic.

Lasă un răspuns

Adresa ta de email nu va fi publicată.