Von Neumann, un importantissimo computer scientist, disse che l’assembly era un errore e che per programmare il codice macchina era più che sufficiente. Per programmare i primi computer infatti esistevano solo linguaggio di programmazione di basso livello: ma cosa sono in realtà? E sopratutto, che differenza c’è con quelli ad alto livello?

Linguaggio di programmazione di basso livello

Il termine alto o basso livello, non si riferisce come si potrebbe erroneamente pensare alla qualità o alla potenza del linguaggio; bensì alla sua comprensibilità.

Un linguaggio si definisce a basso livello, quando le istruzioni sono più comprensibili per la macchina che per il programmatore.

Non a caso infatti, per basso ed alto livello ci si riferisce al grado di astrazione fra il linguaggio considerato (java, python etc) ed il linguaggio macchina (sequenza di 0 e 1).

Messi a confronto, i linguaggi a basso livello sono molto più difficili in quanto richiedono molti più parametri da conoscere e da tenere in considerazione rispetto a quelli ad alto livello: questo però, si traduce (di solito) anche in una minore richiesta di risorse e un minor peso nel programma finale.

Il linguaggio di programmazione a basso livello per eccellenza è l’assembly.

Linguaggi di programmazione ad alto livello

Questa categoria di linguaggi, è sicuramente più facile da imparare in quanto richiede meno conoscenze ed è inoltre più comprensibile: più avanti faremo un confronto con un esempio nei due linguaggi.

Come abbiamo già visto, quando diciamo alto livello ci riferiamo al grado di astrazione: questi linguaggi risultano infatti più comprensibili per il programmatore.

Questo non solo ci permette di facilitare il porting da un sistema operativo (o architettura) all’altro, ma inoltre facilità l’aggiornamento e la modifica dei programmi.

Programmando ad esempio in assembly dobbiamo conoscere fisicamente l’architettura del nostro computer e, dato il grado di complessità a cui siamo arrivati nello sviluppo di un software, non sempre si dimostra un compito facile.

Il confronto

Vediamo un’esempio di hello world di un linguaggio di programmazione ad alto livello, il C:

#include <stdio.h>
void main() {
    printf('Hello, World');
}

Lo stesso equivalente in assembly con sintassi intel (preso da wikipedia):

extern exit, printf

	section .data
	msg db "Hello World!", 10, 0

	section .text
	global main
main:
	push msg
	call printf

	mov dword [esp], 0
	call exit

Come è possibile notare, il secondo codice è decisamente più lungo e difficile nonostante la semplice operazione che vogliamo fare.

La differenza fra i due però è che, anche se questi programmi sono molto piccoli, (in generale) il secondo verrà eseguito più velocemente del primo.

Nei sistemi dove ogni istante è veramente importante (come per esempio nelle catene di montaggio) spesso viene scelto un linguaggio a basso livello proprio per la sua velocità – di certo non per la semplicità.

Perchè i linguaggi a basso livello sono in generale più performanti

Il programma che trasforma l’assembly in codice macchina viene chiamato assemblatore.

A meno di rari casi, una riga di codice assembly viene tradotta 1 a 1 con un insieme di bit predefiniti. Questo vuol dire che se abbiamo 6 righe di codice assembly, e sulla architettura per cui stiamo sviluppando un’istruzione occupa 32 bit, il nostro programma occuperà 6*32 bits.

Assumiamo che vogliamo scrivere un programma per calculare il prodotto di due matrici. Questo programma richiede 30 righe di assembly. Visto che il programma è lento, parliamo con un collega che ci consiglia di usare l’istruzione matr_mut disponibile sul sistema per cui stiamo sviluppando, che prende in input l’indirizzo di due matrici e ci ritorna il prodotto di queste. Fantastico! La moltiplicazione fra matrici viene implementata a livello hardware, quindi ora è velocissima!

Prendiamo lo stesso codice scritto in c. Il programma che prende in input codice c e produce in output un codice macchina eseguibile dal nostro sistema operativo viene chiamato compilatore. Il più famoso penso sia gcc.

Eseguendolo in questo modo gcc -S -masm=intel matr-mult.c è possibile vedere in output l’assembly prodotto.

Nel caso ottimo, gcc prenderà in input il nostro codice ad “alto livello” scritto in C, e produrrà in output un assembly lungo circa 30 istruzioni. Nel caso perfetto, gcc conosce l’esistenza di questa istruzione assembly matr_mult e quindi produrrà un codice lungo una sola istruzione!

Conclusione

Quando si scrive in un linguaggio ad alto livello, bisogna affidarsi alla bontà del compilatore nel produrre un codice macchina efficiente. Usando un linguaggio a basso livello, abbiamo più libertà di ottimizzare il codice in teoria.

In pratica però, a meno che non siate molto esperti dell’architettura per cui state sviluppando, il codice assembly prodotto dal compilatore sarà ottimizzato molto meglio di quanto riusciremmo a fare noi meno esperti nella stragrande maggioranza dei casi.

A volte non c’è modo di evitare di scrivere codice a basso livello come assembly, è per questo che di solito i linguaggi ad alto livello offrono la possibilità (ad esempio tramite macro) di scrivere codice assembly inline nel file.