&Notepad

Will Crichton – September 9, 2018
Ho un problema con la frase “programmazione dei sistemi”. A me è sempre sembrato che combinasse inutilmente due idee: programmazione di basso livello (occuparsi dei dettagli di implementazione della macchina) e progettazione di sistemi (creare e gestire un complesso insieme di componenti interoperanti). Perché è così? Per quanto tempo è stato vero? E cosa potremmo guadagnare ridefinendo l’idea di sistema?

anni ’70: Migliorare l’assemblaggio

Torniamo alle origini dei moderni sistemi informatici per capire come si è evoluto il termine. Non so chi abbia coniato la frase in origine, ma le mie ricerche suggeriscono che uno sforzo serio nel definire i “sistemi informatici” è iniziato intorno ai primi anni 70. In Systems Programming Languages (Bergeron1 et al. 1972), gli autori dicono:

Un programma di sistema è un insieme integrato di sottoprogrammi, che insieme formano un tutto maggiore della somma delle sue parti, e che supera una certa soglia di dimensione e/o complessità. Esempi tipici sono i sistemi di multiprogrammazione, traduzione, simulazione, gestione delle informazioni e condivisione del tempo. Quello che segue è un insieme parziale di proprietà, alcune delle quali si trovano nei non-sistemi, non tutte devono necessariamente essere presenti in un dato sistema.

  1. Il problema da risolvere è di natura ampia e consiste in molti, e di solito abbastanza vari, sottoproblemi.
  2. Il programma di sistema è probabile che sia usato per supportare altri programmi di software e applicazioni, ma può anche essere un pacchetto completo di applicazioni stesso.
  3. E’ progettato per un uso continuo di “produzione” piuttosto che una soluzione one-shot ad un singolo problema applicativo.
  4. E’ probabile che sia in continua evoluzione nel numero e nei tipi di caratteristiche che supporta.
  5. Un programma di sistema richiede una certa disciplina o struttura, sia all’interno che tra i moduli (cioè, “comunicazione”), ed è solitamente progettato e implementato da più di una persona.

Questa definizione è abbastanza condivisibile – i sistemi informatici sono su larga scala, usati a lungo e variabili nel tempo. Tuttavia, mentre questa definizione è in gran parte descrittiva, un’idea chiave nel documento è prescrittiva: sostenere la separazione dei linguaggi di basso livello dai linguaggi di sistema (all’epoca, contrapponendo l’assembly al FORTRAN).

L’obiettivo di un linguaggio di programmazione di sistema è di fornire un linguaggio che possa essere usato senza preoccuparsi eccessivamente di considerazioni di “bit twiddling”, ma che generi codice che non sia sensibilmente peggiore di quello generato a mano. Un tale linguaggio dovrebbe combinare la concisione e la leggibilità dei linguaggi di alto livello con l’efficienza di spazio e tempo e l’abilità di “arrivare a” strutture della macchina e del sistema operativo ottenibili nel linguaggio assembler. Il tempo di progettazione, scrittura e debug dovrebbe essere minimizzato senza imporre un inutile sovraccarico sulle risorse del sistema.

Allo stesso tempo, i ricercatori della CMU pubblicarono BLISS: A Language for Systems Programming (Wulf et al. 1972), descrivendolo come:

Ci riferiamo a BLISS come un “linguaggio di implementazione”, sebbene ammettiamo che il termine sia in qualche modo ambiguo poiché, presumibilmente, tutti i linguaggi informatici sono usati per implementare qualcosa. Per noi la frase connota un linguaggio di livello superiore di uso generale in cui l’enfasi primaria è stata posta su un’applicazione specifica, cioè la scrittura di grandi sistemi software di produzione per una macchina specifica. I linguaggi per scopi speciali, come i compilatori-compilatori, non rientrano in questa categoria, né assumiamo necessariamente che questi linguaggi debbano essere indipendenti dalla macchina. Sottolineiamo la parola “implementazione” nella nostra definizione e non abbiamo usato parole come “progettazione” e “documentazione”. Non ci aspettiamo necessariamente che un linguaggio di implementazione sia un veicolo appropriato per esprimere il progetto iniziale di un grande sistema o per la documentazione esclusiva di quel sistema. Concetti come l’indipendenza dalla macchina, l’espressione del progetto e dell’implementazione nella stessa notazione, l’auto-documentazione e altri, sono chiaramente obiettivi desiderabili e sono criteri con cui abbiamo valutato vari linguaggi.

Qui gli autori contrappongono l’idea di un “linguaggio di implementazione” come se fosse di livello superiore all’assembly, ma di livello inferiore a un “linguaggio di progettazione”. Questo resiste alla definizione del documento precedente, sostenendo che la progettazione di un sistema e l’implementazione di un sistema dovrebbero avere linguaggi separati.

