2016-06-03 41 views
6

Sono stato incaricato di sostituire il codice C++ per Go e sono abbastanza nuovo per le API Go. Sto usando gob per codificare centinaia di voci chiave/valore su pagine disco, ma la codifica gob è troppo gonfia che non è necessaria.Serializzazione Go efficiente di struct su disco

package main 

import (
    "bytes" 
    "encoding/gob" 
    "fmt" 
) 
type Entry struct { 
    Key string 
    Val string 
} 

func main() { 
    var buf bytes.Buffer 
    enc := gob.NewEncoder(&buf) 
    e := Entry { "k1", "v1" } 
    enc.Encode(e) 
    fmt.Println(buf.Bytes()) 
} 

Questo produce un sacco di troppo grosso che non ho bisogno di:

[35 255 129 3 1 1 5 69 110 116 114 121 1 255 130 0 1 2 1 3 75 101 121 1 12 0 1 3 86 97 108 1 12 0 0 0 11 255 130 1 2 107 49 1 2 118 49 0] 

Voglio serializzare len di ogni stringa seguito da byte prime come:

[0 0 0 2 107 49 0 0 0 2 118 49] 

Sono salvando milioni di voci in modo che l'ulteriore ingombro nella codifica aumenti le dimensioni del file di circa x10.

Come posso serializzarlo su quest'ultimo senza codifica manuale?

risposta

8

Utilizzare protobuf per codificare in modo efficiente i dati.

https://github.com/golang/protobuf

vostro principale sarebbe simile a questa:

package main 

import (
    "fmt" 
    "log" 

    "github.com/golang/protobuf/proto" 
) 

func main() { 
    e := &Entry{ 
     Key: proto.String("k1"), 
     Val: proto.String("v1"), 
    } 
    data, err := proto.Marshal(e) 
    if err != nil { 
     log.Fatal("marshaling error: ", err) 
    } 
    fmt.Println(data) 
} 

Si crea un file, example.proto in questo modo:

package main; 

message Entry { 
    required string Key = 1; 
    required string Val = 2; 
} 

di generare il codice vanno dal file proto eseguendo:

$ protoc --go_out=. *.proto 

È possibile esaminare il file generato, se lo si desidera.

È possibile eseguire e vedere l'output risultati:

$ go run *.go 
[10 2 107 49 18 2 118 49] 
15

Se si zip un file chiamato a.txt contenente il testo "hello" (che è 5 caratteri), il risultato zip saranno circa 115 byte. Questo significa che il formato zip non è efficace per comprimere i file di testo? Certamente no. C'è un overhead . Se il file contiene "hello" un centinaio di volte (500 byte), lo zippamento comporterà il file 120 byte! 1x"hello" => 115 byte, 100x"hello" => 120 byte! Abbiamo aggiunto 495 byte, eppure le dimensioni compresse sono aumentate solo di 5 byte.

Qualcosa di simile sta accadendo con il pacchetto encoding/gob:

L'implementazione compila un codec personalizzato per ogni tipo di dati nel flusso ed è più efficace quando un singolo encoder viene utilizzato per trasmettere un flusso di valori, ammortizzando il costo della compilazione.

Quando si "prima" serializzare un valore di un tipo, la definizione del tipo ha anche essere inclusi/trasmissione, in modo che il decoder può correttamente interpretare e decodificare il flusso:

Un flusso di gocce è auto-descrivente.Ogni elemento di dati nel flusso è preceduto da una specifica del suo tipo, espressa in termini di un piccolo insieme di tipi predefiniti. ritorno

Let al tuo esempio:

var buf bytes.Buffer 
enc := gob.NewEncoder(&buf) 
e := Entry{"k1", "v1"} 
enc.Encode(e) 
fmt.Println(buf.Len()) 

Esso stampa:

48 

Ora diamo codificare un po 'di più del tipo stesso:

enc.Encode(e) 
fmt.Println(buf.Len()) 
enc.Encode(e) 
fmt.Println(buf.Len()) 

Ora l'uscita è:

60 
72 

Provalo su Go Playground.

Analisi dei risultati:

ulteriori valori dello stesso Entry tipo unico costo 12 byte, mentre la prima è 48 byte perché è anche incluso la definizione di tipo (che è ~ 26 byte), ma questo è un overhead una tantum .

Quindi sostanzialmente si trasmettere 2 string s: "k1" e "v1" che sono 4 byte, e la lunghezza del string s deve anche essere inclusi, utilizzando 4 byte (dimensione del int su architetture a 32-bit) vi dà i 12 byte , che è il "minimo". (Sì, potresti usare un tipo più piccolo per la lunghezza, ma questo avrebbe i suoi limiti. Una codifica a lunghezza variabile sarebbe una scelta migliore per i numeri piccoli, vedi pacchetto encoding/binary.)

Tutto sommato, encoding/gob fa un bel buon lavoro per le tue esigenze. Non farti ingannare dalle impressioni iniziali.

