Category Archives: Java

Il linguaggio Java

Sul Java che batteva il C (compiler strikes back again)

coffee-309981_640

Il caso trattato nell’articolo Compilati VS interpretati passo 2 (The compiler strikes back) è intrigante: siamo davvero di fronte a uno di quei pochi casi in cui un linguaggio che non viene compilato riesce a battere uno compilato?

Sembrerebbe proprio di sì.

Però ricordiamoci che non è che la JVM realmente interpreta i bytecode in modo “stupido”: c’è la compilazione Just In Time (JIT) e la JVM è tanto ben fatta da poter profilare il codice a runtime per poter migliorare le ottimizzazioni in fase di compilazione JIT…

Questo potrebbe spiegare perché Java sembra battere il C. (Non prendo in considerazione il Python, e nemmeno l’Objective-C per il quale, comunque, si può fare un discorso simile a quello del C).

Passo passo, sperimentando e provando, andiamo a scoprire come stanno le cose…

Elucubrazioni e ipotesi

Intanto nel codice C ho cambiato long double in double: il double in Java è un “64-bit IEEE 754 floating point”, come il double del C. Invece il long double può essere di più. Comunque non cambia praticamente nulla: le performance continuano ad essere inferiori a quelle del Java.

Quello che ho pensato è questo: in Java l’ottimizzatore in qualche modo si accorge che tutti i calcoli da un certo punto in poi non possono che dare infinito (si va oltre le possibilità del double), per cui il ciclo più interno può essere prematuramente interrotto. Oppure: è inutile eseguire la moltiplicazione se f è già diventanto “infinito”.

Se aggiungiamo a mano questa ottimizzazione, il C torna a brillare (dovete includere math.h):

        for(a = 1; a <= i; a++)
        {
            f *= a;
            if (isinf(f)) break;
        }

sh01

Si comincia a ragionare…

Oppure

        for(a = 1; a <= i; a++)
        {
            if (!isinf(f)) {
                f *= a;
            }
        }

Non l’ho provata ma possiamo dire che molto probabilmente sarà più lenta della versione con il break. Il vantaggio di questa versione è che permette l’esecuzione di altro codice nel caso in cui il corpo del ciclo non fosse costituito solo da f *= a.

(Ho fatto anche un’altra modifica: ho usato difftime per avere il numero di secondi… però non serve a molto quando il tutto dura meno di un secondo, e perciò ho messo time sulla linea di comando)

Si può fare di meglio: interrompere il ciclo esterno. Infatti, raggiunto un certo valore di i (chiamiamolo iMax), tutti quelli maggiori faranno in modo che il ciclo interno porti f a “inf”. Un essere umano ci arriva facilmente; ci può arrivare anche un ottimizzatore? Il codice ottimizzato avrebbe, come equivalente di alto livello C, l’aspetto seguente:

    for (i = 0; i <= iMax; i++) {
        double f = 1;
        int a;
 
        for(a = 1; a <= i; a++)
        {
            f *= a;
        }

        nArray[i] = i * f;
    }
    for (i = iMax + 1; i < MAXLOOP; i++) {
        nArray[i] = INFINITY;
    }

Intermezzo: un’idea sciocca

La JVM è stack based. Può essere che un modello simile in certe circostanze risulti più efficiente di uno register based? L’idea è sciocca perché non c’è modo che del codice ben ottimizzato eseguito da un hardware possa essere battuto da un interprete software (che gira sullo stesso hardware).

Ma non è proprio il caso che stiamo analizzando? No. Infatti stiamo cercando il trucco perché la tesi è che ci sia; poiché non lo vediamo, ci sembra che Java vada meglio… Ma in realtà è come se stessimo paragonando le performance del bubble sort scritto in C e Java, senza sapere che Java dietro le quinte lo sostituisce con un quick sort… (Il C non farebbe mai di questi scherzi:-D)

È possibile che le CPU stack based permettano ottimizzazioni migliori di quelle register based? Secondo questo post no:

Another advantage of the register based model is that it allows for some optimizations that cannot be done in the stack based approach. One such instance is when there are common sub expressions in the code, the register model can calculate it once and store the result in a register for future use when the sub expression comes up again, which reduces the cost of recalculating the expression.

Quindi non è che partendo dai bytecode Java si possa generare codice migliore… altrimenti i compilatori userebbero una rappresentazione intermedia stack based nella fase di ottimizzazione.

Comunque, prima di arrivare a realizzare quanto l’idea fosse sciocca, mi sono cimentato nel gioco che vedete nell’immagine.

sh02

Il disassemblato della classe (a destra nell’immagine) si ottiene con javap -c. Tutte le funzioni che vedete a sinistra hanno inline e lavorano su uno “stack” (un array) di strutture contenenti un enum che specifica il tipo (intero, double, array…) e una union.

Inutile dire che i tempi peggiorano…

real        0m6.052s
user        0m6.028s

Il codice lo potete trovare sul mio repository. (Prima o poi cambierà un po’: devo aggiungere un riferimento al post, riordinare e ripulire e commentare ecc.)

gcj

Proviamo ad usare gcj per generare un vero e proprio eseguibile da sol01.java1.

gcj -O3 --main=sol01 sol01.java

sh03

I tempi sono più o meno quelli del C. A questo punto togliamo --main e mettiamo -S per vedere il codice generato. Nessuna sorpresa: a meno di un controllo sull’indice dell’array (per poter throware BadArrayIndex) e qualche altro dettaglio, assomiglia tremendamente a quello generato dalla versione C… del resto il compilatore sotto il cofano è lo stesso.

Dubbio: possibile che il gcc non implementi delle ottimizzazione che invece una VM è in grado di fare a runtime al momento della compilazione JIT?

(Per cercare di essere breve non lo metto :-D, ma ho fatto pure la prova con --profile-generate / esecuzione / --profile-use, giro di giostra che dovrebbe permettere al gcc di fare alcune ottimizzazione eventuali in più… Se non ho sbagliato qualcosa… Niente da fare…)

Sembra quasi che altri marchingegni che compilano Just In Time riescano a capire qualcosa che il gcc non capisce.

Tuttavia, non abbiamo finito di esplorare le possibilità…

clang!

Non ci stiamo dimenticando di qualcosa? gcc è un ottimo compilatore, ma non è l’unico. Proviamo con clang.

sh04

Sorpresa!