Entrambi questi documenti sono artefatti di ricerca o sostenitori. L’ultima voce da considerare (anch’essa del 1972, un anno produttivo!) è Systems Programming (Donovan 1972), un testo educativo per imparare la programmazione dei sistemi.

Che cos’è la programmazione dei sistemi? Si può visualizzare un computer come una specie di bestia che obbedisce a tutti i comandi. È stato detto che i computer sono fondamentalmente persone fatte di metallo o, al contrario, le persone sono computer fatti di carne e sangue. Tuttavia, una volta che ci avviciniamo ai computer, vediamo che sono fondamentalmente macchine che seguono istruzioni molto specifiche e primitive. Nei primi tempi dei computer, la gente comunicava con loro tramite interruttori on e off che denotavano istruzioni primitive. Ben presto la gente volle dare istruzioni più complesse. Per esempio, volevano poter dire X = 30 * Y; dato che Y = 10, qual è X? I computer attuali non possono capire un tale linguaggio senza l’aiuto di programmi di sistema. I programmi di sistema (ad esempio compilatori, caricatori, processori di macro, sistemi operativi) sono stati sviluppati per rendere i computer più adatti ai bisogni dei loro utenti. Inoltre, la gente voleva più assistenza nella meccanica della preparazione dei loro programmi.

Mi piace che questa definizione ci ricordi che i sistemi sono al servizio della gente, anche se sono solo infrastrutture non direttamente esposte all’utente finale.

anni ’90: L’ascesa dello scripting

Negli anni ’70 e ’80, sembra che la maggior parte dei ricercatori vedesse la programmazione dei sistemi di solito come un contrasto alla programmazione assembly. Semplicemente non c’erano altri buoni strumenti per costruire sistemi. (Non sono sicuro di dove fosse Lisp in tutto questo? Nessuna delle risorse che ho letto citava Lisp, anche se sono vagamente consapevole che le macchine Lisp sono esistite, anche se per poco tempo).

Tuttavia, a metà degli anni ’90, si è verificato un grande cambiamento nei linguaggi di programmazione con l’aumento dei linguaggi di scripting con tipizzazione dinamica. Migliorando i precedenti sistemi di scripting di shell come Bash, linguaggi come Perl (1987), Tcl (1988), Python (1990), Ruby (1995), PHP (1995) e Javascript (1995) si fecero strada nel mainstream. Questo culminò nell’influente articolo “Scripting: Higher Level Programming for the 21st Century” (Ousterhout 1998). Questo articolava “la dicotomia di Ousterhout” tra “linguaggi di programmazione di sistema” e “linguaggi di scripting”.

I linguaggi di scripting sono progettati per compiti diversi dai linguaggi di programmazione di sistema, e questo porta a differenze fondamentali nei linguaggi. I linguaggi di programmazione di sistema sono stati progettati per costruire strutture di dati e algoritmi da zero, partendo dagli elementi informatici più primitivi come le parole di memoria. Al contrario, i linguaggi di scripting sono progettati per l’incollaggio: presuppongono l’esistenza di un insieme di componenti potenti e sono destinati principalmente a collegare i componenti insieme. I linguaggi di programmazione di sistema sono fortemente tipizzati per aiutare a gestire la complessità, mentre i linguaggi di scripting sono senza tipi per semplificare le connessioni tra i componenti e fornire un rapido sviluppo delle applicazioni. Diverse tendenze recenti, come macchine più veloci, linguaggi di scripting migliori, la crescente importanza delle interfacce grafiche e delle architetture di componenti, e la crescita di Internet, hanno notevolmente aumentato l’applicabilità dei linguaggi di scripting.

A livello tecnico, Ousterhout ha contrapposto lo scripting ai sistemi lungo gli assi della sicurezza dei tipi e delle istruzioni per frase, come mostrato sopra. A livello di design, ha caratterizzato i nuovi ruoli per ogni classe di linguaggio: la programmazione dei sistemi è per creare componenti, e lo scripting è per incollarli insieme.

In questo periodo, i linguaggi staticamente tipizzati ma garbage collected hanno anche iniziato a guadagnare popolarità. Java (1995) e C# (2000) si sono trasformati nei titani che conosciamo oggi. Mentre questi due non sono tradizionalmente considerati “linguaggi di programmazione di sistemi”, sono stati usati per progettare molti dei più grandi sistemi software del mondo. Ousterhout ha persino menzionato esplicitamente “nel mondo di Internet che sta prendendo forma ora, Java è usato per la programmazione di sistema.”

anni 2010: I confini si confondono

