Java Tutorial - Parte 1 0.1
Un tutorial per esempi
Caricamento in corso...
Ricerca in corso...
Nessun risultato
Java è un linguaggio veloce?

Introduzione

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:

  • misureremo le performances di Java confrontandole con un linguaggio (il linguaggio "C") ed uno interpretato (il linguaggio "Perl")
  • la organizzazione del progetto; finora abbiamo introdotto il concetto di package ma tutti i nostri sorgenti si limitavano ad importare le classi di package esterni; in questo capitolo impareremo a creare i nostri packeges
  • useremo le collections (=collezioni) invece delle arrays
  • lo sviluppo di una applicazione o di un progetto deve procedere per gradi; non è necessario implementare tutte le funzionalità e tutte le ottimizzazioni fin dal primo rilascio

I files del capitolo

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

I numeri primi

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:

public long getPrimesCount( long n )
{
long primesCount = 0;
for ( long i = 2; i <= n; i++ ) {
if ( isPrime( n )) {
primesCount++;
}
}
return primesCount;
}

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:

Il più antico e semplice test di primalità è quello di "divisione per tentativi", che consiste nell'applicare direttamente la definizione di numero primo: si prova a dividere il numero 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ì:

public boolean isPrime( long num )
{
long limit = (long) ( Math.sqrt( num ));
boolean r = true;
for ( long i = 2; i <= limit; i++ ) {
if ( num % i == 0 ) {
r = false;
break;
}
}
return r;
}

I tempi di Java

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:

  • interpreta l'argomento della command-line come un intero lungo (un long di Java) che rappresenta il numero n cioè il limite entro il quale conteggiare i numeri primi
  • ottiene il tempo di sistema in millisecondi prima e dopo l'operazione
  • visualizza il conteggio dei numeri primi ed il tempo impiegato in secondi

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

I tempi di Perl

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):

#
# Function: ritorna il conteggio dei numeri primi minori di
# o uguali al numero fornito come argomento
#
# $_[0]: il numero entro il quale contare i primi
# Return: il conteggio dei primi
sub get_primes_count
{
my $n = $_[0];
my $primesCount = 0;
for ( my $i = 2; $i <= $n; $i++ ) {
if ( is_prime( $i )) {
$primesCount++;
}
}
return $primesCount;
}

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.

I tempi del linguaggio C

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:

bool is_prime( int64_t num )
{
int64_t limit = (int64_t) ( sqrt( num ));
bool r = TRUE;
for ( int64_t i = 2; i <= limit; i++ ) {
if ( num % i == 0 ) {
r = FALSE;
break;
}
}
return r;
}

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":

#define bool int
#define FALSE 0
#define TRUE 1

Il build di un eseguibile

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:

  • preprocessing: la prima fase elabora le cosidette direttive del preprocessore; non sono vere istruzioni e non producono codice macchina
  • assembling: la seconda fase traduce il sorgente in un linguaggio ancora simbolico ma orientato alla macchina
  • compilation: la terza fase è la compilazione vera e propria che traduce il listato assembler in codice macchina nativo
  • linking: la quarta ed ultima fase esegue il collegamento tra tutti i sorgenti e le librerie

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.

I risultati del test

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