Hlavní navigace

Komunikace realizovaná binárním formátem MessagePack (dokončení)

25. 1. 2022
Doba čtení: 31 minut

Sdílet

 Autor: Depositphotos
Ukážeme si, že datové struktury pole a mapa jsou v Message Packu heterogenní (což odpovídá původnímu textovému JSONu). Taktéž provedeme porovnání mezi formáty JSON, XML, BSON, gob a právě Message Packem.

Obsah

1. Komunikace realizovaná binárním formátem MessagePack (dokončení)

2. Serializace polí a řezů

3. Serializace řezů

4. Mapy jako heterogenní datový typ

5. Uložení heterogenní mapy v binárním formátu

6. Uložení heterogenního pole do binárního formátu

7. Uložení časového razítka

8. Message Pack – it's like JSON. But fast and small

9. Serializace vektoru hodnot typu float64

10. Velikosti souborů se serializovaným vektorem

11. Serializace binárního stromu

12. Velikosti souborů se serializovaným binárním stromem

13. Serializace mapy, jejímiž klíči i hodnotami jsou řetězce

14. Velikosti souborů se serializovanou mapou

15. Rychlost serializace

16. Benchmark měřící rychlost serializace

17. Výsledky benchmarku

18. Kam dál?

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Komunikace realizovaná binárním formátem MessagePack (dokončení)

Na úvodní článek o binárním serializačním formátu Message Pack dnes navážeme. Nejprve si na praktických příkladech ukážeme, že datové struktury pole a mapa jsou v Message Packu heterogenní, což odpovídá původnímu textovému JSONu, kterému se binární formát JSON snaží svými možnostmi přiblížit. Dále si ukážeme, jakým způsobem se serializují časová razítka. To je důležitý datový typ, jehož absence ve specifikaci JSONu (a nepřímo i absence příslušného literálu v JavaScriptu) způsobuje, že se časová razítka v JSONu ukládají různými, mnohdy dosti obskurními způsoby. A na závěr provedeme porovnání mezi formáty JSON, XML, BSON, gob a právě Message Packem. Zaměříme se jak na velikosti dat po jejich serializaci, tak i na rychlost serializace, což v některých aplikacích může být limitujícím faktorem.

2. Serializace polí a řezů

Formát Message Pack podporuje ukládání polí, která jsou (jak uvidíme dále) heterogenní. To znamená, že typ každého prvku pole může být prakticky libovolný a nezávislý na typu ostatních prvků. To ostatně velmi dobře odpovídá pojetí polí v JavaScriptu a tím pádem i ve formátu JSON, jehož myšlenky jsou použity v binárním formátu Message Pack. Pokud ovšem budeme Message Pack používat například v programovacím jazyku Go, bude pro nás důležitější vědět, jakým způsobem se serializují řezy (slice) a nikoli pole, protože s řezy se v praxi setkáme mnohem častěji než s klasickými poli. Bude tedy dobré si vyzkoušet, jak se od sebe bude lišit serializace pole a řezu.

Následující demonstrační příklad jsme si již ukázali minule, takže si jen ve stručnosti řekněme, že se v příkladu vytváří klasické pole s tisíci prvky typu uint. Prvky tohoto pole jsou inicializovány na hodnoty 0, 1, 2 až 999. Následně je celé pole serializováno do binárního formátu Message Pack:

package main
 
import (
        "log"
        "os"
 
        "github.com/ugorji/go/codec"
)
 
const filename = "/tmp/array16B.bin"
 
func main() {
        // vytvořit soubor s binárními daty
        fout, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
        if err != nil {
                log.Fatal(err)
        }
        defer fout.Close()
 
        log.Print("Output file created")
 
        // handler
        var handler codec.MsgpackHandle
 
        // objekt realizující zakódování dat
        encoder := codec.NewEncoder(fout, &handler)
 
        log.Print("Encoder created")
 
        const N = 1000
        var values [N]uint
 
        for i := 0; i < N; i++ {
                values[i] = uint(i)
        }
 
        // zakódování dat
        err = encoder.Encode(values)
        if err != nil {
                log.Fatal(err)
        }
 
        log.Print("Done")
}

Výsledkem bude po spuštění předchozího programu binární soubor nazvaný „array16B.bin“, jehož velikost by měla být rovna 2619 bajtům. Samozřejmě můžeme prozkoumat obsah tohoto souboru. Celočíselné prvky s hodnotami 0 až 999 budou uloženy následujícím způsobem:

  1. Hodnoty menší než 128 budou uloženy v jediném bajtu, což je současně nejúspornější možná varianta
  2. Hodnoty mezi 128 až 255 budou uloženy jako dvojice 0×cc + osmibitová hodnota
  3. Hodnoty větší než 255 budou uloženy jako trojice 0×cd + 16bitová hodnota

Můžeme se o tom snadno přesvědčit analýzou vytvořeného binárního souboru:

$ od -A x -t x1 -v array16B.bin

Typ + délka pole + prvních 128 prvků:

000000 dc 03 e8 00 01 02 03 04 05 06 07 08 09 0a 0b 0c
000010 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c
000020 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c
000030 2d 2e 2f 30 31 32 33 34 35 36 37 38 39 3a 3b 3c
000040 3d 3e 3f 40 41 42 43 44 45 46 47 48 49 4a 4b 4c
000050 4d 4e 4f 50 51 52 53 54 55 56 57 58 59 5a 5b 5c
000060 5d 5e 5f 60 61 62 63 64 65 66 67 68 69 6a 6b 6c
000070 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a 7b 7c
000080 7d 7e 7f

Následují prvky s hodnotou 128 až 255:

000080          cc 80 cc 81 cc 82 cc 83 cc 84 cc 85 cc
000090 86 cc 87 cc 88 cc 89 cc 8a cc 8b cc 8c cc 8d cc
0000a0 8e cc 8f cc 90 cc 91 cc 92 cc 93 cc 94 cc 95 cc

A poté zbylé prvky:

000180 fe cc ff cd 01 00 cd 01 01 cd 01 02 cd 01 03 cd
000190 01 04 cd 01 05 cd 01 06 cd 01 07 cd 01 08 cd 01
0001a0 09 cd 01 0a cd 01 0b cd 01 0c cd 01 0d cd 01 0e
0001b0 cd 01 0f cd 01 10 cd 01 11 cd 01 12 cd 01 13 cd

3. Serializace řezů

Nyní předchozí demonstrační příklad nepatrně upravíme, a to takovým způsobem, aby se namísto polí použil řez:

const N = 1000
var values []uint
 
for i := 0; i < N; i++ {
        values = append(values, uint(i))
}

Interně se v případě řezu jedná o referenci na automaticky vytvořené pole nebo na pole, které je explicitně alokovanáno programátorem. Každý řez je v operační paměti uložen ve formě trojice hodnot (jde tedy o záznam – struct či record):

  1. Ukazatele (reference) na zvolený prvek pole s daty, ke kterým přes řez přistupujeme.
  2. Délky řezu, tj. počtu prvků.
  3. Kapacity řezu (do jaké míry může řez narůstat v důsledku přidávání dalších prvků).

