|
Java Tutorial - Parte 1 0.1
Un tutorial per esempi
|
Come abbiamo imparato in I linguaggi, Java è un linguaggio semi-compilato nel quale un listato sorgente (il file .java viene tradotto in un codice intermedio detto bytecode (il file .class) che viene interpretato da una JVM (Java Virtual Machine) la quale converte al volo il bytecode in codice macchina nativo, comprensibile al microprocessore.
Dal momento che il bytecode deve essere interpretato prima di essere eseguito possiamo aspettarci che Java non sia una lepre in fatto di velocità; dobbiamo aspettarci quindi che un linguaggio compilato come il "C", che traduce il sorgente in codice macchina nativo, sia più veloce.
Questo è vero, ma la domanda è: quanto è più veloce un linguaggio compilato rispetto a Java? E quanto è più veloce un linguaggio semi-compilato come Java rispetto ad un linguaggio interpretato come, per esempio, il Perl? Non c'è che una soluzione a questa domanda: misurarne le performance!
In questo capitolo e nei due successivi imparerete alcuni concetti fondamentali che si applicano a molti linguaggi di programmazione, non solo a Java. Gli aspetti su cui ci concentreremo sono i seguenti:
Nella sottocartella javatutor1/projects/speed troverete i files sorgente di tutto il codice descritto in questo capitolo. Di seguito l'elenco dei files ed una loro breve descrizione:
| Nome del file | Descrizione |
|---|---|
| PrimesCount.java | la applicazione in linguaggio Java |
| primes_count.pl | la applicazione in linguaggio Perl |
| primes_count.c | la applicazione in linguaggio "C" |
| primes_count.exe | la applicazione in codice macchina nativo |
Uno dei compiti più difficili da portare a termine nell'ambito della matematica è quello di ottenere la lista o il conteggio di tutti i numeri primi minori o uguali ad un dato numero n:
P(n) = numeri_primi <= n
La distribuzione dei numeri primi nell'insieme dei numeri naturali è caotica e non esiste alcuna formula (vedi Formule per i numeri primi) che ci possa restituire il conteggio esatto di P(n) eccezion fatta per la funzione zeta di Riemann la quale però usa il piano complesso per determinare il risultato e della quale non si è ancora capito, a metà del 2025, il funzionamento (vedi i sette problemi del millenio ).
Al momento attuale, se vogliamo conoscere con precisione il conteggio di P(n) non abbiamo altra scelta che estrarre tutti i numeri P(n) e contarli. In sostanza, il codice per ottenere questo risultato è il seguente, assumendo che n possa essere un intero a 64 bits:
Il metodo suesposto si appoggia al metodo isPrime il quale ritorna true se il numero fornito come argomento è primo. Al momento attuale vi sono diversi algoritmi per stabilire se un numero è primo oppure no (vedi Test di primalità) ma sono, per la maggior parte dei casi, probabilistici: non ci danno la certezza assoluta che il numero in ingresso sia effettivamente primo. Java stesso fornisce il metodo BigInteger.isProbablyPrime che usa uno di questi metodi probabilistici per determinare se un numero intero è primo oppure no.
Ma non è l'algoritmo, quello che ci interessa. Almeno per il momento. Come riportato nel link suesposto:
n per tutti i numeri minori di n: se nessuno di questi lo divide, allora il numero è primo. Un semplice miglioramento di questo metodo si ottiene limitando i tentativi di divisione ai numeri primi minori della radice quadrata di n. Sebbene molto semplice da descrivere e da implementare su un calcolatore, tale metodo è poco usato nella pratica, perché richiede tempi di calcolo che aumentano esponenzialmente rispetto al numero delle cifre di n. Quindi questo è il problema ideale per misurare le performance di un linguaggio di programmazione ed è proprio ciò che faremo. In Java, il metodo per testare la primalità di un numero potrebbe essere scritto così:
Aprite il file sorgente PrimesCount.java nel vostro editor preferito e analizzate il codice per un breve momento. Oltre ai due metodi di cui sopra, il sorgente contiene un metodo main che:
n cioè il limite entro il quale conteggiare i numeri primi Aprite il Prompt dei comandi e spostatevi nella cartella succitata, compilate ed eseguite il programma con i seguenti argomenti: quattrocentomila, un-milione e cinque-milioni:
\javatutor1\projects\speed>javac PrimesCount.java \javatutor1\projects\speed>java -ea PrimesCount 400000 \javatutor1\projects\speed>java -ea PrimesCount 1000000 \javatutor1\projects\speed>java -ea PrimesCount 5000000
Nella tabella seguente vengono riportati i risultati del test; cioè il tempo impiegato a concludere l'operazione dai tre linguaggio presi in esame:
| limite | conteggio | Java | C | Perl |
|---|---|---|---|---|
| 400.000 | 33.860 | 0.2 | ||
| 1.000.000 | 78.498 | 0.6 | ||
| 5.000.000 | 348.513 | 5.4 | ||
| 10.000.000 | 664.579 | 14.2 |
Aprite il file sorgente primes_count.pl nel vostro editor preferito e date un breve sguardo al codice. Poichè questo non è un tutorial sul Perl, mi limito ad un breve commento di uno spezzone di codice del programma scritto in Perl e, nello specifico, di una delle funzioni (o procedure, che dir si voglia):
Il lettore avrà sicuramente notato le grandi differenze tra Java e Perl. In primis, Perl è un linguaggio non tipizzato: le variabili n e primesCount vengono definite senza alcun tipo di dato. Infatti, in Perl una variabile può contenere qualsiasi tipo di dato, sia numerico che stringa. L'attributo my scritto prima del nome variabile indica all'interprete che la variabile è locale.
In secondo luogo, noterete che la procedura (il corrispondente di un metodo Java) viene definita tramite la parola chiave sub ma non possiede nè un tipo di ritorno nè una lista di argomenti: in Perl, infatti, è sufficente ritornare un valore da una procedura con la parola chiave return. Gli argomenti alla procedura vengono memorizzati dall'interprete in una array di nome underscore e la procedura può essere richiamata con qualsiasi numero di argomenti dal chiamante.
Per eseguire il programma Perl non è necessaria alcuna compilazione: il programma sorgente viene semplicemente passato come argomento all'interprete (sempre se lo avete installato, ovviamente! vedi Strumenti necessari allo sviluppo)
>perl.exe primes_count.pl 400000 Count primes less than or equal to: 400000 Count of primes is: 33860 time elapsed: 2 seconds
Notiamo subito che il Perl è davvero una lumaca, nel confronto con Java. Questi sono i risultati:
| limite | conteggio | Java | C | Perl |
|---|---|---|---|---|
| 400.000 | 33.860 | 0.2 | 2.0 | |
| 1.000.000 | 78.498 | 0.6 | 9.0 | |
| 5.000.000 | 348.513 | 5.4 | 81.0 | |
| 10.000.000 | 664.579 | 14.2 |
Non ho avuto il coraggio di eseguire il test con n uguale a 10-milioni poichè il tempo di attesa sarebbe davvero enorme, probabilmente intorno ai 250 secondi.
Infine, proviamo i tempi di un linguaggio compilato: il linguaggio "C". Se volete dare una rapida occhiata al listato sorgente, aprite il file primes_count.c
Poichè questo non è un tutorial sul "C", mi limito ad un breve commento di uno spezzone di codice e, nello specifico, di una delle funzioni:
Il linguaggio "C" assomiglia molto a Java: la sintassi generale è la stessa anche se il tipo di dato che si riferisce ad un intero lungo non è long come in Java bensì int64_t. Questo perchè il linguaggio "C" non ha dei veri e propri standard nei tipi di dato primitivi dal momento che int può riferirsi sia ad interi a 16 bits che a 32 bits, dipende dalla piattaforma.
Analogamente, il tipo long del "C" può riferirsi sia ad interi a 32 bits che a 64 bits: per questo motivo si sono definiti degli standards multipiattaforma che non lasciano spazio a dubbi: il tipo int64_t si riferisce ad interi a 64 bits.
Una altra differenza tra il "C" e Java è che in "C" il tipo booleano non esiste e non esistono nemmeno costanti mnemoniche come TRUE e FALSE nè true e false.
E' possibile, però, simulare tipi inesistenti e le costanti mnemoniche con una speciale direttiva del preprocessore. Ecco come si fà in "C":
Un programma compilato è totalmente diverso da un programma interpretato o semi-compilato; il primo non ha bisogno di un interprete o di una macchina virtuale per essere eseguito; esso viene eseguito direttamente perchè contiene codice nativo, direttamente comprensibile al microprocessore.
Quando compiliamo un programma in un linguaggio compilato otteniamo un eseguibile che si riconosce facilmente perchè il nome del file ha la estensione .exe in Windows; in altri sistemi operativi questo non è vero.
Quando compiliamo il sorgente Java otteniamo un file .class che non è direttamente eseguibile sulla nostra macchina ed infatti, per mandarlo in esecuzione dobbiamo prima eseguire la JVM:
>java [OPTIONS] PrimesCount
Nel comando di cui sopra l'eseguibile è il java.exe che, se guardate nella cartella dove avete installato il OpenJDK lo trovate nella sottocartella bin/. Se non ricordate dove lo avete installato rileggete Strumenti necessari allo sviluppo. Il programma da eseguire PrimesCount non è un eseguibile ma, invece, un file .class che viene si eseguito, ma non direttamente dal microprocessore.
Un programma eseguibile, invece, viene mandato in esecuzione direttamente:
>primes_count.exe 1000000
Questa è la vera, grande differenza tra i linguaggi compilati e quelli interpretati e/o semi-compilati: il programma primes_count.exe contiene codice nativo, non è necessario un interprete, nè una Virtual Machine e nemmeno ... udite udite ... il compilatore "C"!
Questo perchè il programma primes_count.exe è già compilato, lo ho fatto io stesso e può essere mandato in esecuzione direttamente. Vi sembra un grande vantaggio rispetto a Java o Perl? Non è così, credetemi. Lo svantaggio di primes_count.exe è che è stato compilato su una piattaforma Windows 64-bits per una piattaforma dello stesso tipo: non funzionerà mai su Apple, o su Linux e nemmeno su Windows a 32-bits se non ricompilandolo.
Invece, il file PrimesCount.class funzionerà su qualsiasi macchina dotata di un JRE, il Java Runtime Environment
Per compilare un sorgente scritto in linguaggio "C" è necessario un compilatore "C"; nella sezione Strumenti necessari allo sviluppo viene spiegato come installare la GNU compiler collection (GCC). Il file primes_count.exe è un eseguibile cosidetto statically linked (=collegato staticamente) e non ha bisogno nè del compilatore nè delle librerie standard del linguaggio "C" ma se avete installato il GCC e volete cimentarvi nella operazione, continuate a leggere altrimenti passate direttamente a I risultati del test.
Per prima cosa dobbiamo dare i nomi giusti alle operazioni che andremo a svolgere; mentre i neofiti si riferiscono al processo di ottenimento di un eseguibile da un linguaggio compilato con il termine di compilazione, gli addetti ai lavori preferiscono il termine di build (=costruzione) poichè la compilazione è solo una delle quattro fasi del processo:
Per eseguire il build del programma primes_count spostatevi nella sottocartella javatutor1/projects/speed e compilate (o meglio, "buildate"):
> gcc -O3 -o primes_count.exe primes_count.c
Il comando suesposto produce il file primes_count.exe che è, come potete ben immaginare, l'eseguibile che ci serve. Tutto qui? Lo so che il comando vi sembra particolarmente semplice ma non fatevi ingannare dalle apparenze; il build di un eseguibile è un processo piuttosto complicato e la apparente facilità con cui lo abbiamo ottenuto è solo una coincidenza: vi è un solo file sorgente e non abbiamo avuto bisogno di librerie particolari.
Nella tabella sottostante ho riepilogato i risultati dei test. I tempi riportati sono stati eseguiti su un PC All-In-One equipaggiato con un processore Intel Core i5-6500 @ 3.20GHz e 16 GB RAM DDR4:
| limite | conteggio | Java | C | Perl |
|---|---|---|---|---|
| 400.000 | 33.860 | 0.2 | 0.2 | 2.0 |
| 1.000.000 | 78.498 | 0.6 | 0.5 | 9.0 |
| 5.000.000 | 348.513 | 5.4 | 5.1 | 81.0 |
| 10.000.000 | 664.579 | 14.2 | 13.9 |
Come il lettore avrà sicuramente notato, le prestazioni di Java sono di tutto rispetto, non lontane da quelle ottenute con il linguaggio nativo; quest'ultimo è risultato di appena il 3% più veloce di Java.
Vi è però da osservare che la differenza di prestazioni così irrisoria è anche dovuta alla estrema semplicità dell'algoritmo usato per il conteggio dei numeri primi. Usando algoritmi più sofisticati, specialmente quelli che usano le array, la differenza di prestazioni è più marcata, arrivando anche al 30%. Ma questo è un aspetto che vedremo in seguito.
Argomento precedente - Argomento successivo - Indice Generale