Nell’ultimo decennio, la linea tra i linguaggi di scripting e i linguaggi di programmazione dei sistemi ha iniziato a confondersi. Aziende come Dropbox sono state in grado di costruire sistemi sorprendentemente grandi e scalabili solo con Python. Javascript è usato per rendere in tempo reale, complesse UI in miliardi di pagine web. La tipizzazione graduale ha guadagnato terreno in Python, Javascript e altri linguaggi di scripting, permettendo una transizione dal codice “prototipo” al codice “produzione” aggiungendo in modo incrementale informazioni statiche sul tipo.

Al tempo stesso, massicce risorse ingegneristiche riversate nei compilatori JIT sia per i linguaggi statici (ad esempio HotSpot di Java) che per quelli dinamici (ad esempio LuaJIT di Lua, V8 di Javascript, PyPy di Python) hanno reso le loro prestazioni competitive con i tradizionali linguaggi di programmazione di sistema (C, C++). Sistemi distribuiti su larga scala come Spark sono scritti in Scala2. Nuovi linguaggi di programmazione come Julia, Swift e Go continuano a spingere i limiti delle prestazioni sui linguaggi garbage-collected.

Un pannello chiamato Systems Programming in 2014 and Beyond ha presentato le più grandi menti dietro i linguaggi di sistema autoidentificati di oggi: Bjarne Stroustrup (creatore di C++), Rob Pike (creatore di Go), Andrei Alexandrescu (sviluppatore di D) e Niko Matsakis (sviluppatore di Rust). Alla domanda “cos’è un linguaggio di programmazione di sistemi nel 2014”, hanno detto (trascrizione modificata):

  • Niko Matsakis: Scrivere applicazioni lato client. L’opposto polare di ciò per cui Go è progettato. In queste applicazioni, si hanno esigenze di alta latenza, requisiti di alta sicurezza, un sacco di requisiti che non si presentano sul lato server.
  • Bjarne Stroustrup: La programmazione dei sistemi è venuta fuori dal campo in cui si aveva a che fare con l’hardware, e poi le applicazioni sono diventate più complicate. È necessario avere a che fare con la complessità. Se si hanno problemi di limitazioni significative delle risorse, si è nel campo della programmazione dei sistemi. Se hai bisogno di un controllo a grana più fine, allora sei anche nel dominio della programmazione dei sistemi. Sono i vincoli che determinano se si tratta di programmazione di sistemi. State finendo la memoria? Sei a corto di tempo?
  • Rob Pike: Quando abbiamo annunciato Go per la prima volta, lo abbiamo chiamato un linguaggio di programmazione di sistemi, e me ne sono un po’ pentito perché molte persone hanno pensato che fosse un linguaggio di scrittura per sistemi operativi. Quello che avremmo dovuto chiamare è un linguaggio di scrittura per server, che è quello che realmente pensavamo fosse. Ora capisco che quello che abbiamo è un linguaggio per infrastrutture cloud. Un’altra definizione di programmazione di sistemi è la roba che gira nel cloud.
  • Andrei Alexandrescu: Ho alcune cartine di tornasole per verificare se qualcosa è un linguaggio di programmazione di sistemi. Un linguaggio di programmazione di sistemi deve essere in grado di permettervi di scrivere il vostro allocatore di memoria in esso. Dovreste essere in grado di falsificare un numero in un puntatore, dato che è così che funziona l’hardware.

La programmazione di sistemi riguarda le alte prestazioni allora? Vincoli di risorse? Controllo dell’hardware? Infrastruttura cloud? Sembra che, a grandi linee, i linguaggi della categoria C, C++, Rust e D si distinguano in termini di livello di astrazione dalla macchina. Questi linguaggi espongono dettagli dell’hardware sottostante come l’allocazione/layout della memoria e la gestione delle risorse a grana fine.

Un altro modo di pensarci: quando si ha un problema di efficienza, quanta libertà si ha per risolverlo? La parte meravigliosa dei linguaggi di programmazione a basso livello è che quando identificate un’inefficienza, è in vostro potere eliminare il collo di bottiglia con un attento controllo dei dettagli della macchina. Vettorializzate questa istruzione, ridimensionate quella struttura dati per tenerla nella cache, e così via. Nello stesso modo in cui i tipi statici forniscono più sicurezza3 come “queste due cose che sto cercando di aggiungere sono sicuramente numeri interi”, i linguaggi di basso livello forniscono più sicurezza che “questo codice verrà eseguito sulla macchina come ho specificato.”

Al contrario, ottimizzare i linguaggi interpretati è una giungla assoluta. È incredibilmente difficile sapere se il runtime eseguirà costantemente il vostro codice nel modo che vi aspettate. Questo è lo stesso identico problema dei compilatori auto-parallelizzanti – “l’auto-vettorizzazione non è un modello di programmazione” (vedi La storia di ispc). È come scrivere un’interfaccia in Python, pensando “beh spero proprio che chi chiama questa funzione mi dia un int.”