E uno sguardo al codice assembly generato rivela qual è il trucco della JVM (e di clang e di pypy e di…): sfruttare meglio le possibilità del processore della macchina che eseguirà materialmente il codice compilato JIT!

Sembra che il gcc di default preferisca generare codice “generico”.

Torniamo a gcc, ma questa volta aggiungiamo qualche opzione di compilazione e riproviamo:

sh05

Bingo.


  1. Per non so quale motivo avevo salvato il file come sol01.java; poi, invece di rinominarlo in base al nome della classe, ho rinominato la classe… Quando si fanno le cose con criterio…

Ulteriori confronti, anche con C++


Il colpevole torna sempre sul luogo del delitto, Sherlock Holmes insegna. E allora eccomi di nuovo a confrontare le performance dei linguaggi, continuazione del post Ancora confronti sui linguaggi.

Walter, dikiyvolk, ha tradotto la versione Java in C (c’è un controllo sbagliato alla riga 54), così:

#include <stdio.h>
#include <math.h>

static double f[14];

static double fsin(double a) {
    return a - pow(a, 3.0) / f[3] + pow(a, 5.0) / f[5]
             - pow(a, 7.0) / f[7] + pow(a, 9.0) / f[9]
             - pow(a, 11.0) / f[11] + pow(a, 13.0) / f[13];
}

static double fcos(double a) {
    return 1.0 - pow(a, 2.0) / f[2] + pow(a, 4.0) / f[4]
               - pow(a, 6.0) / f[6] + pow(a, 8.0) / f[8]
               - pow(a, 10.0) / f[10] + pow(a, 12.0) / f[12];
}

static double myln(double x) {
    if (x == 0.0) {
        return -1.0e20;
    } else {
        double r = (x - 1.0) / (x + 1.0);
        return 2.0 * (r + pow(r, 3.0) / 3.0
            + pow(r, 5.0) / 5.0
            + pow(r, 7.0) / 7.0
            + pow(r, 9.0) / 9.0);
    }
}

static double ln10 = myln(10.0);

static double mylog(double x) {
    return x / ln10;
}

int main(int argc, char **argv) {
    int i, j;
    f[0] = 1.0;
    for (i = 1; i < 14; i++)
        f[i] = i * f[i - 1];

    ln10 = myln(10.0);
    int deg = 60 * 60;
    int nsteps = 180 * deg;
    double step = M_PI / nsteps;
    double ssum;
    double csum;
    double a, s, c, t = 0.0;
    double ls, lc;

    for (j = 1; j < 11; j++) {
        ssum = 0.0;
        csum = 0.0;
        for (i = 0; i <= nsteps; i++) {
            a = i * step;
            s = fsin(a);
            ls = mylog(myln(s));
            ssum += s;
            c = fcos(a);
            lc = mylog(myln(c));
            csum += c;

            if ((i % (10 * deg)) == 0) {
                if (c != 0.0)
                    t = s / c;

                printf("%3d %11.8f %11.8f %11.8f %15.8e\n",
                       (i / deg), a, s, c, t);
                printf(" %15.8e %15.8e\n", ls, lc);
            }
        }
    }
    printf("%15.8e\n", ssum);
    printf("%15.8e\n", csum);
}


Come si vede i risultati non sono quelli sperati: incredibilmente lento, 3.55 volte quello del Fortran! C’è qualcosa che non va, sicuro.