Tato interní struktura řezů s sebou přináší několik zajímavých důsledků. Je totiž možné, aby existovalo větší množství řezů ukazujících na obecně různé prvky jediného pole. Pokud nyní změníme prvek v jednom řezu, znamená to, že se vlastně modifikuje obsah původního pole a i ostatní řezy nový prvek uvidí. Co je však užitečnější – s řezy jako s datovým typem se velmi snadno pracuje; řezy mohou být předávány do funkcí, vráceny z funkcí atd. Proto se s řezy setkáme mnohem častěji než s klasickými poli.

Po úpravě demonstračního příkladu tak, aby se namísto pole použil řez, získáme soubor nazvaný „array16C.bin“. Velikost i obsah tohoto souboru bude stejný se souborem získaným příkladem uvedeným v předchozí kapitole, o čemž se můžeme velmi snadno přesvědčit:

$ cmp -l -b array16B.bin array16C.bin
 
(nic se nevypíše)
Poznámka: to ovšem znamená, že se z řezu uloží „pouze“ vlastní pole s daty. Ve skutečnosti je ještě uložena skutečná délka pole/řezu, ovšem nikoli již kapacita. To ovšem dává smysl, protože formát Message Pack je určen pro mnoho jazyků a současně nepodporuje všechny datové typy všech podporovaných jazyků (ostatně stejně jako JSON).

Upravený kód demonstračního příkladu bude vypadat následovně:

package main
 
import (
        "log"
        "os"
 
        "github.com/ugorji/go/codec"
)
 
const filename = "/tmp/array16C.bin"
 
func main() {
        // vytvořit soubor s binárními daty
        fout, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
        if err != nil {
                log.Fatal(err)
        }
        defer fout.Close()
 
        log.Print("Output file created")
 
        // handler
        var handler codec.MsgpackHandle
 
        // objekt realizující zakódování dat
        encoder := codec.NewEncoder(fout, &handler)
 
        log.Print("Encoder created")
 
        const N = 1000
        var values []uint
 
        for i := 0; i < N; i++ {
                values = append(values, uint(i))
        }
 
        // zakódování dat
        err = encoder.Encode(values)
        if err != nil {
                log.Fatal(err)
        }
 
        log.Print("Done")
}

4. Mapy jako heterogenní datový typ

Mapy (neboli asociativní pole) jsou ve formátu Message Pack heterogenním datovým typem, což znamená, že jak klíče, tak i hodnoty jednotlivých dvojic mohou být prakticky libovolného typu, nezávisle na typech klíčů a hodnot jiných dvojic uložených ve stejné mapě. Na jednu stranu se tedy jedná o napodobení map ve formátu JSON, ovšem ve skutečnosti není prakticky vůbec omezen typ klíčů – což může způsobovat potíže v těch programovacích jazycích, kde nějaká omezení existují.

Typicky se ovšem u map používají jako klíče řetězce. Serializaci takové mapy si ukážeme v následujícím demonstračním příkladu:

package main
 
import (
        "log"
        "os"
 
        "github.com/ugorji/go/codec"
)
 
const filename = "/tmp/map.bin"
 
func main() {
        // vytvořit soubor s binárními daty
        fout, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
        if err != nil {
                log.Fatal(err)
        }
        defer fout.Close()
 
        log.Print("Output file created")
 
        // handler
        var handler codec.MsgpackHandle
 
        // objekt realizující zakódování dat
        encoder := codec.NewEncoder(fout, &handler)
 
        log.Print("Encoder created")
 
        var m map[string]int = make(map[string]int)
        m["foo"] = 1
        m["bar"] = 2
 
        // zakódování dat
        err = encoder.Encode(m)
        if err != nil {
                log.Fatal(err)
        }
 
        log.Print("Done")
}

Výsledkem bude soubor „map.bin“, jehož obsah je následující:

$ od -A x -t x1z -v map.bin
000000 82 a3 66 6f 6f 01 a3 62 61 72 02                 >..foo..bar.<
00000b

Mapa obsahuje dvě dvojice, což je malý počet. Z tohoto důvodu je typ (mapa) i počet dvojic klíč-hodnota uložena v jediném bajtu 0×80+0×02=0×82. Následuje obsah dvojic klíč-hodnota. Tyto dvojice jsou uloženy za sebou, takže mapa je interně (v binárním formátu) vlastně běžným polem, ovšem s odlišnou hlavičkou:

Bajty Význam
a3 66 6f 6f řetězec „foo“ o délce tří bajtů
01 malé celé číslo 1
a3 62 61 72 řetězec „bar“ o délce tří bajtů
02 malé celé číslo 2

5. Uložení heterogenní mapy v binárním formátu

Nic nám ovšem nebrání v použití mapy s prvky, jejichž typy se od sebe budou navzájem lišit. Takovou mapu lze vytvořit i v jazyce Go, a to konkrétně s využitím typu map[klíče]interface{}, tedy tak, jak je to ukázáno v dalším příkladu:

package main
 
import (
        "log"
        "os"

        "github.com/ugorji/go/codec"
)
 
const filename = "/tmp/map2.bin"
 
func main() {
        // vytvořit soubor s binárními daty
        fout, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
        if err != nil {
                log.Fatal(err)
        }
        defer fout.Close()
 
        log.Print("Output file created")
 
        // handler
        var handler codec.MsgpackHandle
 
        // objekt realizující zakódování dat
        encoder := codec.NewEncoder(fout, &handler)
 
        log.Print("Encoder created")
 
        var m map[string]interface{} = make(map[string]interface{})
        m["foo"] = 1
        m["bar"] = 2
        m["baz"] = 1000000
        m["wee"] = "test"
        m["array"] = []int{1, 2, 3}
        m["map"] = map[string]string{
                "one": "jedna",
                "two": "dve",
        }
 
        // zakódování dat
        err = encoder.Encode(m)
        if err != nil {
                log.Fatal(err)
        }
 
        log.Print("Done")
}

Výsledný binární soubor s mapou bude mít délku 62 bajtů a následující obsah:

$ od -A x -t x1z -v map2.bin
 
000000 86 a3 62 61 72 02 a3 62 61 7a d2 00 0f 42 40 a3  >..bar..baz...B@.<
000010 77 65 65 a4 74 65 73 74 a5 61 72 72 61 79 93 01  >wee.test.array..<
000020 02 03 a3 6d 61 70 82 a3 6f 6e 65 a5 6a 65 64 6e  >...map..one.jedn<
000030 61 a3 74 77 6f a3 64 76 65 a3 66 6f 6f 01        >a.two.dve.foo.<
00003e

První bajt v souboru obsahuje hodnotu 0×86, takže víme, že se jedná o mapu s maximálně patnácti prvky (0×8?) a celkový počet prvků (tedy dvojic klíč+hodnota) je roven šesti (0×?6).

Následuje bajt 0×a3 značící řetězec (první tři bity) kratší než 32 bajtů, za nímž následují znaky řetězce zakódované do bajtů 0×62 0×61 0×72. To je hodnota prvního klíče. Pod tímto klíčem je zapsaná celočíselná hodnota 2 reprezentovaná jediným bajtem 0×02. Naprosto stejným způsobem jsou zapsány i další dvojice klíč-hodnota.

