Per quanto oggi appaia desueto programmare in Fortran, esisterà sempre un vecchio codice da tradurre. Vediamo come gestire il problema sul PI.
Fortran è acronimo di Formula Translator, e rappresenta un linguaggi0 di programmazione: compilato e imperativo, particolarmente adatto per il calcolo numerico e la scienza computazionale.
Si tratta di uno dei primi linguaggi di programmazione, sviluppato a partire dal 1954 da un gruppo di lavoro guidato da John Backus; Il primo compilatore FORTRAN fu sviluppato a partire dal 1954 per il calcolatore IBM 704.
Un po’ di storia
Diverse sono le versioni Fortran apparse: il FORTRAN I nel 1957, il FORTRAN II nel 1958, il FORTRAN III nel 1958 (usato da una ventina di clienti dell’IBM, ma mai pubblicato come prodotto commerciale per la sua mancanza di portabilità), il FORTRAN IV nel 1961 (la versione di maggiore uso e diffusione), il FORTRAN 66 nel 1966, il FORTRAN 77 nel 1977, il Fortran 90 nel 1990, il Fortran 95 nel 1995, il Fortran 2003 nel 2003 e il Fortran 2008 nel 2008. In particolare il Fortran 90 è il primo a riconoscere la programmazione strutturata a blocchi e la ricorsione, consentendo al linguaggio di abbandonare il nomignolo di “spaghetti Fortran” che il codice si era meritato a causa deile numerose istruzioni goto utilizzate.
Oggi il Fortran è stato quasi del tutto abbandonato; come il COBOL, sopravvive in ambienti ristretti e asettici, su applicazioni accademiche di calcolo scientifico o parallelo, su schede acceleratrici speciali e in laboratori di calcolo. Ma dal momento che molti benchmark per computer furono inizialmente progettati in Fortran, potrebbe essere interessante valutare le prestazioni di una macchina utilizzando gli stessi parametri di valutazione, anziché ricorrere ad una versione di codice tradotta (e ottimizzata) in C che i primi computer non potevano permettersi.
Tra i packages rilasciati con ciascuna versione di Linux è fortunatamente presente il programma gfortran, che al pari del gcc si occupa della compilazione di codice specifico. Ovviamente sia Raspbian che Ubuntu dispongono di una versione portabile per processore ARM che oggi andremo ad esaminare.
Installazione di gfortran
Per installare il compilatore GNU Fortran sul Raspberry PI occorre seguire pochi semplici passaggi. Ovviamente il pacchetto è preso dal repository, quindi la procedura vale sia per il Raspberry OS che per le macchine con Ubuntu.
Apriamo un terminale e allineiamo il software caricato con il comando
1 2 |
sudo apt update sudo apt upgrade |
Ora che abbiamo aggiornato il sistema con le ultime librerie, possiamo scaricare il compilatore Fortran dal repository:
1 |
sudo apt install gfortran |
Voila. Siamo pronti ad iniziare. Per sicurezza, controlliamo che l’installazione sia andata a buon fine con il comando
1 |
gfortran -v |
che ci offrirà la versione, le opzioni di compilazine utilizzate per il pacchetto, le variabili di ambiente utilizzate, la configurazione ed il compilatore.
Hello world!
E’ giunto il momento di scrivere il nostro primo programma. Come nelle migliori famiglie, anche in questo caso scrivere un “hello world” in Fortran è il modo migliore per iniziare.
Apriamo il nostro editor di fiducia, e scriviamo il seguente codice:
1 2 3 4 |
program hello ! This is a comment line; it is ignored by the compiler print *, 'Hello, World!' end program hello |
Salviamo il file con il nome “hello.f90” e compiliamo con
1 |
gfortran hello.f90 -o hello |
quindi lanciamo il programma con
1 |
./hello |
Perché l’estensione .f90 anziché .f?
Come abbiamo detto, il Fortran ha 70 anni di storia e di evoluzioni, ma la stragrande maggioranza di codice che gira risale alle versioni FortranIV/Fortran77. Il Fortran90 ha iniziato a diffondersi dopo la presentazione di linguaggi strutturati (a blocchi) come ADA, Pascal e C, ma quasi nessuno si è preso la briga di ritradurre la vecchia base di codice sulla nuova piattaforma di compilazione, seguendo l’adagio “ciò che funziona non si cambia”.
Per convenzione, la maggior parte degli attuali compilatori Fortran selezionano lo standard di linguaggio da utilizzare interpretando il suffisso del file contenente il codice sorgente: FORTRAN 77 per .f (o meno comunemente .for), Fortran 90 per .f90, Fortran 95 per .f95. Altri standard, qualora supportati, possono essere selezionati sulla linea di compilazione attraverso il flag -std=fxx (ad esempio std=f95).
Per i più curiosi, ecco come appariva lo stesso programma in Fortran77:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
program main c*********************************************************************72 c cc MAIN is the main program for HELLO. c c Discussion: c c HELLO is a simple FORTRAN77 program that says "Hello, world!". c c Licensing: c c This code is distributed under the GNU LGPL license. c c Modified: c c 18 May 2009 c c Author: c c John Burkardt c implicit none write ( *, '(a)' ) ' Hello, world!' stop end |
In cui c rappresenta una “scheda commento” del linguaggio (anche il Fortran utilizzava le schede perforate per la programmazione…).
Salvate il programma con hello.f compilate come sopra ed eseguite:
1 2 3 |
gfortran hello.f -o hello1 ./hello1 |
In questo modo avremo due file sorgente, indicati dall’estensione .f e .f90 ed i relativi eseguibili hello ed hello1.
Per i più curiosi, che volessero conoscere quali siano i comandi intrinsic della libreria di base, suggeriamo di leggere questo articolo e compilarne gli esempi.
Programmi più impegnativi
Ora che abbiamo imparato come compilare semplici programmi in Fortran, vediamo qualche esempio.
Il seguente programma, scritto in Fortran77, calcola il Massimo Comun Divisore utilizzando l’algoritmo di Euclide:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
* euclid.f (FORTRAN 77) * Find greatest common divisor using the Euclidean algorithm PROGRAM EUCLID PRINT *, 'A?' READ *, NA IF (NA.LE.0) THEN PRINT *, 'A must be a positive integer.' STOP END IF PRINT *, 'B?' READ *, NB IF (NB.LE.0) THEN PRINT *, 'B must be a positive integer.' STOP END IF PRINT *, 'The GCD of', NA, ' and', NB, ' is', NGCD(NA, NB), '.' STOP END FUNCTION NGCD(NA, NB) IA = NA IB = NB 1 IF (IB.NE.0) THEN ITEMP = IA IA = IB IB = MOD(ITEMP, IB) GOTO 1 END IF NGCD = IA RETURN END |
Il programma principale si occupa di richiedere i dati e di testarne la validità, quindi chiama la funzione NGCD, che cicla sin quando non si ottiene il risultato desiderato. Come nel C, la funzione restituisce un valore che viene stampato nel main. Notare la sintassi dei confronti (ad esempio NB.LE.0 per less or equal) e la gestione dell’input tramite READ.
Un altro esempio in Fortran77, un po’ più “complesso”, utilizza i numeri complessi come tipo di dato implicito:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
* cmplxd.f (FORTRAN 77) * Demonstration of COMPLEX numbers * * Prints the values of e ** (j * i * pi / 4) for i = 0, 1, 2, ..., 7 * where j is the imaginary number sqrt(-1) PROGRAM CMPLXD IMPLICIT COMPLEX(X) PARAMETER (PI = 3.141592653589793, XJ = (0, 1)) DO 1, I = 0, 7 X = EXP(XJ * I * PI / 4) IF (AIMAG(X).LT.0) THEN PRINT 2, 'e**(j*', I, '*pi/4) = ', REAL(X), ' - j',-AIMAG(X) ELSE PRINT 2, 'e**(j*', I, '*pi/4) = ', REAL(X), ' + j', AIMAG(X) END IF 2 FORMAT (A, I1, A, F10.7, A, F9.7) 1 CONTINUE STOP END |
X viene definito complesso, e viene acceduto come REAL(X) e AIMG(X). Il costrutto IF viene utilizzato per decidere se porre il segno meno (“-“) alla parte immaginaria. Notare anche i due goto impliciti contraddistinti dalle label 1 e 2.
L’output del programma è rappresentato dalla tabella seguente:
1 2 3 4 5 6 7 8 |
e**(j*0*pi/4) = 1.0000000 + j0.0000000 e**(j*1*pi/4) = 0.7071068 + j0.7071068 e**(j*2*pi/4) = 0.0000000 + j1.0000000 e**(j*3*pi/4) = -0.7071068 + j0.7071068 e**(j*4*pi/4) = -1.0000000 - j0.0000001 e**(j*5*pi/4) = -0.7071066 - j0.7071069 e**(j*6*pi/4) = 0.0000000 - j1.0000000 e**(j*7*pi/4) = 0.7071070 - j0.7071065 |
Programmazione parallela in Fortran
E passiamo a qualcosa di più “tosto”, utilizzando le features del Fortran90.
Scriveremo innanzi tutto un semplice programma per la gestione delle matrici:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
PROGRAM Matrice !-------------------------------------------------------------------------- ! Lettura e scrittura di una matrice !-------------------------------------------------------------------------- IMPLICIT NONE REAL,DIMENSION(10,10) :: mat ! L'attributo DIMENSION dichiara una matrice (qui di 10x10 elementi) ! DEFINIZIONI: ! Rango dell'array (rank): numero di indici (2 in questo caso) ! Estensione di un indice (extent): n. di valori ammessi per l'indice ! Forma dell'array (shape): sequenza delle estensioni (qui 10x10) ! Ampiezza dell'array (size): numero di elementi (qui 100) ! Un matrice viene memorizzata per colonne, cioe' le colonne si succedono ! ordinatamente nelle parole di memoria. INTEGER :: N, M, I WRITE(*,*) "Dimensioni della matrice? " READ(*,*) N, M WRITE(*,*) "Dammi la matrice per righe: " DO I=1,N READ(*,*) mat(I,1:M) ! mat(I,1:M): sezione di matrice, in questo caso la riga I-esima. ! La sintassi 1:M indica tutti gli interi fra 1 ed M. ! Gli indici degli elementi e delle sezioni vanno racchiusi tra ! parentesi tonde, le uniche che il Fortran conosca. ! NOTA: l'istruzione precedente legge singole righe, la cui ! introduzione deve pertanto essere terminata con INVIO. END DO WRITE(*,*) "La matrice e': " DO I=1,N ! Scrivi la matrice per righe WRITE(*,*) mat(I,1:M) END DO STOP END PROGRAM Matrice |
I due cicli DO…END DO contengono l’input e l’output. Il tipo mat() predefinito consente di operare in automatico per righe senza necessità di nidificazione.
Ora che sappiamo come operare su vettori e matrici, vediamo come scrivere il classico crivello di Eratostene per la riceca di numeri primi.
Nella prima versione sequenziale vettorizzata avremo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
PROGRAM EratosteneS !-------------------------------------------------------------------------- ! Crivello di Eratostene per la ricerca dei numeri primi. ! Per ogni numero inizialmente presente nel crivello, ne elimina i ! multipli. Alla fine della ricerca, il crivello contiene i soli primi. ! Versione sequenziale con alcune istruzioni vettoriali. ! NOTA. Il ciclo DO indica che si tratta di un programma sequenziale. !-------------------------------------------------------------------------- IMPLICIT NONE INTEGER :: I & ! intero di servizio , N ! limite di ricerca ! Insiemi rappresentati mediante funzione caratteristica binaria: ! l'indice del vettore corrisponde al numero, il valore ne indica la ! presenza LOGICAL,ALLOCATABLE,DIMENSION(:):: crivello ! insieme di ricerca WRITE(*,*) "Dammi il limite di ricerca dei numeri primi: " READ(*,*) N !-- Alloca ed inizializza il vettore ALLOCATE (crivello(2:N)) crivello = .TRUE. ! Riempi il crivello ! Assegnazione vettoriale: uno scalare e' conforme a qualunque array !-- Estrai i numeri primi DO I=2,N/2 ! Per tutti gli interi fino ad N/2... IF (.NOT.crivello(I)) CYCLE ! ... ancora presenti nel crivello... ! L'istruzione CYCLE interrompe l'iterazione corrente crivello(2*I:N:I)=.FALSE. ! ... togli tutti i loro multipli ! Sezione di vettore con passo (stride): estrae gli elementi di ! crivello fra 2*I ed N che distano I posizioni fra loro. END DO !-- Stampa i primi WRITE(*,*) "I primi sono:" ! Tra tutti gli interi, stampa solo i primi WRITE(*,*) PACK((/(I,I=2,N)/), crivello) ! PACK: f. intrinseca vettoriale, estrae gli elementi del primo argom. ! che corrispondono al valore vero del secondo argomento (maschera). ! E' richiesta la conformita' degli argomenti. STOP END PROGRAM EratosteneS |
A parte la sintassi un po’ criptica, il senso è abbastanza chiaro: le operazioni che agiscono sui vettori vengono automaticamente parallelizzate ove possibile.
Si può fare di meglio? Certamente! Il codice che segue sostituisce l’isctruzione DO con una FORALL, che si occupa di parallelizzare la ricerca vettoriale su più vettori (numeri, in questo caso) contemporaneamente, rendendo peraltro il sorgente più compatto.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
PROGRAM EratosteneP !-------------------------------------------------------------------------- ! Crivello di Eratostene per la ricerca dei numeri primi ! Per ogni numero inizialmente presente nel crivello, ne elimina i ! multipli. Alla fine della ricerca, il crivello contiene i soli primi. ! Versione completamente vettoriale (parallela). ! NOTA. Rispetto alla versione sequenziale, il ciclo DO e' stato sostituito ! dal costrutto FORALL. !-------------------------------------------------------------------------- IMPLICIT NONE INTEGER :: I & ! intero di servizio , N ! limite di ricerca ! Insiemi rappresentati mediante funzione caratteristica binaria: ! l'indice del vettore corrisponde al numero, il valore ne indica la ! presenza. LOGICAL,ALLOCATABLE,DIMENSION(:):: crivello ! insieme di ricerca WRITE(*,*) "Dammi il limite di ricerca dei numeri primi: " READ(*,*) N !-- Alloca ed inizializza il vettore ALLOCATE (crivello(2:N)) crivello = .TRUE. ! Riempi il crivello !-- Estrai i numeri primi FORALL (I=2:N/2) ! Per tutti gli interi fino ad N/2... crivello(2*I:N:I)=.FALSE. ! ... togli i loro multipli dal crivello END FORALL !-- Stampa i primi WRITE(*,*) "I primi sono:" WRITE(*,*) PACK((/(I,I=2,N)/), crivello) STOP END PROGRAM EratosteneP |
Considerazioni finali
Abbiamo visto come installare un compilatore Fortran sul nostro Raspberry PI, e iniziare a lavorarci in modo praticamente seamless. Il PI, con i suoi 4 cores utilizzabili, si dimostrerà un’ottima palestra per chiunque vorrà cimentarsi in modo più approfondito. Ma ovviamente non è tutto. Dal momento che il gfortran che abbiamo utilizzato è un package del repository di Ubuntu Linux, possiamo aggiungere le librerie MPI (Messasge Passing Interface) e creare un cluster di Raspberry programmabili per eseguire task distribuiti e/o paralleli in architettura NUMA, aggiungendo moduli ogniqualvolta sia necessaria maggior potenza. E dal momento che anche i SBC della serie Nvidia Jetson sono basati su Ubuntu (o possono caricarlo), nulla ci vieta di estendere le potenzialità di questi oggettini alle accelerazioni CUDA, utilizzando 128, 256 o 384 core paralleli come sistemi di calcolo attraverso le API di programmazione: infatti, oltre alle note interfacce in C, Nvidia rende disponibili le operazioni CUDA anche ai programmatori Fortran, con evidenti vantaggi dal punto di vista della gestione di codice complesso.
Niente male, vero?
Join our groups on Telegram…