Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

martedì 27 agosto 2024

No Comment
come e perché scrivere i commenti nel C

Dom Cobb: Ég hef allt undir stjórn.
Arthur: hið gagnstæða væri alvarlegt.

(...una premessa: questo post è un remake di un mio vecchio post. Ma, anche se tratta lo stesso argomento, amplia un po' il discorso è mi è sembrato il caso di riproporlo. Leggete e mi direte...)

Non so se avete visto lo splendido Inception di Christopher Nolan. Bene, provate a pensare come sarebbe vedere un film con una trama così complessa doppiato in una lingua che non conoscete, come l'islandese per esempio (e senza sottotitoli!). Ci capireste qualcosa? Cosa si dicono Dom Cobb e Arthur qua sotto? Ecco, i sottotitoli sono i commenti del Cinema...

...adesso ti spiego: Ég hef allt undir stjórn...

Se siete di quelli che "Non scrivo commenti. Sono una perdita di tempo"  fermatevi qui. Tanto non riuscirei a convincervi (e neanche ci tengo).

Per tutti gli altri: il commento è un vostro amico fedele, non vi abbandonerà mai. Il commento è l'ultima barriera tra il capire e il non capire "Cosa avevo in testa quando ho scritto quel codice un anno fa ?". Il commento è la vostra dimostrazione di rispetto per i colleghi di lavoro: prima o poi, qualcuno di loro metterà le mani sul vostro Software, e il numero di maledizioni che vi manderà sarà inversamente proporzionale al numero (e alla qualità) dei commenti che avete scritto.

Certo, ci sono varie maniere di commentare il codice: si va dal "Absolute No Comment" al "Auto-Commentante con aggiunta di commenti" (beh, quest'ultimo è un po' esagerato), con tutte le sfumature intermedie possibili...

Facciamo una piccola dimostrazione: esaminiamo tre maniere si scrivere una funzione di lettura di un dispositivo di controllo industriale: si tratta, nel Caso 3, di codice quasi reale (solo quasi, eh!) che ho scritto alcuni anni fa: per questo esempio l'ho solo adattato un po'. Si noti che i tre codici sono praticamente identici, anche se la lettura dà tutt'altra impressione...

Caso 1: codice Absolute No Comment

int smsg(Dev *p)
{
int n=0;
if (p->msg[0]) {
snprintf(p->btx,sizeof(p->btx),"data: %s",p->msg);
if (!(n=(sdata(p->ch,(char*)p->btx,strlen(p->btx)))))
return -1;
p->msg[0]=0;
}
return n;
}

Come avrete notato non ci sono commenti, il codice è (a dir poco) criptico, e si risparmia su tutto: nomi, spazi, linee: non si sa mai che qualcuno riesca perfino a capire cosa c'è scritto. Per maniaci della segretezza.

Caso 2: codice auto-commentante

int sendMessageToDeviceXYZ(
DeviceXYZ *p_deviceXYZ)
{
int number_sent = 0;

if (p_deviceXYZ->message_to_send[0]) {
snprintf(p_deviceXYZ->buffer_tx,
sizeof(p_deviceXYZ->buffer_tx), "data: %s",
p_deviceXYZ->message_to_send);

if ((number_sent = sendDeviceData(p_deviceXYZ->channel_tx,
(char *)p_deviceXYZ->buffer_tx,
strlen(p_deviceXYZ->buffer_tx))) == -1) {
return -1;
}

p_deviceXYZ->message_to_send[0] = 0;
}

return number_sent;
}

Come avrete notato non ci sono commenti, ma il codice è auto-esplicativo e non si va al risparmio. I nomi sono scelti per la massima chiarezza. Per chi non ama i commenti ma vuole essere chiaro a tutti i costi.

Caso 3: codice chiaro e commentato

/* NAME
* sendMsgDev - invia un messaggio al dispositivo
* SYNOPSIS
* int sendMsgDev(
* DevXYZ *dev); // dispositivo destinazione
* DESCRIPTION
* sendMsgDev() invia un messaggio al dispositivo di tipo XYZ. L'argomento <dev>
* è un pointer alla struttura dati del dispositivo.
* RETURN VALUE
* In caso di successo, sendMsgDev() restituirà il numero di caratteri inviati.
* Altrimenti, verrà restituito -1.
*/

int sendMsgDev(
DevXYZ *dev) // dispositivo destinazione
{
// reset numero di char spediti
int n_sent = 0;

// controllo se c'è un messaggio da spedire
if (dev->msg_2_snd[0]) {
// formatto il messaggio nel buffer di trasmissione
snprintf(dev->buf_tx, sizeof(dev->buf_tx), "data: %s", dev->msg_2_snd);

// spedisco il messaggio al dispositivo
if ((n_sent = sendData(dev->chan_tx,
(char *)dev->buf_tx, strlen(dev->buf_tx))) == -1) {
// errore di invio
return -1;
}

// messaggio spedito: reset del contenuto
dev->msg_2_snd[0] = 0;
}

// ritorno il numero di char spediti
return n_sent;
}

Come avrete notato ci sono una intestazione (abbastanza completa, ma si può espandere) e molti brevi commenti. Il codice non è auto-esplicativo ma è sufficientemente chiaro. Per comment-lovers.

Qualcuno potrebbe dire che in questo ultimo caso ci sono fin troppi commenti, e magari molto ovvi: ma io penso che Melius abundare quam deficere e se a qualcuno i commenti non interessano può anche non leggerli! O dà così fastidio che ci siano dei dettagli che solo alcuni possano trovare utili? Bisogna ricordare che l'obiettivo del Caso 3 è che si possa interpretare il codice solo leggendo i commenti, ma non è obbligatorio farlo! Ci sono troppi commenti? E allora non leggerli e non rompermi i cabasisi!

Inutile dirlo: io appartengo alla scuola comment-lovers. Però rispetto anche i seguaci del codice auto-commentante. Sui signori del Absolute No Comment l'unica cosa che posso dire è... No Comment. Comunque, se vi può interessare, il Caso 1 non è una forzatura, e ho visto anche di peggio, non fatemi parlare... Anzi, si, parlerò! E vi racconterò tre aneddoti:

  • Ho sentito con le mie orecchie (true story) frasi di questo tipo: "Ho dovuto fare dei cambi a quel Software che avevi scritto tu: inutile dirti che, grazie al codice chiarissimo e stra-commentato, ho potuto farlo in un attimo. Grazie!" (il "tu" della frase è uno specialista del Codice Chiaro e Commentato, non faccio nomi, ah ah ah).
  • Ho sentito con le mie orecchie (true story) frasi di questo tipo: "Devo mettere le mani sul Software scritto da quel mascalzone di Tizio Caio... Ci metterò una settimana solo per capire dove fare le modifiche! Per fortuna se ne è andato, ma ci ha lasciato una bella eredità..." (il sig."Tizio Caio" è uno specialista del codice Absolute No Comment, ometto il nome vero...).
  • Ho lavorato in posti dove ho visto (giuro!file di migliaia di linee non auto-commentanti e senza neanche un commento (orrore! orrore!).

Ecco, so che quanto sto per dire vi sembrerà un po' radicale (e in effetti lo è), ma lo dirò lo stesso: se quanto ho scritto nell'articolo vi da fastidio o vi sembra inutile può essere dovuto solo a due motivi:

  1. Sicuramente non avete mai lavorato su Software scritto da altri... siete proprio fortunati!
  2. Siete quel mascalzone di Tizio Caio (o siete uguali a lui).

Ok, per oggi può bastare. Chiedo scusa se ho offeso qualcuno ma, tutto sommato, chi si è offeso ha la coda di paglia e se lo merita, ah ah ah.

Ciao e al prossimo post! 

giovedì 11 luglio 2024

Furiosa Go
come spedire una struttura in Go - pt.2

Smeg: Chi sono?
Dementus: Qualcuno competente ed eccessivamente risentito.
Smeg: Cosa pensi che vogliano?
Dementus: Me, senza il mio equipaggio.

Ed eccoci di nuovo sul pezzo. E, visto che si tratta della seconda parte dell'articolo Fury Go, non posso non agganciarmi allo splendido prequel di Mad Max: Fury Road, e cioè a Furiosa: A Mad Max Saga diretto sempre dal Maestro George Miller. E, in effetti, anche questo articolo è un prequel: nell'altro avevo descritto come spedire una struttura in Go via IPC socket (UNIX domain socket), e il Software l'avevo scritto partendo da una mia versione "base" che inviava solo dei semplici testi. Ed ecco, in questa seconda parte vi mostrerò, come prequel, la versione base, dimostrandovi, come promesso, che le prestazioni buone ma non eccellenti del benchmark erano dovute (spoiler) solo alla codifica della struttura, perché con i soli testi il Go va come un treno!

...adesso vi faccio vedere cosa sa fare il Go...

Ok, e allora andiamo con il prequel, descrivendo la versione base che ho scritto prima di quella che usa le strutture. Siamo pronti? Si? E allora partiamo direttamente con il codice!

// reader.go - main processo figlio: è un reader (un server) su IPC socket
package main

import (
"bufio"
"fmt"
"net"
"os"
"time"
)

// funzione main
func 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 listen
fmt.Println(err)
return
}

// prenoto la chiusura del listener e rimuovo (eventualmente) il file di scambio
defer lner.Close()
defer os.Remove("./myipcs")

// accetta connessioni da un writer entrante
conn, err := lner.AcceptUnix()
if err != nil {
// errore accept
fmt.Println(err)
return
}

// set time di partenza per calcolare il tempo impiegato
start := time.Now()

// loop di lettura messaggi dal writer
n_msg := 0
connrdr := bufio.NewReader(conn) // reader sulla connessione
for {
// leggo con il conn reader
client_msg, err := connrdr.ReadString('\n')
if err != nil {
// errore di lettura
fmt.Println(err)
return
}

// test numero messaggi per forzare l'uscita
n_msg++
if n_msg == 2000000 {
// il processo chiude la connessione ed esce per numero raggiunto
fmt.Printf("reader: ultimo messaggio ricevuto: %s", client_msg)
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 socket
package main

import (
"fmt"
"net"
"os"
"time"
)

// funzione main
func main() {

// mi assicuro che il writer parta dopo il reader
fmt.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 dial
fmt.Println(err)
return
}

// loop di scrittura messaggi per il reader
var my_text string
index := 0
for {
// test index per forzare l'uscita
if index == 2000000 {
// il processo chiude la connessione ed esce per indice raggiunto
fmt.Printf("writer: processo %d terminato (text=%s messaggi=%d)\n",
os.Getpid(), my_text, index)
conn.Close()
return
}

// compongo il messaggio e lo invio
index++
my_text = fmt.Sprintf("un-messaggio-di-test:%d\n", index)

// invio il messaggio al server remoto
_, err = conn.Write([]byte(my_text))
if err != nil {
fmt.Println("errore di invio: ", err)
return
}
}
}

Come avrete notato dalla descrizione nella prima linea (e anche dal codice, spero!) ho usato anche questa volta gli IPC socket. Poi ho anche scritto la versione con i Network Socket, ma non vi mostrerò il codice perché è quasi identico. Effettivamente, per la magia del Go, il codice è semplicissimo rispetto alla analoga versione in C vista qui (in questo caso era la versione "fast"), ed è anche quasi identico alla versione con le strutture dello scorso articolo. Come sempre il codice è stra-commentato, e credo che possa essere facilmente compreso anche da chi non conosce il Go, però mi interessa, a questo punto, aggiungere qualche dettaglio per far notare le (poche) differenze rispetto a quello dello scorso articolo.

Cominciamo, allora, con il writer: in entrambe versioni si crea, inizialmente, un oggetto "connessione" conn:

// connessione al server remoto sul file di scambio "myipcs"
addr := net.UnixAddr{Name: "./myipcs", Net: "unix"}
conn, err := net.DialUnix("unix", nil, &addr)

Poi, nella versione con le strutture, si invia il messaggio attraverso il codec encoding/gob passandogli la connessione:

// set encoder e spedizione dall'encoder
encoder := gob.NewEncoder(conn)
err = encoder.Encode(message)

Invece, nella versione base si scrive, direttamente, con l'oggetto connessione creato all'inizio:

// invio il messaggio al server remoto
_, err = conn.Write([]byte(my_text))

E ora passiamo al reader: anche qui, in entrambe versioni, si crea un oggetto "connessione" conn:

// accetta connessioni da un writer entrante
conn, err := lner.AcceptUnix()

Poi, nella versione con le strutture, si riceve il messaggio attraverso il codec passandogli la connessione:

// set decoder e ricezione dal decoder
decoder := gob.NewDecoder(conn)
decoder.Decode(&message)

Invece, nella versione base, si legge con un oggetto "Reader" (della libreria bufio) creato sulla connessione vista sopra:

connrdr := bufio.NewReader(conn) // reader sulla connessione
for {
// leggo con il conn reader
client_msg, err := connrdr.ReadString('\n')

Come avrete notato le differenze sono poche ma significative. Notare anche che nella versione con le strutture il testo del messaggio è una stringa dentro la struttura Message, mentre nella versione base il messaggio è direttamente una stringa terminata con un "newline": questo è un particolare importante, perché in fase di ricezione con la funzione ReadString appena vista è necessario specificare qual'è il terminatore di stringa.

E vabbé, so che siete curiosi, è ora di passare ai risultati! Di seguito i risultati dei benchmark delle "versioni base" che inviano solo testi in Go:

aldo@Linux $ cd ../go-ipcsocketbase/
aldo@Linux $ ./processes
sono il padre (15381): attendo la terminazione dei figli
sono il figlio 1 (15382): eseguo il nuovo processo
sono il figlio 2 (15383): eseguo il nuovo processo
processo 15382 partito (reader)
processo 15383 partito (writer)
writer: processo 15383 terminato (text=un-messaggio-di-test:2000000
messaggi=2000000)
reader: ultimo messaggio ricevuto: un-messaggio-di-test:2000000
reader: processo 15382 terminato (messaggi=2000000 tempo totale:3.058s)
sono il padre (15381): figlio 15382 terminato (0)
sono il padre (15381): figlio 15383 terminato (0)
./processes: processi terminati
aldo@Linux $ cd ../go-socketbase/
aldo@Linux $ ./processes
sono il padre (15408): attendo la terminazione dei figli
sono il figlio 1 (15409): eseguo il nuovo processo
sono il figlio 2 (15410): eseguo il nuovo processo
processo 15410 partito (writer)
processo 15409 partito (reader)
writer: processo 15410 terminato (text=un-messaggio-di-test:2000000
messaggi=2000000)
reader: ultimo messaggio ricevuto: un-messaggio-di-test:2000000
reader: processo 15409 terminato (messaggi=2000000 tempo totale:8.46s)
sono il padre (15408): figlio 15409 terminato (0)
sono il padre (15408): figlio 15410 terminato (0)
./processes: processi terminati

A questo punto i risultati parlano senza temi di smentite: i tempi realizzati sono decisamente migliori di quelli mostrati per le versioni che spedivano strutture complesse, quindi è evidente che la maggior parte del tempo di CPU se la mangiava la libreria specializzata encoding/gob, che funziona bene però, a quanto pare, non è un fulmine. E, grazie ai test appena mostrati sopra si può affermare (come anticipato nella prima parte dell'articolo) che il Go è un linguaggio notevolmente veloce, alla faccia di chi pensa il contrario... Notare che la versione IPC con i suoi 3.058s è addirittura veloce come la versione in C (che impiegava 3.309s ma con messaggi leggermente più lunghi a causa della presenza dell'indice)! Anche in questo caso (come già nello scorso articolo) la versione con i Network socket è un po' più lenta (8.46s) ma è, comunque, sufficientemente veloce.

Ok, credo che, per il momento si può chiudere la parentesi Go sulla comunicazione tra processi: credo che i risultati siano stati interessanti, specialmente per il fatto di avere confrontato codici analoghi per C e Go. Non so di cosa parlerò` prossimamente: in questo momento ho in mente solo le prossime (meritate) vacanze. Ci sentiremo più avanti, ben rilassati e pronti per nuove avventure in C (o in Go...)!

Ciao, e al prossimo post!

sabato 22 giugno 2024

Fury Go
come spedire una struttura in Go - pt.1

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 messaggi
typedef struct {
unsigned long index; // indice dei dati
char 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 socket
package main

import (
"encoding/gob"
"fmt"
"net"
"os"
"time"
)

// struttura Message per i messaggi
type Message struct {
Index int // indice dei dati
Text string // testo dei dati
}

// funzione main
func 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 listen
fmt.Println(err)
return
}

// prenoto la chiusura del listener e rimuovo (eventualmente) il file di scambio
defer lner.Close()
defer os.Remove("./myipcs")

// accetta connessioni da un writer entrante
conn, err := lner.AcceptUnix()
if err != nil {
// errore accept
fmt.Println(err)
return
}

// set time di partenza per calcolare il tempo impiegato
start := time.Now()

// loop di lettura messaggi dal writer
n_msg := 0
var message Message
for {
// set decoder e ricezione dal decoder
decoder := gob.NewDecoder(conn)
decoder.Decode(&message)

// test numero messaggi per forzare l'uscita
n_msg++
if n_msg == 2000000 {
// il processo chiude la connessione ed esce per numero raggiunto
fmt.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 socket
package main

import (
"encoding/gob"
"fmt"
"net"
"os"
"time"
)

// struttura Message per i messaggi
type Message struct {
Index int // indice dei dati
Text string // testo dei dati
}

// funzione main
func main() {

// mi assicuro che il writer parta dopo il reader
fmt.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 dial
fmt.Println(err)
return
}

// loop di scrittura messaggi per il reader
var message Message
message.Index = 0
for {
// test index per forzare l'uscita
if message.Index == 2000000 {
// il processo chiude la connessione ed esce per indice raggiunto
fmt.Printf("writer: processo %d terminato (text=%s messaggi=%d)\n",
os.Getpid(), message.Text, message.Index)
conn.Close()
return
}

// compongo il messaggio e lo invio
message.Index++
message.Text = fmt.Sprintf("un-messaggio-di-test:%d", message.Index)

// set encoder e spedizione dall'encoder
encoder := 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 go-fastipcsocket/
aldo@Linux $ ./processes
sono il padre (18903): attendo la terminazione dei figli
sono il figlio 1 (18904): eseguo il nuovo processo
sono il figlio 2 (18905): eseguo il nuovo processo
processo 18905 partito (writer)
processo 18904 partito (reader)
writer: processo 18905 terminato (text=un-messaggio-di-test:2000000 messaggi=2000000)
sono il padre (18903): figlio 18905 terminato (0)
reader: ultimo messaggio ricevuto: un-messaggio-di-test:2000000
reader: processo 18904 terminato (messaggi=2000000 tempo totale:13.081s)
sono il padre (18903): figlio 18904 terminato (0)
./processes: processi terminati
aldo@Linux $ cd fastipcsocket/
aldo@Linux $ ./processes
sono il padre (14990): attendo la terminazione dei figli
sono il figlio 1 (14991): eseguo il nuovo processo
sono il figlio 2 (14992): eseguo il nuovo processo
processo 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:2000000
reader: 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:

  1. 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).
  2. 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.
  3. 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!