Povšimněte si, že jednou hodnotou uloženou do mapy je další (vnořená) mapa, která začíná bajtem 0×82 – jedná se tedy o mapu se dvěma dvojicemi klíč+hodnota.

6. Uložení heterogenního pole do binárního formátu

Nepatrnou úpravou předchozího příkladu vytvoříme heterogenní pole, tj. pole, v němž mohou mít jeho prvky jakoukoli hodnotu nezávislou na typech ostatních prvků pole. Nejprve se podívejme na zdrojový kód tohoto demonstračního příkladu (v něm sice pracujeme s řezem a nikoli s polem, ovšem víme již, že se řezy serializují naprosto stejným způsobem jako běžná pole):

package main
 
import (
        "log"
        "os"
 
        "github.com/ugorji/go/codec"
)
 
const filename = "/tmp/array16D.bin"
 
func main() {
        // vytvořit soubor s binárními daty
        fout, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
        if err != nil {
                log.Fatal(err)
        }
        defer fout.Close()
 
        log.Print("Output file created")
 
        // handler
        var handler codec.MsgpackHandle
 
        // objekt realizující zakódování dat
        encoder := codec.NewEncoder(fout, &handler)
 
        log.Print("Encoder created")
 
        var values []interface{}
 
        values = append(values, 1)
        values = append(values, 100000)
        values = append(values, "test")
        values = append(values, []int{1, 2, 3})
        values = append(values, map[string]string{
                "one": "jedna",
                "two": "dve",
        })
 
        // zakódování dat
        err = encoder.Encode(values)
        if err != nil {
                log.Fatal(err)
        }
 
        log.Print("Done")
}

Výsledkem bude binární soubor nazvaný „array16D.bin“, jehož délka bude 35 bajtů. Oproti mapě z předchozích kapitol je to méně, což je ovšem logické, protože nyní nemusíme ukládat klíče, ale pouze hodnoty (tedy prvky) pole:

$ od -A x -t x1z -v array16D.bin
 
000000 95 01 d2 00 01 86 a0 a4 74 65 73 74 93 01 02 03  >........test....<
000010 82 a3 6f 6e 65 a5 6a 65 64 6e 61 a3 74 77 6f a3  >..one.jedna.two.<
000020 64 76 65                                         >dve<
000023

První bajt s hodnotou 0×95 značí krátké pole (prefix 0×9?) s pěti prvky (0×?5). Ihned poté následují hodnoty prvků pole, z nichž každý s sebou nese i informace o datovém typu:

Bajty Význam
0×01 celočíselná hodnota 1
0×d2 0×00 0×01 0×86 0×a0 celočíselná hodnota 0×186a0 == 100000
0×74 0×65 0×73 0×74 0×93 řetězec „test“ o délce čtyři bajty
0×93 0×01 0×02 0×03 tříprvkové pole celočíselných hodnot 1, 2 a 3
0×82 … mapa se dvěma dvojicemi klíč+hodnota

7. Uložení časového razítka

V praxi je velmi důležité zajistit ukládání popř. přenos časových razítek. Ve formátu JSON na tento důležitý datový typ není pamatováno, takže se setkáme s různými způsoby uložení informací o datu a času – například se lze setkat například s nicneříkajím zápisem „01/02/03“ atd.

Naproti tomu formát Message Pack dokáže s časovými razítky pracovat a dokonce podporuje tři různé způsoby uložení. O jakou hodnotu se jedná lze zjistit z prefixového bajtu:

Prefix Délka Význam Přesnost
0×d6 4 bajty 32bitová hodnota sekund od 1970–01–01 00:00:00 UTC sekundy
0×d7 8 bajtů 34bitová hodnota sekund od 1970–01–01 00:00:00 UTC + počet nanosekund nanosekundy
0×c7 12 bajtů 64bitová hodnota sekund od 1970–01–01 00:00:00 UTC + počet nanosekund nanosekundy

Podívejme se nyní na způsob uložení časového razítka, konkrétně aktuální hodnoty data+času získané ve chvíli, kdy program zavolá funkci time.Now():

package main
 
import (
        "log"
        "os"
        "time"
 
        "github.com/ugorji/go/codec"
)
 
const filename = "/tmp/timestamp.bin"
 
func main() {
        // vytvořit soubor s binárními daty
        fout, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
        if err != nil {
                log.Fatal(err)
        }
        defer fout.Close()
 
        log.Print("Output file created")
 
        // handler
        var handler codec.MsgpackHandle
 
        // důležité - serializace časového razítka ve správném datovém formátu
        handler.WriteExt = true
 
        // objekt realizující zakódování dat
        encoder := codec.NewEncoder(fout, &handler)
 
        log.Print("Encoder created")
 
        t := time.Now()
        log.Print(t)
 
        // zakódování dat
        err = encoder.Encode(t)
        if err != nil {
                log.Fatal(err)
        }
 
        log.Print("Done")
}
Poznámka: nesmíme zapomenout na tento řádek. Pokud by nebyl uveden, bylo by časové razítko uloženo jako prostá sekvence osmi bajtů (tedy jako pole):
// důležité - serializace časového razítka ve správném datovém formátu
handler.WriteExt = true

Po spuštění příkladu by se měl vytvořit binární soubor nazvaný „timestamp.bin“ s délkou desíti bajtů. Obsah tohoto souboru si opět vypíšeme:

$ od -A x -t x1 -v timestamp.bin
 
000000 d7 ff 72 1f e3 88 61 ed 8a 66
00000a

První bajt 0×d7 značí, že se bude jednat o časové razítko uložené v osmi bajtech. Celý záznam by měl vypadat takto:

+--------+--------+--------+--------+--------+------|-+--------+--------+--------+--------+
|  0xd7  |   -1   | nanosec. in 30-bit unsigned int |   seconds in 34-bit unsigned int    |
+--------+--------+--------+--------+--------+------^-+--------+--------+--------+--------+

Druhý bajt je skutečně roven –1 neboli 0×ff. Dalších sedm bajtů (64 bitů) je rozděleno na 30 bitů a 34 bitů. V prvních 30 bitech je desetinná část času v nanosekundách (až do hodnoty 999999999 nanosekund, tedy těsně pod sekundu). A posledních 34 bitů určuje počet sekund od 1.1.1970. Počet nanosekund je asi nezajímavý, ale podívejme se na posledních 34 bitů (resp. postačuje posledních 32 bitů, protože nejvyšší dva bity jsou nulové):

0x61ed8a66 = 1642957414

Což lze převést na příkazové řádce do čitelného formátu:

$ date -d @1642957414
 
Sun 23 Jan 2022 06:03:34 PM CET
Poznámka: 34bitový počet sekund umožňuje vytvářet časová razítka až do 2514–05–30.

8. Message Pack – it's like JSON. But fast and small