Oggi: …cos’è la programmazione dei sistemi?

Questo mi riporta al mio problema originale. Quello che molti chiamano programmazione dei sistemi, io lo considero solo come programmazione di basso livello, che espone i dettagli della macchina. Ma che dire dei sistemi allora? Ricordiamo la nostra definizione del 1972:

  1. Il problema da risolvere è di natura ampia e consiste di molti, e di solito abbastanza vari, sotto-problemi.
  2. Il programma di sistema è probabile che sia usato per supportare altri programmi software e applicazioni, ma può anche essere un pacchetto completo di applicazioni esso stesso.
  3. E’ progettato per un continuo uso “produttivo” piuttosto che una soluzione one-shot ad un singolo problema applicativo.
  4. E’ probabile che sia in continua evoluzione nel numero e nei tipi di caratteristiche che supporta.
  5. Un programma di sistema richiede una certa disciplina o struttura, sia all’interno che tra i moduli (cioè la “comunicazione”), ed è solitamente progettato e implementato da più di una persona.

Queste sembrano più questioni di ingegneria del software (modularità, riuso, evoluzione del codice) che questioni di prestazioni di basso livello. Il che significa che ogni linguaggio di programmazione che dà priorità all’affrontare questi problemi è un linguaggio di programmazione di sistemi! Questo ancora non significa che ogni linguaggio sia un linguaggio di programmazione di sistemi. I linguaggi di programmazione dinamici sono probabilmente ancora lontani dai linguaggi di sistema, poiché i tipi dinamici e gli idiomi come “chiedere perdono, non permesso” non sono favorevoli ad una buona qualità del codice.

Cosa ci porta questa definizione, allora? Ecco un esempio: i linguaggi funzionali come OCaml e Haskell sono molto più orientati ai sistemi rispetto ai linguaggi di basso livello come C o C++. Quando insegniamo la programmazione dei sistemi ai laureandi, dovremmo includere principi di programmazione funzionale come il valore dell’immutabilità, l’impatto dei sistemi di tipi ricchi nel migliorare la progettazione dell’interfaccia e l’utilità delle funzioni di ordine superiore. Le scuole dovrebbero insegnare sia la programmazione dei sistemi che la programmazione di basso livello.

Come sostenuto, c’è una distinzione tra la programmazione dei sistemi e la buona ingegneria del software? Non proprio, ma un problema qui è che l’ingegneria del software e la programmazione di basso livello sono spesso insegnate separatamente. Mentre la maggior parte dei corsi di ingegneria del software sono di solito Java-centrici “scrivi buone interfacce e test”, dovremmo anche insegnare agli studenti come progettare sistemi che hanno significativi vincoli di risorse. Forse chiamiamo la programmazione di basso livello “sistemi” perché molti dei sistemi software più interessanti sono di basso livello (per esempio database, reti, sistemi operativi, ecc.). Poiché i sistemi di basso livello hanno molti vincoli, richiedono ai loro progettisti di pensare in modo creativo.

Un’altra inquadratura è che i programmatori di basso livello dovrebbero cercare di capire quali idee nella progettazione di sistemi potrebbero essere adattate per affrontare la realtà dell’hardware moderno. Penso che la comunità di Rust sia stata estremamente innovativa in questo senso, guardando a come i principi di buona progettazione del software/programmazione funzionale possano essere applicati a problemi di basso livello (ad esempio i futures, la gestione degli errori, o naturalmente la sicurezza della memoria).

Per riassumere, quello che chiamiamo “programmazione dei sistemi” penso dovrebbe essere chiamato “programmazione di basso livello”. La progettazione di sistemi informatici come campo è troppo importante per non avere un nome proprio. Separare chiaramente queste due idee fornisce una maggiore chiarezza concettuale sullo spazio della progettazione dei linguaggi di programmazione, e apre anche la porta alla condivisione di intuizioni attraverso i due spazi: come possiamo progettare il sistema intorno alla macchina, e viceversa?

Si prega di indirizzare i commenti alla mia casella di posta elettronica all’indirizzo [email protected] o Hacker News.

  1. Fatto curioso: due degli autori di questo articolo, R. Bergeron e Andy Van Dam, sono membri fondatori della comunità grafica e della conferenza SIGGRAPH. Parte di un modello continuo in cui i ricercatori di grafica stabiliscono la tendenza nella progettazione dei sistemi, come l’origine della GPGPU.

  2. Collegamento obbligatorio alla Scalabilità! Ma a quale COSTO?

  3. Idealmente i tipi statici sono garantiti al 100% (o i vostri soldi indietro), ma in pratica la maggior parte dei linguaggi permette una certa quantità di Obj.magic.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.