Toast: Che stai facendo?
Dag: Prego.
Toast: Chi preghi?
Dag: Chiunque ci ascolti.
Nell'ultimo articolo avevo giurato che l'argomento Fast IPC era, "almeno momentaneamente", chiuso. Poi, mentre riguardavo, per l'ennesima volta, lo stupendo Mad Max: Fury Road del Maestro George Miller, ho avuto un flash (si, ma non vi preoccupate, è durato solo un microsecondo, mentre guardo un film sono sempre molto concentrato). Il flash era questo: "E se ripetessi i test IPC socket usando il Go?" L'idea era intrigante e, alla fine, ho ceduto a me stesso. E così ho anche capito che riesco, con grande facilità, a non mantenere le promesse... avrò mica un gran futuro come politico? ah ah ah.
...e se provassi a farlo con il Go?... |
E allora veniamo al dunque: il titolo qui sopra "come spedire una struttura in Go" è un po' fuorviante riguardo alla premessa iniziale: l'idea è ripetere alcuni benchmark usando il Go (Golang per gli amici) però per farlo bisogna scontrarsi un po' con una delle differenze che ha questo linguaggio rispetto al C (e al C++); come ricorderete (e se no potreste fare una rapida rilettura di quel vecchio post) i test erano basati sull'invio "a raffica" di molti messaggi (2000000!) composti così:
// struttura Data per i messaggitypedef struct {unsigned long index; // indice dei datichar text[1024]; // testo dei dati} Data;
La scelta di questa struttura non era casuale: avrei potuto, più semplicemente, inviare solo dei messaggi di testo, ma avevo deciso di inviare dati complessi ("Data" contiene solo due campi ma potrebbe contenerne moltissimi) per rimarcare che con il C è usuale trattare dati di qualsiasi tipo, e chi riceve un messaggio "complesso" lo può ricostruire semplicemente depositandolo in una variabile dello stesso tipo (ah, la potenza del C...). E poi, grazie alla struttura "Data" ho potuto anche mostrare come gestire un indice dei messaggi, il che non guasta mai.
E il Go? Come ben sapete (e ne ho parlato qui) il Go è un vero linguaggio ad alto livello, con tutti i pro e i contro che questo comporta. Tra i pro c'è, ovviamente, il fatto che è possibile scrivere applicazioni anche complesse con notevole semplicità e compattezza, sicuramente più del C (e C++). Però, quando si tratta di maneggiare dati a livello base (o meglio binario) il Go entra un po' in difficoltà, e questo è il caso che stiamo trattando: spedire (e ricevere) messaggi complessi (strutture) non è per nulla semplice e scontato come lo è per il nostro amato C. Ma è, comunque, possibile: e tra poco vedremo come e con quali prestazioni.
E ora, bando alle ciance, facciamo cantare il codice! Vediamo come sono i nostri reader.go e writer.go (equivalenti, più o meno, ai reader.c e writer.c visti qui). Per eseguire il benchmark è presente anche l'onnipresente processes.c, che vi risparmio perché è rimasto invariato. Vai col codice!
// reader.go - main processo figlio: è un reader (un server) su IPC socketpackage mainimport ("encoding/gob""fmt""net""os""time")// struttura Message per i messaggitype Message struct {Index int // indice dei datiText string // testo dei dati}// funzione mainfunc main() {// start ascolto sul file di scambio "myipcs" (con UNIX domain socket)fmt.Printf("processo %d partito (reader)\n", os.Getpid())addr := net.UnixAddr{Name: "./myipcs", Net: "unix"}lner, err := net.ListenUnix("unix", &addr)if err != nil {// errore listenfmt.Println(err)return}// prenoto la chiusura del listener e rimuovo (eventualmente) il file di scambiodefer lner.Close()defer os.Remove("./myipcs")// accetta connessioni da un writer entranteconn, err := lner.AcceptUnix()if err != nil {// errore acceptfmt.Println(err)return}// set time di partenza per calcolare il tempo impiegatostart := time.Now()// loop di lettura messaggi dal writern_msg := 0var message Messagefor {// set decoder e ricezione dal decoderdecoder := gob.NewDecoder(conn)decoder.Decode(&message)// test numero messaggi per forzare l'uscitan_msg++if n_msg == 2000000 {// il processo chiude la connessione ed esce per numero raggiuntofmt.Printf("reader: ultimo messaggio ricevuto: %s\n", message.Text)fmt.Printf("reader: processo %d terminato (messaggi=%d tempo totale:%s)\n",os.Getpid(), n_msg,time.Since(start).Truncate(time.Millisecond).String())conn.Close()return}}}
// writer.go - main processo figlio: è un writer (un client) su IPC socketpackage mainimport ("encoding/gob""fmt""net""os""time")// struttura Message per i messaggitype Message struct {Index int // indice dei datiText string // testo dei dati}// funzione mainfunc main() {// mi assicuro che il writer parta dopo il readerfmt.Printf("processo %d partito (writer)\n", os.Getpid())time.Sleep(100 * time.Millisecond)// connessione al server remoto sul file di scambio "myipcs"addr := net.UnixAddr{Name: "./myipcs", Net: "unix"}conn, err := net.DialUnix("unix", nil, &addr)if err != nil {// errore dialfmt.Println(err)return}// loop di scrittura messaggi per il readervar message Messagemessage.Index = 0for {// test index per forzare l'uscitaif message.Index == 2000000 {// il processo chiude la connessione ed esce per indice raggiuntofmt.Printf("writer: processo %d terminato (text=%s messaggi=%d)\n",os.Getpid(), message.Text, message.Index)conn.Close()return}// compongo il messaggio e lo inviomessage.Index++message.Text = fmt.Sprintf("un-messaggio-di-test:%d", message.Index)// set encoder e spedizione dall'encoderencoder := gob.NewEncoder(conn)err = encoder.Encode(message)if err != nil {fmt.Println("errore di codifica: ", err)return}}}
Come avrete notato dalla descrizione nella prima linea (e anche dal codice, spero!) ho usato per il test gli IPC socket (UNIX domain socket). Poi ho ripetuto anche con i Network Socket, ma non mostrerò il codice perché è quasi identico. Effettivamente, per la magia del Go, il codice è semplicissimo rispetto alla analoga versione in C citata (che in questo caso era la versione "fast").
Però la complessità dell'operazione di spedire strutture complesse è mascherata dall'uso di un libreria specializzata, la encoding/gob, senza la quale il codice sarebbe molto più complesso (ebbene si, una libreria specializzata per una operazione semplice per il C ma complicata per il Go). E, come vedremo tra poco, le prestazioni non sono eccellenti come ci si aspetterebbe (spoiler: per colpa della encoding/gob). Comunque il codice è stra-commentato, e credo che possa essere facilmente compreso anche da chi non conosce il Go, per cui non mi dilungherò in spiegazioni superflue.
E vabbé, so che siete curiosi, è ora di passare ai risultati! Di seguito i risultati del benchmark in Go e, per comparazione, vi riporto anche i risultati della versione C:
aldo@Linux $ cd fastipcsocket/aldo@Linux $ ./processessono il padre (14990): attendo la terminazione dei figlisono il figlio 1 (14991): eseguo il nuovo processosono il figlio 2 (14992): eseguo il nuovo processoprocesso 14991 partito (reader)processo 14992 partito (writer)writer: processo 14992 terminato (text=un-messaggio-di-test:2000000 messaggi=2000000)sono il padre (14990): figlio 14992 terminato (0)reader: ultimo messaggio ricevuto: un-messaggio-di-test:2000000reader: processo 14991 terminato (messaggi=2000000 tempo CPU: 3.309 - tempo totale:3.309s)sono il padre (14990): figlio 14991 terminato (0)./processes: processi terminati
Ebbene si, per trattare 2000000 (!) di messaggi la versione C ha bisogno di 10 secondi in meno! (13.081s vs 3.309s). Però, a questo punto, bisogna fare qualche considerazione:
- Come versione di riferimento in C ho usato quella "fast", visto che il meccanismo della versione Go è a size variabile ed è, quindi, somigliante. Comunque anche usando la versione C "normal" la differenza è alta: 8 secondi (13.081s vs 4.823s). (Ho scritto un sacco di benchmark... ma non ve li mostro tutti per non farvi addormentare, ah ah ah).
- Vi riporto, per curiosità, i risultati delle versioni con i Network Socket: 26.794s per il Go e 3.88s per il C. Questo era previsto, gli IPC socket essendo "locali" sono mediamente più veloci dei Network Socket, anche se il peggioramento della differenza Go vs C un po' sorprende.
- Comunque, non fatevi ingannare dalle prestazioni: in termini assoluti 13.081s (e 26.794s) per 2000000 di messaggi sono, comunque, pochi! Il Go è un linguaggio veloce!
E, riguardo al punto 3 appena mostrato qui sopra, vi cito lo spoiler accennato poco fa (...per colpa della "encoding/gob"...): nella seconda parte dell'articolo (in arrivo prossimamente su questi schermi) vi faro vedere di che cosa è capace il Go quando maneggia solo testi.
Ok, per oggi può bastare: per il momento vi saluto, e vi raccomando, come sempre, di non trattenere il respiro in attesa della seconda parte (potrebbe nuocere gravemente alla vostra salute, ah ah ah).
Ciao, e al prossimo post!
Nessun commento:
Posta un commento