Přímo na úvodní stránce o formátu Message Pack se píše, že tento formát dokáže ukládat stejná data jako JSON (což jsme si již ukázali), ale současně by měla být celá operace rychlejší a výsledné soubory (po serializaci) menší. Obě tato tvrzení si samozřejmě musíme ověřit, a to nejenom porovnáním Message Packu s formátem JSON. Do porovnání zahrneme i některé další textové i binární formáty, zejména BSON, XML a Gob. Přitom BSON (nikoli B-JSON) je binární obdobou JSONu, tj. míří do stejné niky jako Message Pack. Naproti tomu XML je většinou určen pro poněkud jiné účely, stejně jako formát Gob neboli Go Objects. V případě Gob se jedná o formát určený primárně pro použití v programovacím jazyku Go, což znamená, že jeho využití je relativně specifické (ukládání rozsáhlých dat, komunikace mezi dvojicí služeb naprogramovaných v Go atd.). Tento formát umožňuje serializaci prakticky jakékoli datové struktury, ovšem je ho možné použít i pro primitivní datové typy, resp. pro jejich hodnoty.

9. Serializace vektoru hodnot typu float64

Nejdříve si vyzkoušíme, jak velké soubory vzniknou po serializaci vektoru s jedním tisícem hodnot typu float64. Tento vektor bude reprezentován jednorozměrným polem, jehož obsah postupně uložíme do:

  1. Binárního formátu Message Back
  2. Binárního formátu BSON
  3. Binárního formátu Gob
  4. Textového formátu JSON v minifikované podobě
  5. Textového formátu JSON v čitelné podobě (s odsazením atd.)
  6. Textového formátu XML v minifikované podobě
  7. Textového formátu XML v čitelné podobě (s odsazením atd.)

Program, který tuto serializaci provede, vypadá následovně:

package main
 
import (
        "bytes"
        "encoding/gob"
        "encoding/json"
        "encoding/xml"
        "fmt"
        "gopkg.in/mgo.v2/bson"
        "io/ioutil"
 
        "github.com/ugorji/go/codec"
)
 
// Vector represents type of data to be serialized into various formats
type Vector []float64
 
func encodeVectorIntoBSON(vector Vector) ([]byte, error) {
        bsonOutput, err := bson.Marshal(vector)
 
        if err != nil {
                return bsonOutput, err
        }
        return bsonOutput, nil
}
 
func encodeVectorIntoJSON(vector Vector) ([]byte, error) {
        jsonOutput, err := json.Marshal(vector)
 
        if err != nil {
                return jsonOutput, err
        }
        return jsonOutput, nil
}
 
func encodeVectorIntoIndentedJSON(vector Vector) ([]byte, error) {
        jsonOutput, err := json.MarshalIndent(vector, "", "    ")
 
        if err != nil {
                return jsonOutput, err
        }
        return jsonOutput, nil
}
 
func encodeVectorIntoXML(vector Vector) ([]byte, error) {
        xmlOutput, err := xml.Marshal(vector)
 
        if err != nil {
                return xmlOutput, err
        }
        return xmlOutput, nil
}
 
func encodeVectorIntoIndentedXML(vector Vector) ([]byte, error) {
        xmlOutput, err := xml.MarshalIndent(vector, "", "    ")
 
        if err != nil {
                return xmlOutput, err
        }
        return xmlOutput, nil
}
 
func encodeVectorIntoGob(vector Vector) ([]byte, error) {
        var buffer bytes.Buffer
        encoder := gob.NewEncoder(&buffer)
 
        err := encoder.Encode(vector)
        if err != nil {
                return buffer.Bytes(), err
        }
        return buffer.Bytes(), nil
}
 
func encodeVectorIntoMsgPack(vector Vector) ([]byte, error) {
        var buffer bytes.Buffer
 
        // handler
        var handler codec.MsgpackHandle
 
        // objekt realizující zakódování dat
        encoder := codec.NewEncoder(&buffer, &bhandler)
 
        // zakódování dat
        err := encoder.Encode(vector)
        if err != nil {
                return buffer.Bytes(), err
        }
        return buffer.Bytes(), nil
}
 
func saveVector(encodedVector []byte, filename string) {
        err := ioutil.WriteFile(filename, encodedVector, 0644)
        if err != nil {
                fmt.Println(err)
        } else {
                fmt.Println("Stored into file", filename)
        }
}
 
func printBufferInfo(buffer []byte) {
        fmt.Println("\nBuffer with encoded vector: ", len(buffer))
}
 
func main() {
        var array [1000]float64
 
        for i := 0; i < len(array); i++ {
                if i == 0 {
                        array[i] = 1.0
                } else {
                        array[i] = 1.0 / float64(i)
                }
        }
 
        var vector Vector = array[:]
 
        encodedVector, err := encodeVectorIntoXML(vector)
        if err != nil {
                fmt.Println(err)
                return
        }
        printBufferInfo(encodedVector)
        saveVector(encodedVector, "/tmp/vector1.xml")
 
        encodedVector, err = encodeVectorIntoIndentedXML(vector)
        if err != nil {
                fmt.Println(err)
                return
        }
        printBufferInfo(encodedVector)
        saveVector(encodedVector, "/tmp/vector2.xml")
 
        encodedVector, err = encodeVectorIntoJSON(vector)
        if err != nil {
                fmt.Println(err)
                return
        }
        printBufferInfo(encodedVector)
        saveVector(encodedVector, "/tmp/vector1.json")
 
        encodedVector, err = encodeVectorIntoIndentedJSON(vector)
        if err != nil {
                fmt.Println(err)
                return
        }
        printBufferInfo(encodedVector)
        saveVector(encodedVector, "/tmp/vector2.json")
 
        encodedVector, err = encodeVectorIntoBSON(vector)
        if err != nil {
                fmt.Println(err)
                return
        }
        printBufferInfo(encodedVector)
        saveVector(encodedVector, "/tmp/vector1.bson")
 
        encodedVector, err = encodeVectorIntoGob(vector)
        if err != nil {
                fmt.Println(err)
                return
        }
        printBufferInfo(encodedVector)
        saveVector(encodedVector, "/tmp/vector1.gob")
 
        encodedVector, err = encodeVectorIntoMsgPack(vector)
        if err != nil {
                fmt.Println(err)
                return
        }
        printBufferInfo(encodedVector)
        saveVector(encodedVector, "/tmp/vector1.bin")

}

10. Velikosti souborů se serializovaným vektorem

Velmi snadno můžeme porovnat velikosti jednotlivých souborů a určit si i pořadí podle velikosti (resp. spíše malosti) výsledků serializace:

Soubor Velikost Pořadí Poznámka
vector1.bin 9003 2 hlavička pole + 1000×(1+8 bajtů)
vector1.bson 12895 3  
vector1.gob 8960 1  
vector1.json 21017 4 minifikovaný
vector2.json 26018 5 čitelný s odsazením
vector1.xml 39016 6 minifikovaný
vector2.xml 40015 7 čitelný s odsazením
Poznámka: výhra formátu Gob je mj. způsobena tím, že se pole ukládají jako homogenní datová struktura, tj. u jednotlivých prvků není nutné specifikovat jejich typ. Tím lze oproti formátu Message Pack ušetřit 1000 bajtů – ovšem z výsledků vidíme, že rozdíl ve velikosti je jen 57 bajtů, což znamená, že Gob svou výhodu plně nevyužil.

Při přenosech dat po síti se může (i transparentně) provádět komprimace dat, takže si ještě pro zajímavost ukažme, jak se budou jednotlivé soubory lišit po komprimaci GZIPem:

Soubor Velikost Pořadí
vector1.bin.gz 5431 2
vector1.bson.gz 6537 3
vector1.gob.gz 5234 1
vector1.json.gz 7559 4
vector2.json.gz 7652 5
vector1.xml.gz 8182 6
vector2.xml.gz 8230 7

11. Serializace binárního stromu

Druhý benchmark je založen na serializaci binárního stromu s 255 uzly. Samotnou konstrukci tohoto stromu (tak, aby byl vyvážený) zajišťuje tato rekurzivní funkce:

func constructTree(bt *BinaryTree, min int, max int) {
        middle := (min + max) / 2
        if min < middle && middle < max {
                fmt.Println(middle)
                bt.Insert(Item(middle))
                constructTree(bt, min, middle)
                constructTree(bt, middle, max)
        }
}

Opět si pochopitelně ukážeme celý zdrojový kód příkladu:

package main
 
import (
        "bytes"
        "encoding/gob"
        "encoding/json"
        "encoding/xml"
        "fmt"
        "gopkg.in/mgo.v2/bson"
        "io/ioutil"
 
        "github.com/ugorji/go/codec"
)
 
type Item int
 
type Node struct {
        Value Item
        Left  *Node
        Right *Node
}
 
type BinaryTree struct {
        Root *Node
}
 
func (bt *BinaryTree) Insert(value Item) {
        node := &Node{value, nil, nil}
        if bt.Root == nil {
                bt.Root = node
        } else {
                insertNode(bt.Root, node)
        }
}
 
func insertNode(node, newNode *Node) {
        if newNode.Value < node.Value {
                if node.Left == nil {
                        node.Left = newNode
                } else {
                        insertNode(node.Left, newNode)
                }
        } else {
                if node.Right == nil {
                        node.Right = newNode
                } else {
                        insertNode(node.Right, newNode)
                }
        }
}
 
func printTree(node *Node, level int) {
        if node != nil {
                format := ""
                for i := 0; i < level; i++ {
                        format += "       "
                }
                format += "---[ "
                level++
                printTree(node.Left, level)
                fmt.Printf(format+"%d\n", node.Value)
                printTree(node.Right, level)
        }
}
 
func encodeBinaryTreeIntoBSON(bt BinaryTree) ([]byte, error) {
        bsonOutput, err := bson.Marshal(bt)
 
        if err != nil {
                return bsonOutput, err
        }
        return bsonOutput, nil
}
 
func encodeBinaryTreeIntoJSON(bt BinaryTree) ([]byte, error) {
        jsonOutput, err := json.Marshal(bt)
 
        if err != nil {
                return jsonOutput, err
        }
        return jsonOutput, nil
}
 
func encodeBinaryTreeIntoIndentedJSON(bt BinaryTree) ([]byte, error) {
        jsonOutput, err := json.MarshalIndent(bt, "", "    ")
 
        if err != nil {
                return jsonOutput, err
        }
        return jsonOutput, nil
}
 
func encodeBinaryTreeIntoXML(bt BinaryTree) ([]byte, error) {
        xmlOutput, err := xml.Marshal(bt)
 
        if err != nil {
                return xmlOutput, err
        }
        return xmlOutput, nil
}
 
func encodeBinaryTreeIntoIndentedXML(bt BinaryTree) ([]byte, error) {
        xmlOutput, err := xml.MarshalIndent(bt, "", "    ")
 
        if err != nil {
                return xmlOutput, err
        }
        return xmlOutput, nil
}
 
func encodeBinaryTreeIntoGob(bt BinaryTree) ([]byte, error) {
        var buffer bytes.Buffer
        encoder := gob.NewEncoder(&buffer)
 
        err := encoder.Encode(bt)
        if err != nil {
                return buffer.Bytes(), err
        }
        return buffer.Bytes(), nil
}
 
func encodeBinaryTreeIntoMsgPack(bt BinaryTree) ([]byte, error) {
        var buffer bytes.Buffer
 
        // handler
        var handler codec.MsgpackHandle
 
        // objekt realizující zakódování dat
        encoder := codec.NewEncoder(&buffer, &handler)
 
        // zakódování dat
        err := encoder.Encode(bt)
        if err != nil {
                return buffer.Bytes(), err
        }
        return buffer.Bytes(), nil
}
 
func saveBinaryTree(encodedTree []byte, filename string) {
        err := ioutil.WriteFile(filename, encodedTree, 0644)
        if err != nil {
                fmt.Println(err)
        } else {
                fmt.Println("Stored into file", filename)
        }
}
 
func constructTree(bt *BinaryTree, min, max int) {
        middle := (min + max) / 2
        if min < middle && middle < max {
                fmt.Println(middle)
                bt.Insert(Item(middle))
                constructTree(bt, min, middle)
                constructTree(bt, middle, max)
        }
}
 
func printBufferInfo(buffer []byte) {
        fmt.Println("\nBuffer with encoded tree: ", len(buffer))
}
 
func main() {
        var bt BinaryTree
        constructTree(&bt, 0, 256)
 
        printTree(bt.Root, 0)
 
        encodedTree, err := encodeBinaryTreeIntoXML(bt)
        if err != nil {
                fmt.Println(err)
                return
        }
        printBufferInfo(encodedTree)
        saveBinaryTree(encodedTree, "/tmp/tree1.xml")
 
        encodedTree, err = encodeBinaryTreeIntoIndentedXML(bt)
        if err != nil {
                fmt.Println(err)
                return
        }
        printBufferInfo(encodedTree)
        saveBinaryTree(encodedTree, "/tmp/tree2.xml")
 
        encodedTree, err = encodeBinaryTreeIntoJSON(bt)
        if err != nil {
                fmt.Println(err)
                return
        }
        printBufferInfo(encodedTree)
        saveBinaryTree(encodedTree, "/tmp/tree1.json")
 
        encodedTree, err = encodeBinaryTreeIntoIndentedJSON(bt)
        if err != nil {
                fmt.Println(err)
                return
        }
        printBufferInfo(encodedTree)
        saveBinaryTree(encodedTree, "/tmp/tree2.json")
 
        encodedTree, err = encodeBinaryTreeIntoBSON(bt)
        if err != nil {
                fmt.Println(err)
                return
        }
        printBufferInfo(encodedTree)
        saveBinaryTree(encodedTree, "/tmp/tree1.bson")
 
        encodedTree, err = encodeBinaryTreeIntoGob(bt)
        if err != nil {
                fmt.Println(err)
                return
        }
        printBufferInfo(encodedTree)
        saveBinaryTree(encodedTree, "/tmp/tree1.gob")
 
        encodedTree, err = encodeBinaryTreeIntoMsgPack(bt)
        if err != nil {
                fmt.Println(err)
                return
        }
        printBufferInfo(encodedTree)
        saveBinaryTree(encodedTree, "/tmp/tree1.bin")

}

Průběh činnosti tohoto prográmku:

Buffer with encoded tree:  8076
Stored into file /tmp/tree1.xml
 
Buffer with encoded tree:  31378
Stored into file /tmp/tree2.xml
 
Buffer with encoded tree:  8575
Stored into file /tmp/tree1.json
 
Buffer with encoded tree:  42115
Stored into file /tmp/tree2.json
 