G. Lipari ottiene risultati simili. Ma, ricordate i classici: “Quando il gioco si fa duro, i duri cominciano a giocare” (“When the goin’ gets tough the tough get goin'”), Animal House 1978.

E difatti trova il colpevole e propone la soluzione per velocizzare l’esecuzione. La funzione pow() usa double anche per l’esponente, nel caso in esame questo è sempre intero per cui sostituendo tutte le pow() con mypow() definita così definita

inline double mypow(double x, int y) {
    double r = 1;
    for (int i=0; i<y; i++) r *= x;
    return r;
}

Eh sì, semplice adesso che la vedi! xy è nient’altro che x moltiplicato per sé stesso y volte 🙂

si ha:

#include <stdio.h>

#ifndef M_PI
#    define M_PI 3.14159265358979323846
#endif

static double f[14];

inline double mypow(double x, int y) {
    double r = 1;
    for (int i=0; i<y; i++) r *= x;
    return r;
}

static double fsin(double a) {
    return a - mypow(a, 3) / f[3] + mypow(a, 5) / f[5]
             - mypow(a, 7) / f[7] + mypow(a, 9) / f[9]
             - mypow(a, 11) / f[11] + mypow(a, 13) / f[13];
}

static double fcos(double a) {
    return 1.0 - mypow(a, 2) / f[2] + mypow(a, 4) / f[4]
               - mypow(a, 6) / f[6] + mypow(a, 8) / f[8]
               - mypow(a, 10) / f[10] + mypow(a, 12) / f[12];
}

static double myln(double x) {
    if (x == 0.0) {
        return -1.0e20;
    } else {
        double r = (x - 1.0) / (x + 1.0);
        return 2.0 * (r + mypow(r, 3) / 3.0
            + mypow(r, 5) / 5.0
            + mypow(r, 7) / 7.0
            + mypow(r, 9) / 9.0);
    }
}

static double ln10 = myln(10.0);

static double mylog(double x) {
    return x / ln10;
}

int main(int argc, char **argv) {
    int i, j;
    f[0] = 1.0;
    for (i = 1; i < 14; i++)
        f[i] = i * f[i - 1];

    ln10 = myln(10.0);
    int deg = 60 * 60;
    int nsteps = 180 * deg;
    double step = M_PI / nsteps;
    double ssum;
    double csum;
    double a, s, c, t = 0.0;
    double ls, lc;

    for (j = 1; j < 11; j++) {
        ssum = 0.0;
        csum = 0.0;
        for (i = 0; i <= nsteps; i++) {
            a = i * step;
            s = fsin(a);
            ls = mylog(myln(s));
            ssum += s;
            c = fcos(a);
            lc = mylog(myln(c));
            csum += c;

            if ((i % (10 * deg)) == 0) {
                if (c != 0.0)
                    t = s / c;

                printf("%3d %11.8f %11.8f %11.8f %15.8e\n",
                       (i / deg), a, s, c, t);
                printf(" %15.8e %15.8e\n", ls, lc);
            }
        }
    }
    printf("%15.8e\n", ssum);
    printf("%15.8e\n", csum);
}


OK, adesso ottengo

Nel mio caso la nuova versione è 4.15 volte più veloce (meno del valore annunciato dal dr.prof. non so se sbaglio qualcosa. O forse il mio ‘puter è meno performante: non sembra ci siano variazioni tra il dichiarare inline o no la mypow().

Un’altra cosa ancora: secondo questa pagina  dovrebbe essere possibile velocizzare la pow() passandogli un intero come secondo parametro; dalle prove fatte non mi risulta.

Ho provato a ricompilare la versione Fortran con l’opzione -O3 ma non ci sono variazioni.

E con Java? Secondo me dovrebbe funzionare. Non mi sembra esista la direttiva inline ma ne so poco.

import static java.lang.System.out;

class jc10l {
    static double f[];

    static double ln10 = myln(10.0);

    public static double mypow(double x, int y) {
        double r = 1;
        for (int i = 0; i < y; i++) r *= x;
        return r;
    }

    public static double fsin(double a) {
        return a - mypow(a, 3) / f[3] + mypow(a, 5) / f[5]
                 - mypow(a, 7) / f[7] + mypow(a, 9) / f[9]
                 - mypow(a, 11) / f[11] + mypow(a, 13) / f[13];
    }

    public static double fcos(double a) {
        return 1.0 - mypow(a, 2) / f[2] + mypow(a, 4) / f[4]
                   - mypow(a, 6) / f[6] + mypow(a, 8) / f[8]
                   - mypow(a, 10) / f[10] + mypow(a, 12) / f[12];
    }

    public static double myln(double x) {
        if(x == 0.0) {
            return -1.0e20;
        } else {
            double r = (x - 1.0) / (x + 1.0);
            return 2.0 * (r + mypow(r, 3) / 3.0
                            + mypow(r, 5) / 5.0
                            + mypow(r, 7) / 7.0
                            + mypow(r, 9) / 9.0);
        }
    }

    public static double mylog(double x) {
        return x / ln10;
    }

    public static void main(String args[]) {
        int i, j;
        f = new double[14];
        f[0] = 1.0;
        for (i = 1; i < 14; i++) {
            f[i] = i * f[i - 1];
        }
        int deg = 60 * 60;
        int nsteps = 180 * deg;
        double step = Math.PI / nsteps;
        double ssum;
        double csum;
        double a, s, c, t = 0.0;
        double ls, lc;

        for (j = 1; j < 11; j++) {
            ssum = 0.0;
            csum = 0.0;
            for (i = 0; i <= nsteps; i++) {
                a = i * step;
                s = fsin(a);
                ls = mylog(myln(s));
                ssum += s;
                c = fcos(a);
                lc = mylog(myln(c));
                csum += c;
                if ((i % (10 * deg)) == 0) {
                    if (c != 0.0) {
                        t = s / c;
                    }
                    out.printf("%3d %11.8f %11.8f %11.8f %15.8e\n",
                          (i / deg), a, s, c, t);
                    out.printf("              %15.8e %15.8e\n",
                               ls, lc);
                }
            }
            out.println(ssum);
            out.println(csum);
        }
    }
}


Ehi! quasi veloce come il C, e comunque più eloce del Fortran!


Ulteriore prova: sostituire l’operatore **con mypow() per il Fortran. Non credo sia il caso di riportare il codice, ecco perché:

Quindi in conclusione (spero questa volta sia davvero tale) il codice C è il più performante. Subito dopo Java e il Fortran. L’operatore ** del Fortran è molto più efficiente di una funzione che esegue un ciclo di moltiplicazioni; oltretutto si paga il costo della chiamata.

C’è poi la domanda (eterna) che continua a non avere una risposta convincente per me: confrontando i listati non trovo che il codice Fortran sia più facile; OK è più simile al Basic e siamo abituati da tempo (per la versione formatata, quella per le schede) ma sono motivi validi e sufficienti?
Adesso mi cacciano 😦

Ancora confronti sui linguaggi

Intanto questa dovrebbe essere l’ultima puntata del tormentone sviluppato negli ultimi post: comincio a detestarlo al pari del caldo afoso che perdura.


Poi devo confessare un mio errore, grave: nella versione Fortran del listato ho postato un file non aggiornato, quello del modulo m_conf.f08. Questa è la versione corretta:

module m_conf
implicit none

real(8) :: f(13)

contains
    real(8) function fsin(a) result(r)
        real(8) :: a
        r = a - a ** 3 / f(3) + a ** 5 / f(5) &
            & - a ** 7 / f(7) + a ** 9 / f(9) &
            & - a ** 11 / f(11) + a ** 13 / f(13)
    end function fsin

    real(8) function fcos(a) result(r)
        real(8) :: a
        r = 1 - a ** 2 / f(2) + a ** 4 / f(4) &
            & - a ** 6 / f(6) + a ** 8 / f(8) &
            & - a ** 10 / f(10) + a ** 12 / f(12)
    end function fcos

end module

Conseguenza della mancata dichiarazione di real(8) per le funzioni fsin() e fcos() queste restituivano un valore real(4). Ma attenzione: siccome tutte le variabili erano in doppia precisione (ahemm, real(8), una volta si diceva così) il tempo di esecuzione non cambia!

Allora l’errore –OK, il grave errore– è quasi solo metodologico. E a mia giustificazione potrei citare il fatto di essere costantemente interrotto da infinite incombenze. E dal caldo, non dimentichiamo il caldo. C’è poi un altro motivo, più grave: a lavorare da solo non hai la possibilità di farlo vedere a qualcuno, raccontarglielo e allora queste cose salterebbero fuori subitissimo.

In ogni caso per verificare se il Fortran è davvero così più performante rispetto a Python ho modificato i codici facendo ripetere i calcoli 10 volte e introducendo il calcolo dei logaritmi decimali di seno e coseno.

Io da piccolo invece del liceo ho frequentato l’istituto per geometri (sono figlio di contadini). Uh! anche Piergiorgio Odifreddi e Flavio Briatore hanno fatto i geometri, pensa te!
A quei tempi non esistevano le calcolatrici elettroniche e i conti si dovevano fare a mano. In particolare per il corso di topografia questi assumevano un’importanza preponderante e si usavano i logaritmi, quelli del Bruhns. Chi fosse Karl Christian Bruhns l’ho scoperto solo l’altro giorno, grazie al solito Wiki. Devo confessarvi che adesso il Bruhns mi risulta simpatico; ma solo da adesso 😉

OK, fine dell’intermezzo nostalgico; il calcolo viene fatto usando la serie

presa da qui.

Poi la trasformazione da logaritmo naturale a decimale richiede solo una divisione ma nello spirito del test si è resa come una funzione. Questo perché anticamente, quando i ‘puter erano grossi e lenti, sarebbe stato un errore grave: chiamare una funzione richiede(va) aggiungere istruzioni, in questo caso inutili. Ma basta divagazioni, ecco le versioni aggiornate.

Python

#!/usr/bin/env python

import math

v = 2.0
f = ([1.0, 1.0, v])
for i in range(3, 14):
    v *= i
    f.append(v)

def fsin(a):
    return a - pow(a, 3) / f[3] + pow(a, 5) / f[5] \
             - pow(a, 7) / f[7] + pow(a, 9) / f[9] \
             - pow(a, 11) / f[11] + pow(a, 13) / f[13]

def fcos(a):
    return 1 - pow(a, 2) / f[2] + pow(a, 4) / f[4] \
             - pow(a, 6) / f[6] + pow(a, 8) / f[8] \
             - pow(a, 10) / f[10] + pow(a, 12) / f[12]

def ln(x):
    if x == 0.0:
        return -1.0e100
    r = (x - 1.0) / (x + 1.0)
    return 2.0 * (r + pow(r, 3) / 3.0 + pow(r, 5) / 5.0
                    + pow(r, 7) / 7.0 + pow(r, 9) / 9.0)

def log(x):
    return x / ln10

pi = math.pi
deg = 60 * 60
nsteps = 180 * deg
step = pi / nsteps
ln10 = ln(10.0)
fmt = "{:3d} {:10.8f} {: 10.8f} {: 10.8f} {: 10.8e}"
fml = "{:12s} {: 12.8e} {: 12.8e}"

ssum = 0.0
csum = 0.0
for i in range (0, nsteps + 1):
    a = i * step
    s = fsin(a)
    ls = log(ln(s))
    ssum += s
    c = fcos(a)
    lc = log(ln(c))
    csum += c
    if (i % (10 * deg)) == 0:
        if c != 0.0:
            t = s / c
        print fmt.format(int(i / deg), a, s, c, t)
        print fml.format('', ls, lc)
print ssum, csum

Fortran

program fc10l
use m_c10l
implicit none

real(8) :: v, pi, step, ssum, csum
real(8) :: a, s, c, t, ls, lc
integer(4) :: deg, nsteps, i, j
character(30) fmt_, fml_

f(1) = 1.0
do i = 2, 13
    f(i) = i * f(i-1)
end do

pi = 4 * atan(1.0)
deg = 60 * 60
nsteps = 180 * deg
step = pi / nsteps
ln10 = myln(dble(10.0))
fmt_ = "(i3, 3(1x, f11.8), 1x, e12.4)"
fml_ = "(12x, 2(e12.4))"

do j = 1, 10
    ssum = 0.0
    csum = 0.0

    do i = 0, nsteps
        a = i * step
        s = fsin(a)
        ls = mylog(myln(s))
        ssum = ssum + s
        c = fcos(a)
        lc = mylog(myln(c))
        csum = csum + c
        if (mod(i,10 * deg) == 0) then
            if (c /= 0.0) t = s / c
            print fmt_, int(i / deg), a, s, c, t
            print fml_, ls, lc
        end if
    end do

    print *, ssum, csum
end do
end program

 

module m_c10l
implicit none

real(8) :: f(13)
real(8) ln10

contains
    real(8) function fsin(a) result(r)
        real(8) :: a
        r = a - a ** 3 / f(3) + a ** 5 / f(5) &
            & - a ** 7 / f(7) + a ** 9 / f(9) &
            & - a ** 11 / f(11) + a ** 13 / f(13)
    end function fsin

    real(8) function fcos(a) result(r)
        real(8) :: a
        r = 1 - a ** 2 / f(2) + a ** 4 / f(4) &
            & - a ** 6 / f(6) + a ** 8 / f(8) &
            & - a ** 10 / f(10) + a ** 12 / f(12)
    end function fcos

    real(8) function myln(x) result(res)
        real(8) :: x, r
        if(x == 0.0) then
            res = -1.0e20
        else
            r = (x - 1.0) / (x + 1.0)
            res = 2.0 *(r ** 3 / 3.0 + r ** 5 / 5.0 &
                & + r ** 7 / 7.0 + r ** 9 / 9.0)
        end if
    end function

    real(8) function mylog(x) result(res)
        real(8) :: x
        res = x / ln10
    end function

end module

Java

import static java.lang.Math.pow;
import static java.lang.System.out;

class jc10l {
    static double f[];
    //static double ln10;
    static double ln10 = myln(10.0);

    public static double fsin(double a) {
        return a - pow(a, 3.0) / f[3] + pow(a, 5.0) / f[5]
                 - pow(a, 7.0) / f[7] + pow(a, 9.0) / f[9]
                 - pow(a, 11.0) / f[11] + pow(a, 13.0) / f[13];
    }

    public static double fcos(double a) {
        return 1.0 - pow(a, 2.0) / f[2] + pow(a, 4.0) / f[4]
                   - pow(a, 6.0) / f[6] + pow(a, 8.0) / f[8]
                   - pow(a, 10.0) / f[10] + pow(a, 12.0) / f[12];
    }

    public static double myln(double x) {
        if(x == 0.0) {
            return -1.0e20;
        } else {
            double r = (x - 1.0) / (x + 1.0);
            return 2.0 * (r + pow(r, 3.0) / 3.0
                            + pow(r, 5.0) / 5.0
                            + pow(r, 7.0) / 7.0
                            + pow(r, 9.0) / 9.0);
        }
    }

    public static double mylog(double x) {
        return x / ln10;
    }

    public static void main(String args[]) {
        int i, j;
        f = new double[14];
        f[0] = 1.0;
        for (i = 1; i < 14; i++) {
            f[i] = i * f[i - 1];
        }
        int deg = 60 * 60;
        int nsteps = 180 * deg;
        double step = Math.PI / nsteps;
        double ssum;
        double csum;
        double a, s, c, t = 0.0;
        double ls, lc;

        for (j = 1; j < 11; j++) {
            ssum = 0.0;
            csum = 0.0;
            for (i = 0; i                 a = i * step;
                s = fsin(a);
                ls = mylog(myln(s));
                ssum += s;
                c = fcos(a);
                lc = mylog(myln(c));
                csum += c;
                if ((i % (10 * deg)) == 0) {
                    if (c != 0.0) {
                        t = s / c;
                    }
                    out.printf("%3d %11.8f %11.8f %11.8f %15.8e\n",
                          (i / deg), a, s, c, t);
                    out.printf("              %15.8e %15.8e\n",
                               ls, lc);
                }
            }
            out.println(ssum);
            out.println(csum);
        }
    }

}

newLISP

#!/usr/bin/newlisp

(set-locale "C") ;;; sono abituato così, perso un'ora!

;         0 1 2  3  4 5 6  7  8 9 10  11  12 13
(set 'p '(1 1 -1 -1 1 1 -1 -1 1 1 -1  -1  1  1))
(set 'f '(1))
(for (i 1 13)
    (set 't (* (p i) i (abs (f (- i 1)))))
    (push t f i)
)

(define (fsin a)
    (add a (div (pow a 3) (f 3)) (div (pow a 5) (f 5))
           (div (pow a 7) (f 7)) (div (pow a 9) (f 9))
           (div (pow a 11) (f 11)) (div (pow a 13) (f 13))
    )
)

(define (fcos a)
    (add 1 (div (pow a 2) (f 2)) (div (pow a 4) (f 4))
           (div (pow a 6) (f 6)) (div (pow a 8) (f 8))
           (div (pow a 10) (f 10)) (div (pow a 12) (f 12))
    )
)

(define (myln x)
    (if (= x 0.0)
        (set 'r -1.0e100)
        (begin
            (set 'r (div (dec x 1.0) (add x 1.0)))
            (mul 2.0 (add r (div (pow r 3) 3.0)
                            (div (pow r 5) 5.0)
                            (div (pow r 7) 7.0)
                            (div (pow r 9) 9.0)
                      )
            )
        )
    )
)

(define (mylog x)
    (div x ln10)
)

(set 'pi (mul 4 (atan 1.0))
     'deg (* 60 60)
     'nsteps (* 180 deg)
     'step (div pi nsteps)
     'ln10 (myln 10)
)

(for (j 1 10)
    (set 'ssum 0
         'csum 0
    )

    (for (i 0 nsteps)
        (set 'a (mul i step)
             's (fsin a)
             'ls (mylog (myln s))
             'c (fcos a)
             'lc (mylog (myln c))
        )
        (inc ssum s)
        (inc csum c)
        (if (!= c 0)
            (set 't (div s c)))
        (if (= (mod i (* deg 10)) 0)
            (begin
            (println (format "%3d %11.8f %11.8f %11.8f %15.8e"
                     (/ i deg) a s c t))
            (println (format "                %15.8e %15.8e"
                     ls lc))
            )
        )
    )
    (println ssum " " csum)
)

(exit)

Con newLISP ho perso un’ora a inseguire un errore mysteryuoso assay! Continuava a dirmi che le parentesi non erano bilanciate anche se l’editor mi diceva che erano OK. Dimenticare qualche parentesi con il Lisp è la cosa peggiore che un niubbo possa fare, ma io tanto niubbo non sono e poi ormai una certa pratica ce l’ho. Altro errore è quando apri e chiudi dove non devi ma neanche questo mi sembrava il caso. Poi l’illuminazione, la virgola al posto del punto. Atz! dimenticato (set-locale "C"), sì io i numeri sono abituato a scriverli alla ‘mericana.
A dire il vero ho dimenticato Nmila punti-e-virgola nella versione Java ma lì il compilatore te lo dice.

Nell’esecuzione dei confronti è tornato mooolto utile il fatto che time scrive su stderr; quindi ridirigendo l’output (stdout) sul teminale si può avere un risultato leggibile

Il risultato finale conferma sostanzialmente quello della prima versione. I rapporti rispetto al Fortran dei tempi di esecuzione diminuiscono leggermente ma rimangono elevati.

Per motivi non solo informatici pare che si userà il Fortran per i calcoli e Python per cose complementari, come la costruzione dei grafici riassuntivi delle elaborazioni. Mi sa che ne riparlerò, forse… 😉

Ancora confronti con il Fortran

Questo post è la continuazione (OK, poi basta) di Un confronto Fortran – Python.


Oggi provo altri due linguaggi. Si comincia con Java, largamente usato, anche se in campi completamente diversi. Una nota per gli eventuali javanisti: ho tentato di tenermi il più fedele possibile alla versione Python; inoltre non uso mai Java e è possibile che ci siano ingenuità e katzate kolossali, benvenute le osservazioni, come al solito.

import static java.lang.Math.pow;
import static java.lang.System.out;

class jconf {
    static double f[];

    public static double fsin(double a) {
        return a - pow(a, 3.0) / f[3] + pow(a, 5.0) / f[5]
                 - pow(a, 7.0) / f[7] + pow(a, 9.0) / f[9]
                 - pow(a, 11.0) / f[11] + pow(a, 13.0) / f[13];
    }

    public static double fcos(double a) {
        return 1.0 - pow(a, 2.0) / f[2] + pow(a, 4.0) / f[4]
                   - pow(a, 6.0) / f[6] + pow(a, 8.0) / f[8]
                   - pow(a, 10.0) / f[10] + pow(a, 12.0) / f[12];
    }

    public static void main(String args[]) {
        int i;
        f = new double[14];
        f[0] = 1.0;
        for (i = 1; i < 14; i++) {
            f[i] = i * f[i - 1];
        }
        int deg = 60 * 60;
        int nsteps = 180 * deg;
        double step = Math.PI / nsteps;
        double ssum = 0.0;
        double csum = 0.0;
        double a, s, c, t = 0.0;

        for (i = 0; i <= nsteps; i++) {
            a = i * step;
            s = fsin(a);
            ssum += s;
            c = fcos(a);
            csum += c;
            if ((i % (10 * deg)) == 0) {
                if (c != 0.0) {
                    t = s / c;
                }
                out.printf("%3d %11.8f %11.8f %11.8f %15.8e\n",
                      (i / deg), a, s, c, t);
            }
        }
        out.println(ssum);
        out.println(csum);
    }

}


Giudizio (provvisorio) di G.: “sembra C“, che sarebbe un grosso difetto.

Poi, solo per confronto, tanto si fa in fretta, ho provato il mio linguaggio di scripting prediletto. Anche qui benvenute le osservazioni ricordando che è la traduzione quasi fedele dello script Python. In effetti mi sono discostato in due punti:

  • per la notazione prefissa del Lisp l’alternanza di somme e sottrazioni nelle funzioni fsin e fcos produrrebbe codice “non bello esteticamente” e allora ho messo il segno nella lista f;
  • sì! l’array f qui è diventato una lista; gli array ci sarebbero anche ma ehi! siamo nel Lisp.
#!/usr/bin/newlisp

;         0 1 2  3  4 5 6  7  8 9 10  11  12 13
(set 'p '(1 1 -1 -1 1 1 -1 -1 1 1 -1  -1  1  1))
(set 'f '(1))
(for (i 1 13)
    (set 't (* (p i) i (abs (f (- i 1)))))
    (push t f i)
)

(define (fsin a)
    (add a (div (pow a 3) (f 3)) (div (pow a 5) (f 5))
           (div (pow a 7) (f 7)) (div (pow a 9) (f 9))
           (div (pow a 11) (f 11)) (div (pow a 13) (f 13))
    )
)

(define (fcos a)
    (add 1 (div (pow a 2) (f 2)) (div (pow a 4) (f 4))
           (div (pow a 6) (f 6)) (div (pow a 8) (f 8))
           (div (pow a 10) (f 10)) (div (pow a 12) (f 12))
    )
)

(set 'pi (mul 4 (atan 1.0))
     'deg (* 60 60)
     'nsteps (* 180 deg)
     'step (div pi nsteps)
)

(set 'ssum 0
     'csum 0
)

(for (i 0 nsteps)
    (set 'a (mul i step)
         's (fsin a)
         'c (fcos a)
    )
    (inc ssum s)
    (inc csum c)
    (if (!= c 0)
        (set 't (div s c)))
    (if (= (mod i (* deg 10)) 0)
        (println (format "%3d %11.8f %11.8f %11.8f %15.8e"
                 (/ i deg) a s c t))
    )
)
(println ssum " " csum)

(exit)


Notare come i due linguaggi di oggi settino il separatore decimale in base alle convenzioni locali; è possibile peraltro ridefinirlo (come anche per il Fortran).

A questo punto riporto, aggiornata, la tabella riassuntiva del confronto. Notate l’ultima riga: il rapporto tra il tempo di esecuzione del linguaggio rispetto a quello del Fortran. Java è in seconda posizione ma lo credevo più performante. Bene newLISP, molto meglio di Python. Naturalmente per quello che questi numeri valgono, la scelta dev’essere fatta rispetto a altre considerazioni, ovviamente.
Poi, OK, non parlo più del Lisp, “dai è peggio del C, se possibile!“, “ma no, dai, vedi com’è bello, uniforme“, ““, ““, ….

Problemi con il costruttore


Malgrado il titolo, non sto ristrutturando il bagno.
E no, non parlerò dei problemi della famiglia Ligresti.
Semmai parlerò di programmazione, stavolta in Java. Il post è un pochino tecnico, cercherò di renderlo semplice, ma se cominciate ad annoiarvi siete autorizzati a saltare alla conclusioni.

L’errore

Qualche tempo fa mi si presenta uno studente con un programma Java che gli andava in crash. Cercherò qui di farvi capire qual’è il problema con un piccolo esempio esplicativo (giusto per non farvi vedere la tonnellata di codice del programma originale, che non serve a niente). Ecco il mio programmino:

class Base {
    public Base() {
        System.out.println("Base constructor");
        function();
    }
    public void function() {
        System.out.println("Base.function()");
    }
}

class Derived extends Base {
    public Derived(String n) {
        super();
        System.out.println("Derived constructor");
    }
    public void function() {
        System.out.println("Derived.function()");
    }
}

public class ConstructorDemo {
    public static void main(String args[]) {
        Derived obj = new Derived();
        System.out.println("Fine del programma!");
    }
}

Se volete provarlo, salvate il file con nome “ConstructorDemo”, compilate con “javac ConstructorDemo.java”, ed eseguite con “java ConstructorDemo”. Otterrere il seguente output:


Base constructor
Derived.function()
Derived constructor
Fine del programma!

Notate l’ordine delle chiamate: quando cerchiamo di creare un oggetto di tipo “Derived”, per prima cosa viene chiamato il costruttore della classe Derived, che a sua volta per prima cosa chiama il costruttore della classe Base (con super()), il quale stampa a video la prima scritta.
Questo costruttore della classe Base chiama la funzione “function()” che è ridefinita nella classe Derived: quindi, entra in gioco il polimorfismo dei linguaggi Object Oriented, per cui tra le due funzioni che si chiamano “function()” quella chiamata è la seconda che stampa la seconda scritta. Infine, si ritorna nel costruttore Derived(), che stampa a video la terza scritta, e nel main che stampa l’ultima scritta.
Spero mi abbiate seguito fin qui.
Ebbene, questo programma è bacato. Cioè, se lo eseguite funziona, niente di male. Ma chi ha scritto la classe Base è una testa d’uovo e ha fatto un errore di progetto della classe. Infatti, un costruttore non dovrebbe mai chiamare una funzione che potrebbe essere in seguito essere ridefinita da una classe derivata! Perché l’oggetto nel suo complesso non è stato ancora costruito del tutto, e in particolare ci sono parti ancora non inizializzate. Per metter in evidenza il problema farò due piccole modifiche alla classe Derived. Ecco la nuova versione:

class Base {
    public Base() {
        System.out.println("Base constructor");
        function();
    }

    public void function() {
        System.out.println("Base.function()");
    }
}

class Derived extends Base {
    private String name; 
    public Derived(String n) {
        super();
        name = n;
        System.out.println("Derived constructor");
    }
    public void function() {
        System.out.println("Derived.function() - " + name.toUpperCase());
    }
}

public class ConstructorDemo {
    public static void main(String args[]) {
        Derived obj = new Derived("Giuseppe");
        System.out.println("Fine del programma!");
    }
}

Adesso il costruttore della classe Derived dichiara una variabile privata di tipo String, e ha un parametro sempre di tipo String. La variabile name viene inizializzata nel costruttore per essere uguale al parametro n. Inoltre, la funzione “function()” adesso, oltre a stampare la scritta “Derived.function()”, stampa anche il contenuto della stringa name convertito in maiuscole. Ed ecco l’output del programma:

Base constructor
Exception in thread "main" java.lang.NullPointerException
at Derived.function(ConstructorDemo.java:22)
at Base.(ConstructorDemo.java:6)
at Derived.(ConstructorDemo.java:17)
at ConstructorDemo.main(ConstructorDemo.java:28)

Penso sia abbastanza chiaro cosa sia successo: il costruttore Base() chiama la funzione “function()” che usa la variabile name prima che tale variabile venga inizializzata nel costruttore Derived, e quindi bum.
L’errore non è in chi ha scritto la classe Derived(), ma in chi ha scritto la classe Base() chiamando la “function()” nel costruttore. Naturalmente in questo caso è sempre il sottoscritto ad aver scritto entrambe le classi, ma in generale non è detto.

Chi è stato?


Ma non è stato lo studente a fare l’errore. Infatti, lui aveva appena seguito un corso di programmazione Android, la classe “Base()” faceva parte dell’SDK, e la classe Derived era stata scritta da lui per il progetto del corso. E naturalmente non poteva immaginare che chi aveva scritto l’SDK avesse fatto un errore così madornale. Più precisamente la classe Base era la android.widget.SimpleCursorTreeAdapter. Per fortuna il problema è stato successivamente risolto in una successiva release.
Ma è davvero un problema così grave? Beh, se uno studente fa una cosa così al mio esame di OOSD, io sarei tentato di bocciarlo, o quantomeno abbassargli parecchio il voto. Sì, per me è piuttosto grave.

Conclusioni

Possibile che a Google facciano delle cavolate così? Beh, sì, è possibile, l’errore è sempre dietro l’angolo e non sempre i programmatori delle grosse aziende come Google sono “top of the notch”.
E quindi la prima lezione di questo post è: sempre … cioè no, mai (cit.) chiamare funzioni polimorfiche dal costruttore, a meno che la classe non sia dichiarata final (ovvero non derivabile). Idem in C++ o in ogni altro linguaggio OO.
E la seconda è: non sopravvalutate mai l’abilità dei programmatori degli altri! Meditate gente, meditate …

Compilati VS interpretati passo 2 (The compiler strikes back)

Ciao!!!

Spero che i fans di guerre stellari non me ne vogliano per la citazione del titolo 🙂 Dopo settimane di ritardo e alcune discussioni con Ari (come chi è? … non avete letto i commenti al primo articolo?) sono tornato con ulteriori test e risultati da proporvi.
In realtà questa seconda parte doveva essere diversa, ma viste le giuste osservazioni fatte da Ari ho deciso di cambiarlo per dare ascolto a chi questo blog lo legge e quindi da un senso e uno stimolo a chi qui scrive esclusivamente per passione.
Torniamo a noi! Ari mi ha fatto giustamente notare che le routines di python che leggono i file sono scritte in C. Quindi in realtà non è python che legge velocemente i file, ma il codice C dietro le quinte che fa il lavoro sporco.
Ne è seguita una piccola discussione sulla valenza da dare al mio precedente articolo e su quali sarebbero stati i test giusti da fare… e di seguito troverete i risultati che ho ottenuto 🙂
Ho giocato anche a fare la “Cassandra” di turno (non ditemi che non sapete chi è Cassandra 😦 ) per cui vi riporto le mie sibilline parole:

“Per cui nella seconda parte dell’articolo faremo i test che suggerisci, ma ho il sospetto che vedremo dei risultati interessanti, almeno in base a quella che è stata la mia esperienza con Java.”

Ma andiamo a cominciare…
Il test eseguito è molto semplice. Ho fatto calcolare il fattoriale da 1 a 10.000 con una bella procedura non ricorsiva e poi ho fatto un “ottimo” sort con una procedura non efficente (ditemi quanti di voi saprebbero scrivere a memoria un algoritmo di quicksort 🙂 )

Vediamo i sorgenti e i risultati ottenuti per python, C, Objective C e Java.

#!/usr/bin/env python2.6
# -*- coding: utf-8 -*-

import time
import array

MAXLOOP = 10000
nArray = array.array('d', range(MAXLOOP))

t0 = time.time()
print "Tempo inizio caricamento array: ", time.strftime("%H:%M:%S")
for i in range(MAXLOOP):
  f = float(1)

  for a in range(i):
    f = f * a

  nArray[i] = i + f

t1 = time.time()
print "Tempo fine caricamento array: ", time.strftime("%H:%M:%S")
print "Tempo di caricamento dei dati: ", t1 - t0, " secondi"

t0 = time.time()
print "Tempo inizio ordinamento: ", time.strftime("%H:%M:%S")

# ordinamento in maniera decrescente
for i in range(MAXLOOP):
  dMax = nArray[i];

  for j in range (i + 1, MAXLOOP):
    if nArray[j] > dMax:
      dTemp = nArray[j];
      nArray[j] = dMax;
      nArray[i] = dTemp;
      dMax = dTemp;

t1 = time.time()
print "Tempo fine ordinamento: ", time.strftime("%H:%M:%S")
print "Tempo di caricamento dei dati: ", t1 - t0, " secondi"

I tempi sono stati sulla mia macchina freebsd sono stati:

– 11,06 s per il caricamento dell’array con calcolo del fattoriale
– 42,07 s per il sort

Vediamo il C come si comporta:

#include <stdio.h>
#include <time.h>

const int MAXLOOP = 10000;

int main (int argc, const char * argv[])
{
  time_t startTime;
  time_t endTime;
  struct tm * timeinfo;

  long double nArray[MAXLOOP];
  int i;

  time(&startTime);
  timeinfo = localtime(&startTime);
  printf("Tempo inizio caricamento array: %s", asctime(timeinfo));

  for (i = 0; i < MAXLOOP; i++)
  {
    // Calcolo del fattoriale
    long double f = 1;
    int a;

    for(a = 1; a <= i; a++)
    {
      f *= a;
    }

    nArray[i] = i * f;
  }

  time(&endTime);
  timeinfo = localtime(&endTime);
  printf("Tempo fine caricamento array: %s", asctime(timeinfo));

  time(&startTime);
  timeinfo = localtime(&startTime);
  printf("Tempo inizio ordinamento: %s", asctime(timeinfo));
  // ordinamento in maniera decrescente
  for (i = 0; i < MAXLOOP; i++)
  {
    double dMax = nArray[i];
    int j;
    for (j = i + 1; j < MAXLOOP; j++)     
    {       
      if (nArray[j] > dMax)
      {
        double dTemp = nArray[j];
        nArray[j] = dMax;
        nArray[i] = dTemp;
        dMax = dTemp;
      }
    }
  }
  time(&endTime);
  timeinfo = localtime(&endTime);
  printf("Tempo fine ordinamento: %s", asctime(timeinfo));

  return 0;
}

Tempi:

– 4 s per il caricamento dell’array con calcolo del fattoriale
– meno di 1 s per il sort

Adesso è il turno di objective C.

#import <Foundation/Foundation.h>

const int MAXLOOP = 10000;

int main (int argc, const char * argv[])
{
  NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

  NSDate *startTime;
  NSDate *endTime;

  NSMutableArray *nArray = [[NSMutableArray alloc] initWithCapacity: MAXLOOP];
  int i;

  startTime = [NSDate date];
  NSLog(@"Tempo inizio caricamento array: %@", startTime);

  for (i = 0; i < MAXLOOP; i++)
  {
    // Calcolo del fattoriale
    long double f = 1;
    int a;

    for(a = 1; a <= i; a++)
    {
      f *= a;
    }

    [nArray addObject: [NSNumber numberWithDouble: i * f]];
  }

  endTime = [NSDate date];
  NSLog(@"Tempo fine caricamento array: %@", endTime);

  startTime = [NSDate date];
  NSLog(@"Tempo inizio sort: %@", startTime);

  // ordinamento in maniera decrescente
  for (i = 0; i < MAXLOOP; i++)
  {
    double dMax = [[nArray objectAtIndex: i] doubleValue];
    int j;
    for (j = i + 1; j < MAXLOOP; j++)     
    {       
      if ([[nArray objectAtIndex: j] doubleValue] > dMax)
      {
        double dTemp = [[nArray objectAtIndex: j] doubleValue];
        [nArray replaceObjectAtIndex: j withObject: [NSNumber numberWithDouble: dMax]];
        [nArray replaceObjectAtIndex: i withObject: [NSNumber numberWithDouble: dTemp]];
        dMax = dTemp;
      }
    }
  }
  endTime = [NSDate date];
  NSLog(@"Tempo fine sort: %@", endTime);

  [pool drain];
  return 0;

}

Tempi:

– 4 s per il caricamento dell’array con calcolo del fattoriale
– 2,13 s per il sort

Per finire passiamo a Java.

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

public class SortTest {

    static final int MAXLOOP = 10000;

    public static void main(String[] args) {
        // TODO code application logic here
        int i;
        double[] nArray = new double[MAXLOOP];

        DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
        Date startTime = new Date();
        System.out.printf("Tempo inizio caricamento array: %s\n", dateFormat.format(startTime));
        long start = System.nanoTime();

        for (i = 0; i < MAXLOOP; i++)
        {
            // Calcolo del fattoriale
            double f = 1;
            int a;

            for(a = 1; a <= i; a++)
            {
                f *= a;
            }

            nArray[i] = i * f;
        }
        long end = System.nanoTime();

        Date endTime = new Date();
        System.out.printf("Tempo fine caricamento array: %s\n", dateFormat.format(endTime));

        System.out.printf("Tempo caricamento array: %d ms\n", (end - start) / 1000000);

        startTime = new Date();
        System.out.printf("Tempo inizio ordinamento: %s\n", dateFormat.format(startTime));
        start = System.nanoTime();

        // ordinamento in maniera decrescente
        for (i = 0; i < MAXLOOP; i++)
        {
            double dMax = nArray[i];
            int j;
            for (j = i + 1; j < MAXLOOP; j++)             
            {                 
                if (nArray[j] > dMax)
                {
                    double dTemp = nArray[j];
                    nArray[j] = dMax;
                    nArray[i] = dTemp;
                    dMax = dTemp;
                }
            }
        }
        end = System.nanoTime();

        endTime = new Date();
        System.out.printf("Tempo fine ordinamento: %s\n", dateFormat.format(endTime));

        System.out.printf("Tempo ordinamento array: %d ms\n", (end - start) / 1000000);
    }
}

Tempi:

– 248 ms per il caricamento dell’array con calcolo del fattoriale
– 278 ms per il sort

Quindi riassumiamo il tutto in un formato più leggibile.

Caricamento
+ fattoriale

Sort

Python

11,06 s

42,07 s

C

4 s

< 1 s

Objective-C

4 s

2,13 s

Java

0,25 s

0,28 s

Per cui come diceva Ari, il compilato è più efficente. Possiamo notare la differenza di tempi nel sort tra objective-c e C ma l’array usato in objective-c è un NSMutableArray che vuole oggetti al suo interno, per cui è presente un overhead dovuto alla creazione dell’oggetto… in soldoni non stiamo spostando dei semplici long double come in C.
Infine la sorpresa “cassandriana” di Java… che è un bytecode che viene interpretato da una virtual machine.
Anche python genera del bytecode… evidentemente meno efficente di quello java.

Ma torniamo a noi con le conclusioni di questa seconda parte. Comincio mettendo le mani avanti. Io sono un programamtore “stagionato” o “antico” questo vuol dire che mi piace il codice che funziona velocemente e non mi ammazza un processore con 6 core e GHz di potenza se lancio 2 istanze di un programma. Anche se preferisco il C/C++ o Delphi, non mi faccio trascinare dalle mode e ho capito oramai da anni che il linguaggio “buono” è quello che ti permette di staccare il cliente dalla propria giugulare prima di finire dissanguati 🙂 per cui se il mio software deve leggere file, fare sort e usare le regular expression allora python mi permette con poche righe di codice di fare tutto ciò e con poche modifiche di spostarmi su varie piattaforme.
Se invece il problema da risolvere è in ambiente real time o prevede calcoli a go-go o pochissima RAM come in un sistema embedded allora la scelta giusta è il C (provate a far girare python su una centralina di un’auto 😛 ). Se volete lavorare su MAC e scrivere PDF e vedere le vostre applicazioni volare passate ad objective-C.
La scusa della battaglia compilati VS interpretati era solo un pretesto per scrivere del codice e sfatare preconcetti e come ho detto in più occasioni non voleva essere un test scientifico che avrebbe richiesto uno rigore diverso.

L’importante è “divertirsi” ed imparare. Senza diventare gli hooligans dei chip. Ciao e alla prossima