Se questo 12 byte per una Entry è troppo "molto" per te, si può sempre avvolgere il flusso in un compress/flate o compress/gzip scrittore di ridurre ulteriormente le dimensioni (in cambio di lenti codifica/decodifica e requisiti di memoria leggermente superiori per il processo).

Dimostrazione:

Testiamo i 3 soluzioni:

  • Utilizzando un'uscita "nudo" (senza compressione)
  • Utilizzo compress/flate per comprimere l'uscita di encoding/gob
  • Uso compress/gzip comprimere l'output di encoding/gob

Scriveremo mille voci, cambiando chiavi e valori di ciascuno, essendo "k000", "v000", "k001", "v001" ecc Questo significa che la dimensione non compressa di un Entry è 4 byte + 4 byte + 4 byte + 4 byte = 16 byte (2x 4 byte di testo, 2x4 byte di lunghezza).

Il codice simile a questo:

names := []string{"Naked", "flate", "gzip"} 
for _, name := range names { 
    buf := &bytes.Buffer{} 

    var out io.Writer 
    switch name { 
    case "Naked": 
     out = buf 
    case "flate": 
     out, _ = flate.NewWriter(buf, flate.DefaultCompression) 
    case "gzip": 
     out = gzip.NewWriter(buf) 
    } 

    enc := gob.NewEncoder(out) 
    e := Entry{} 
    for i := 0; i < 1000; i++ { 
     e.Key = fmt.Sprintf("k%3d", i) 
     e.Val = fmt.Sprintf("v%3d", i) 
     enc.Encode(e) 
    } 

    if c, ok := out.(io.Closer); ok { 
     c.Close() 
    } 
    fmt.Printf("[%5s] Length: %5d, average: %5.2f/Entry\n", 
     name, buf.Len(), float64(buf.Len())/1000) 
} 

uscita:

[Naked] Length: 16036, average: 16.04/Entry 
[flate] Length: 4123, average: 4.12/Entry 
[ gzip] Length: 4141, average: 4.14/Entry 

Prova sul Go Playground.

Come si può vedere: l'uscita "nuda" è 16.04 bytes/Entry, appena poco sopra la dimensione calcolata (a causa dell'overhead minuscolo di una volta discusso sopra).

Quando si utilizza flate o gzip per comprimere l'output, è possibile ridurre le dimensioni dell'output a circa 4.13 bytes/Entry, che corrisponde a circa il 26% della dimensione teorica, sono sicuro che soddisfi voi. (Si noti che con i dati di "vita reale" il rapporto di compressione sarebbe probabilmente molto più alto in quanto le chiavi e i valori che ho usato nel test sono molto simili e quindi molto bene comprimibili, comunque il rapporto dovrebbe essere intorno al 50% con i dati reali).

+1

Impressionante analisi (ammiro sempre le tue risposte) ma in questo caso particolare mi sembra di spiegare la scienza missilistica a un bambino che ha chiesto perché la sua bicicletta a tre ruote è un po 'lenta. ;-) Anche se penso che 'gob' abbia decisamente dei suoi usi, per un compito così semplice che l'OP sembra avere, sono sicuro che una semplice reimplementazione di ciò che è già stato fatto in C++ è giustificata. Un altro aspetto positivo di questo approccio è che il nuovo codice sarà comparabile con i dati legacy che hanno. – kostix

+0

@kostix Questo è stato il mio primo pensiero e impressione anche sulla domanda, ma poi ho visto la sua ultima riga: _ "senza codifica manuale" _... Ecco perché ho deciso di rimanere con 'encoding/gob'. – icza

3

"La codifica manuale", sei così spaventato, è banalmente fatto in Go utilizzando lo standard encoding/binary package.

appari per memorizzare i valori di lunghezza stringa come interi a 32 bit in formato big-endian, quindi si può solo andare avanti e fare proprio questo in Go:

package main 

import (
    "bytes" 
    "encoding/binary" 
    "fmt" 
    "io" 
) 

func encode(w io.Writer, s string) (n int, err error) { 
    var hdr [4]byte 
    binary.BigEndian.PutUint32(hdr[:], uint32(len(s))) 
    n, err = w.Write(hdr[:]) 
    if err != nil { 
     return 
    } 
    n2, err := io.WriteString(w, s) 
    n += n2 
    return 
} 

func main() { 
    var buf bytes.Buffer 

    for _, s := range []string{ 
     "ab", 
     "cd", 
     "de", 
    } { 
     _, err := encode(&buf, s) 
     if err != nil { 
      panic(err) 
     } 
    } 
    fmt.Printf("%v\n", buf.Bytes()) 
} 

Playground link.

Si noti che in questo esempio sto scrivendo in un buffer di byte, ma questo è solo a scopo dimostrativo — dal encode() scrive a un io.Writer, è possibile passare un file aperto, un socket di rete e quant'altro attuazione di tale interfaccia.

+0

Dopo il tuo commento volevo persino suggerire di andare avanti e pubblicare la versione "manuale". +1. – icza