Buffer with encoded tree:  7406
Stored into file /tmp/tree1.bson
 
Buffer with encoded tree:  1431
Stored into file /tmp/tree1.gob
 
Buffer with encoded tree:  5363
Stored into file /tmp/tree1.bin

12. Velikosti souborů se serializovaným binárním stromem

Opět si jednotlivé soubory porovnejme podle velikosti:

Soubor Velikost Pořadí Poznámka
tree1.bin 5363 2 rekurzivní struktura namísto stromu
tree1.bson 7406 3 rekurzivní struktura namísto stromu
tree1.gob 1431 1 skutečný strom s ukazateli na další uzly
tree1.json 8575 4 minifikovaný
tree2.json 42115 6 čitelný s odsazením
tree1.xml 8076 5 minifikovaný
tree2.xml 31378 7 čitelný s odsazením
Poznámka: na tomto místě je vhodné poznamenat, že jedině formát Gob podporuje ukazatele, resp. přesněji řečeno reference mezi uzly a jedná se tedy o jediný formát, který strom uloží v původní podobě. Tomu také odpovídá naprosto nejmenší velikost výsledného binárního souboru.

Použitím GZIPu se rozdíly do značné míry smažou, což jen vypovídá o tom, kolik duplicitních dat se v původních souborech nacházelo:

Soubor Velikost Pořadí
tree1.bin.gz 745 2
tree1.bson.gz 924 5
tree1.gob.gz 714 1
tree1.json.gz 840 3
tree2.json.gz 1474 7
tree1.xml.gz 869 4
tree2.xml.gz 1306 6
Poznámka: paradoxně v tomto srovnání prohrává BSON, i když se jedná o binární formát. Nezdá se však, že by byl vhodný pro ukládání grafových struktur.

13. Serializace mapy, jejímiž klíči i hodnotami jsou řetězce

Poslední porovnání velikosti souborů po serializaci dat provedeme s mapou, jejímiž klíči i hodnotami jsou řetězce. S takovou mapou se lze v praxi setkat velmi často – příkladem mohou být některé konfigurační soubory atd. Mapa, kterou budeme ukládat, bude mít 260 dvojic klíč-hodnota.

Poznámka: důležité upozornění – mapy není možné přímo ukládat do formátu XML, takže tento formát z dalšího porovnání odstraníme. Důvodem je to, že mapa netvoří rekurzivní datovou strukturu, přesněji u ní chybí kořenový uzel.

Program určený pro serializaci mapy do různých formátů bude vypadat následovně:

package main
 
import (
        "bytes"
        "encoding/gob"
        "encoding/json"
        "fmt"
        "gopkg.in/mgo.v2/bson"
        "io/ioutil"
 
        "github.com/ugorji/go/codec"
)
 
type Map map[string]string
 
func encodeMapIntoBSON(m Map) ([]byte, error) {
        bsonOutput, err := bson.Marshal(m)
 
        if err != nil {
                return bsonOutput, err
        }
        return bsonOutput, nil
}
 
func encodeMapIntoJSON(m Map) ([]byte, error) {
        jsonOutput, err := json.Marshal(m)
 
        if err != nil {
                return jsonOutput, err
        }
        return jsonOutput, nil
}
 
func encodeMapIntoIndentedJSON(m Map) ([]byte, error) {
        jsonOutput, err := json.MarshalIndent(m, "", "    ")
 
        if err != nil {
                return jsonOutput, err
        }
        return jsonOutput, nil
}
 
func encodeMapIntoGob(m Map) ([]byte, error) {
        var buffer bytes.Buffer
        encoder := gob.NewEncoder(&buffer)
 
        err := encoder.Encode(m)
        if err != nil {
                return buffer.Bytes(), err
        }
        return buffer.Bytes(), nil
}
 
func encodeMapIntoMsgPack(m Map) ([]byte, error) {
        var buffer bytes.Buffer
 
        // handler
        var handler codec.MsgpackHandle
 
        // objekt realizující zakódování dat
        encoder := codec.NewEncoder(&buffer, &handler)
 
        // zakódování dat
        err := encoder.Encode(m)
        if err != nil {
                return buffer.Bytes(), err
        }
        return buffer.Bytes(), nil
}
 
func saveMap(encodedMap []byte, filename string) {
        err := ioutil.WriteFile(filename, encodedMap, 0644)
        if err != nil {
                fmt.Println(err)
        } else {
                fmt.Println("Stored into file", filename)
        }
}
 
func printBufferInfo(buffer []byte) {
        fmt.Println("\nBuffer with encoded map: ", len(buffer))
}
 
func main() {
        var m Map = make(map[string]string)
        m["foo"] = "text"
        m["bar"] = "test"
        m["baz"] = "Příliš žluťoučký kůň"
        m["longer key"] = "Příliš žluťoučký kůň"
 
        for i := 0; i < 256; i++ {
                key := fmt.Sprintf("key: %02x", i)
                value := fmt.Sprintf("value: %d", i)
                m[key] = value
        }
 
        encodedMap, err := encodeMapIntoJSON(m)
        if err != nil {
                fmt.Println(err)
                return
        }
        printBufferInfo(encodedMap)
        saveMap(encodedMap, "/tmp/map1.json")
 
        encodedMap, err = encodeMapIntoIndentedJSON(m)
        if err != nil {
                fmt.Println(err)
                return
        }
        printBufferInfo(encodedMap)
        saveMap(encodedMap, "/tmp/map2.json")
 
        encodedMap, err = encodeMapIntoBSON(m)
        if err != nil {
                fmt.Println(err)
                return
        }
        printBufferInfo(encodedMap)
        saveMap(encodedMap, "/tmp/map1.bson")
 
        encodedMap, err = encodeMapIntoGob(m)
        if err != nil {
                fmt.Println(err)
                return
        }
        printBufferInfo(encodedMap)
        saveMap(encodedMap, "/tmp/map1.gob")
 
        encodedMap, err = encodeMapIntoMsgPack(m)
        zif err != nil {
                fmt.Println(err)
                return
        }
        printBufferInfo(encodedMap)
        saveMap(encodedMap, "/tmp/map1.bin")
}

14. Velikosti souborů se serializovanou mapou

Opět si porovnejme velikosti výsledných souborů:

Soubor Velikost Pořadí Poznámka
map1.bin 4850 1  
map1.bson 6152 4  
map1.gob 4876 2  
map1.json 5888 3 minifikovaný
map2.json 7449 5 čitelný s odsazením
Poznámka: poprvé zde můžeme vidět významné změny v pořadí. Zejména konečně vyhrál formát Message Pack (i když jen o 26 bajtů), ale navíc se ukazuje, že textový JSON může být úspornější než binární formát BSON. Pokud je totiž JSON minifikovaný, bude každý řetězec uložen tak, že se kromě binární podoby řetězce (UTF-8) navíc uloží pouze další dva bajty se znakem uvozovek. Samotné „zakódování“ mapy do JSONu si vyžádá další čtyři bajty pro znaky „{“, „:“, „}“ a „,“, což opět není mnoho.

15. Rychlost serializace

Nyní již máme představu o možnostech formátu Message Pack z hlediska velikosti výsledných souborů (nebo posílaných datových bloků). Formát Message Pack v tomto ohledu obstál; pouze formát Gob byl v některých ohledech lepší. Zbývá nám však zjistit rychlost serializace, což je opět velmi důležitý údaj, zejména v oblasti mikroslužeb, IoT atd. Nyní se však již pohybujeme čistě na půdě programovacího jazyka Go a serializačních knihoven určených pro tento jazyk – v případě použití jiného jazyka a/nebo knihovny totiž můžeme dostat odlišné rychlosti (ovšem velikosti souborů zůstanou stále stejné).

Rychlost serializace si ověříme na kódu odvozeného od předchozího demonstračního příkladu. Bude se jednat o sadu funkcí pro serializaci mapy do různých formátů, ovšem s tím, že serializace bude provedena pouze do paměti – nebudeme tedy zahrnovat rychlost I/O operací:

type Map map[string]string
 
func encodeMapIntoBSON(m Map) ([]byte, error) {
        bsonOutput, err := bson.Marshal(m)
 
        if err != nil {
                return bsonOutput, err
        }
        return bsonOutput, nil
}
 
func encodeMapIntoJSON(m Map) ([]byte, error) {
        jsonOutput, err := json.Marshal(m)
 
        if err != nil {
                return jsonOutput, err
        }
        return jsonOutput, nil
}
 
func encodeMapIntoIndentedJSON(m Map) ([]byte, error) {
        jsonOutput, err := json.MarshalIndent(m, "", "    ")
 
        if err != nil {
                return jsonOutput, err
        }
        return jsonOutput, nil
}
 
func encodeMapIntoGob(m Map) ([]byte, error) {
        var buffer bytes.Buffer
        encoder := gob.NewEncoder(&buffer)
 
        err := encoder.Encode(m)
        if err != nil {
                return buffer.Bytes(), err
        }
        return buffer.Bytes(), nil
}
 
func encodeMapIntoMsgPack(m Map) ([]byte, error) {
        var buffer bytes.Buffer
 
        // handler
        var handler codec.MsgpackHandle
 
        // objekt realizující zakódování dat
        encoder := codec.NewEncoder(&buffer, &handler)
 
        // zakódování dat
        err := encoder.Encode(m)
        if err != nil {
                return buffer.Bytes(), err
        }
        return buffer.Bytes(), nil
}

16. Benchmark měřící rychlost serializace

Samotný benchmark využívá základní sadu nástrojů programovacího jazyka Go. Modul určený pro spouštění jednotkových testů totiž dokáže spouštět i benchmarky a následně vyhodnocovat rychlost jednotlivých funkcí pro různé velikosti vstupní veličiny. Tato veličina je automaticky měněna takovým způsobem, aby benchmarky byly dokončeny po určité době, a to na každém hardware (tedy na rychlém počítači bude provedeno větší množství iterací a naopak). Následně jsou výsledky benchmarků v čitelné podobě vypsány.

Nejprve si ukažme pomocnou funkci pro konstrukci mapy zadané velikosti, která je následně serializována:

package main
 
import (
        "fmt"
 
        "testing"
)
 
func createMap(n int) Map {
        var m Map = make(map[string]string)
        m["foo"] = "text"
        m["bar"] = "test"
        m["baz"] = "Příliš žluťoučký kůň"
        m["longer key"] = "Příliš žluťoučký kůň"
 
        for i := 0; i < n; i++ {
                key := fmt.Sprintf("key: %02x", i)
                value := fmt.Sprintf("value: %d", i)
                m[key] = value
        }
        return m
}

Následuje vlastní realizace benchmarku, který opakovaně volá funkci f, která má provádět serializaci vstupní mapy. Za f se dosadí libovolná funkce z předchozí kapitoly:

func benchmark(b *testing.B, n int, f func(m Map) ([]byte, error)) {
        m := createMap(n)
        for i := 0; i < b.N; i++ {
                f(m)
        }
}

Testovat postupně budeme serializaci mapy s 4+1 prvkem, 4+100 prvky a 4+1000 prvky. A měřit pochopitelně budeme serializaci do formátů BSON, minifikovaný JSON, čitelný JSON a Message Pack:

func BenchmarkBSON1(b *testing.B) {
        benchmark(b, 1, encodeMapIntoBSON)
}
 
func BenchmarkBSON100(b *testing.B) {
        benchmark(b, 100, encodeMapIntoBSON)
}
 
func BenchmarkBSON1000(b *testing.B) {
        benchmark(b, 1000, encodeMapIntoBSON)
}
 
func BenchmarkJSON1(b *testing.B) {
        benchmark(b, 1, encodeMapIntoJSON)
}
 
func BenchmarkJSON100(b *testing.B) {
        benchmark(b, 100, encodeMapIntoJSON)
}
 
func BenchmarkJSON1000(b *testing.B) {
        benchmark(b, 1000, encodeMapIntoJSON)
}
 
func BenchmarkIndentedJSON1(b *testing.B) {
        benchmark(b, 1, encodeMapIntoIndentedJSON)
}
 
func BenchmarkIndentedJSON100(b *testing.B) {
        benchmark(b, 100, encodeMapIntoIndentedJSON)
}
 
func BenchmarkIndentedJSON1000(b *testing.B) {
        benchmark(b, 1000, encodeMapIntoIndentedJSON)
}
 
func BenchmarkGob1(b *testing.B) {
        benchmark(b, 1, encodeMapIntoGob)
}
 
func BenchmarkGob100(b *testing.B) {
        benchmark(b, 100, encodeMapIntoGob)
}
 
func BenchmarkGob1000(b *testing.B) {
        benchmark(b, 1000, encodeMapIntoGob)
}
 
func BenchmarkMsgPack1(b *testing.B) {
        benchmark(b, 1, encodeMapIntoMsgPack)
}
 
func BenchmarkMsgPack100(b *testing.B) {
        benchmark(b, 100, encodeMapIntoMsgPack)
}
 
func BenchmarkMsgPack1000(b *testing.B) {
        benchmark(b, 1000, encodeMapIntoMsgPack)
}

17. Výsledky benchmarku

Samotný benchmark se spustí tímto příkazem:

$ go test -bench=.

Získané výsledky mohou vypadat následovně:

goos: linux
goarch: amd64
pkg: msgpack-test
cpu: Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz
BenchmarkBSON1-8                  933855              1121 ns/op
BenchmarkBSON100-8                 62947             21862 ns/op
BenchmarkBSON1000-8                 5594            191998 ns/op
BenchmarkJSON1-8                  848180              1400 ns/op
BenchmarkJSON100-8                 44877             27312 ns/op
BenchmarkJSON1000-8                 3687            343840 ns/op
BenchmarkIndentedJSON1-8          454699              2566 ns/op
BenchmarkIndentedJSON100-8         27103             45834 ns/op
BenchmarkIndentedJSON1000-8         2395            504380 ns/op
BenchmarkGob1-8                   568262              2425 ns/op
BenchmarkGob100-8                  68746             23603 ns/op
BenchmarkGob1000-8                  7363            159924 ns/op
BenchmarkMsgPack1-8               847807              1351 ns/op
BenchmarkMsgPack100-8             231123              5438 ns/op
BenchmarkMsgPack1000-8             26774             46252 ns/op
PASS
ok      msgpack-test    21.273s

Lepší bude výsledky roztřídit podle velikosti mapy:

skolení ELK

BenchmarkBSON1-8                  933855              1121 ns/op
BenchmarkJSON1-8                  848180              1400 ns/op
BenchmarkIndentedJSON1-8          454699              2566 ns/op
BenchmarkGob1-8                   568262              2425 ns/op
BenchmarkMsgPack1-8               847807              1351 ns/op
 
BenchmarkBSON100-8                 62947             21862 ns/op
BenchmarkJSON100-8                 44877             27312 ns/op
BenchmarkIndentedJSON100-8         27103             45834 ns/op
BenchmarkGob100-8                  68746             23603 ns/op
BenchmarkMsgPack100-8             231123              5438 ns/op
 
BenchmarkBSON1000-8                 5594            191998 ns/op
BenchmarkJSON1000-8                 3687            343840 ns/op
BenchmarkIndentedJSON1000-8         2395            504380 ns/op
BenchmarkGob1000-8                  7363            159924 ns/op
BenchmarkMsgPack1000-8             26774             46252 ns/op
Poznámka: povšimněte si, že formát Message Pack, resp. přesněji řečeno použitá serializační knihovna Message Packu, je absolutně nejrychlejší už pro relativně malé mapy se 104 prvky (a ještě významněji pro větší mapy). To je poněkud překvapující zjištění, protože osobně bych čekal, že zvítězí formát Gob.

18. Kam dál?

Formát Message Pack je vhodný použít v situacích, kdy je nutné rychle přenášet velké množství dat, která navíc musí být „samopopisná“, což znamená, že u každé datové položky je určen (uložen) i její datový typ. V případě, že „samopopisnost“ není vyžadována, tj. ve chvíli, kdy obě komunikující strany očekávají shodný formát dat, nebo si formát předají jiným kanálem, může být výhodnější použít Protocol Buffers neboli Protobuf. To je však již téma na samostatný článek.

19. Repositář s demonstračními příklady

Zdrojové kódy všech minule i dnes použitých demonstračních příkladů byly uloženy do nového Git repositáře, který je dostupný na adrese https://github.com/tisnik/go-root (stále na GitHubu :-). V případě, že nebudete chtít klonovat celý repositář (ten je ovšem – alespoň prozatím – velmi malý, dnes má přibližně stovku kilobajtů), můžete namísto toho použít odkazy na jednotlivé demonstrační příklady, které naleznete v následující tabulce:

# Příklad/soubor Stručný popis Cesta
1 msgpack_nil.go serializace hodnoty nil https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_nil.go
2 msgpack_true.go serializace hodnot true https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_true.go
3 msgpack_false.go serializace hodnot false https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_false.go
4 msgpack_small_int.go serializace celočíselné hodnoty menší než 127 https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_small_int.go
5 msgpack_longer_int.go serializace celočíselné hodnoty menší než 216 https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_longer_int.go
6 msgpack_even_longer_int.go serializace celočíselné hodnoty větší než 216 https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_even_longer_int.go
7 msgpack_long_int.go serializace celočíselné hodnoty větší než 232 https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_long_int.go
8 msgpack_single.go serializace hodnoty s plovoucí řádovou čárkou (single) https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_single.go
9 msgpack_double.go serializace hodnoty s plovoucí řádovou čárkou (double) https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_double.go
       
10 msgpack_short_string.go serializace krátkého řetězce (méně než 31 znaků) https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_short_string.go
11 msgpack_longer_string.go serializace delšího řetězce (méně než 256 znaků) https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_longer_string.go
12 msgpack_even_longer_string.go serializace dlouhého řetězce https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_even_longer_string.go
       
13 msgpack_short_array1.go krátké pole čtyř hodnot 1–4 https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_short_array1.go
14 msgpack_short_array2.go krátké pole čtyř hodnot 100, 200, 300 a 400 https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_short_array2.go
15 msgpack_bytes.go pole 1000 bajtů https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_bytes.go
16 msgpack_array_16A.go pole 1000 prvků typu int https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_array_16A.go
17 msgpack_array_16B.go pole 1000 prvků typu uint https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_array_16B.go
       
18 msgpack_map.go serializace mapy https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_map.go
       
19 msgpack_array_16C.go pole vzniklé serializací řezu (slice) https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_array_16C.go
20 msgpack_array_16D.go serializace heterogenního pole https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_array_16D.go
21 msgpack_map2.go serializace heterogenní mapy https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_map2.go
22 msgpack_timestamp.go serializace časového razítka https://github.com/tisnik/go-root/blob/master/msgpack/msgpac­k_timestamp.go
       
22 size_comparison_A.go porovnání velikosti vektorů typu float64 po jejich serializaci do různých formátů https://github.com/tisnik/go-root/blob/master/msgpack/si­ze_comparison_A.go
23 size_comparison_B.go porovnání velikosti binárních stromů po jejich serializaci do různých formátů https://github.com/tisnik/go-root/blob/master/msgpack/si­ze_comparison_B.go
24 size_comparison_C.go porovnání velikosti map po jejich serializaci do různých formátů https://github.com/tisnik/go-root/blob/master/msgpack/si­ze_comparison_C.go
       
25 benchmark/ měření rychlosti serializace map do různých formátů https://github.com/tisnik/go-root/blob/master/msgpack/benchmark

20. Odkazy na Internetu

  1. Základní informace o MessagePacku
    https://msgpack.org/
  2. MessagePack na Wikipedii
    https://en.wikipedia.org/wi­ki/MessagePack
  3. Comparison of data-serialization formats (Wikipedia)
    https://en.wikipedia.org/wi­ki/Comparison_of_data-serialization_formats
  4. Repositáře msgpacku
    https://github.com/msgpack
  5. Specifikace ukládání různých typů dat
    https://github.com/msgpac­k/msgpack/blob/master/spec­.md
  6. Podpora MessagePacku v různých jazycích
    https://msgpack.org/#languages
  7. Základní implementace formátu msgpack pro Go
    https://github.com/msgpack/msgpack-go
  8. go-codec
    https://github.com/ugorji/go
  9. Gobs of data
    https://blog.golang.org/gobs-of-data
  10. Formát BSON
    http://bsonspec.org/
  11. Problematika nulových hodnot v Go, aneb proč nil != nil
    https://www.root.cz/clanky/pro­blematika-nulovych-hodnot-v-go-aneb-proc-nil-nil/
  12. IEEE-754 Floating Point Converter
    https://www.h-schmidt.net/FloatConverter/I­EEE754.html
  13. Base Convert: IEEE 754 Floating Point
    https://baseconvert.com/ieee-754-floating-point
  14. Brain Floating Point – nový formát uložení čísel pro strojové učení a chytrá čidla
    https://www.root.cz/clanky/brain-floating-point-ndash-novy-format-ulozeni-cisel-pro-strojove-uceni-a-chytra-cidla/

Autor článku

Pavel Tišnovský vystudoval VUT FIT a v současné době pracuje ve společnosti Red Hat, kde vyvíjí nástroje pro OpenShift.io.