2016-06-03 2 views
6

Мне было поручено заменить код на C++ на Go, и я довольно новичок в API Go. Я использую gob для кодирования сотен записей ключа/значения на дисковые страницы, но в кодировке gob слишком много раздуваться, что не нужно.Эффективная сериализация структуры на диск

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()) 
} 

Это производит много наворотов, что мне не нужно:

[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] 

Я хочу сериализации Len каждой струны следуют необработанные байты как:

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

Я экономя миллионы записей, поэтому дополнительный вздутие в кодировке увеличивает размер файла примерно на x10.

Как я могу сериализовать его без последнего кодирования?

ответ

8

Используйте protobuf для эффективного кодирования данных.

https://github.com/golang/protobuf

Ваш главный будет выглядеть следующим образом:

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) 
} 

Вы создаете файл, example.proto так:

package main; 

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

Вы генерировать код идут из файла прото от пробега:

$ protoc --go_out=. *.proto 

Вы можете просмотреть сгенерированный файл, если хотите.

Вы можете запустить и увидеть вывод результатов:

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

Если сжать файл с именем a.txt, содержащий текст "hello" (который является 5 символов), то результат молнии будет около 115 байт. Означает ли это, что формат zip неэффективен для сжатия текстовых файлов? Конечно нет. Существует накладные расходы. Если файл содержит "hello" сто раз (500 байт), то при zipping это приведет к тому, что файл будет 120 байт! 1x"hello" => 115 байт, 100x"hello" => 120 байт! Мы добавили 495 байтов, но сжатый размер увеличился только на 5 байт.

Нечто подобное происходит и с encoding/gob пакетом:

Реализация компилирует пользовательский кодек для каждого типа данных в потоке и является наиболее эффективным, когда один кодер используется для передачи потока значений, амортизировать стоимость компиляции.

Когда вы «первый» сериализовать значение типа, то определение типа также должна быть включена/передается, так что декодер может правильно интерпретировать и декодирования потока:

Поток глотков самоописателен.Каждому элементу данных в потоке предшествует спецификация его типа, выраженная в терминах небольшого набора предопределенных типов.

Вернемся к вашему примеру:

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

Он печатает:

48 

Теперь давайте кодировать несколько из типа же:

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

сейчас выход:

60 
72 

Попробуйте на Go Playground.

Анализ результатов:

Дополнительные значения одного и того же Entry типа только стоимость 12 байт, в то время как первый является 48 байт, потому что определение типа также включен (который составляет ~ 26 байт), но это одноразовые накладные расходы.

Так в основном вы передаете 2 string S: "k1" и "v1", которые 4 байта, а длина string с также должен быть включен, используя 4 байт (размером int на 32-разрядных архитектур) дает вам 12 байт , что является «минимумом». (Да, вы могли бы использовать меньший тип для длины, но это имело бы свои ограничения. Кодировка переменной длины была бы лучшим выбором для небольших чисел, см. encoding/binary.)

В целом, encoding/gob делает довольно хорошая работа для ваших нужд. Не обманывайте себя начальными впечатлениями.

Если это 12 байт для одного Entry слишком «много» для вас, вы всегда можете обернуть поток в compress/flate или compress/gzip писателя для дальнейшего уменьшения размера (в обмен на более медленной кодирования/декодирования и немного выше требования к памяти для процесс).

Демонстрация:

Давайте тестировать 3 решения:

  • не используя "голый" выход (без сжатия)
  • Использование compress/flate для сжатия выходного сигнала encoding/gob
  • Использование compress/gzip для сжатия выходного сигнала encoding/gob

Мы будем писать тысячу записей, изменение ключей и значений каждого из них, будучи "k000", "v000", "k001", "v001" и т.д. Это означает, что размер несжатого из Entry составляет 4 байта + 4 байта + 4 байта + 4 байта = 16 байты (2x 4 байта, длина 2x4 байтов).

код выглядит следующим образом:

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) 
} 

Выход:

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

Попробуйте на Go Playground.

Как вы можете видеть: «голый» выход - 16.04 bytes/Entry, чуть меньше расчетного размера (из-за однократного крошечного накладного расхода, описанного выше).

Когда вы используете вентилятор или gzip для сжатия выходного сигнала, вы можете уменьшить выходной размер примерно до 4.13 bytes/Entry, что составляет около ~ 26% от теоретического размера, я уверен, что вас удовлетворяет. (Обратите внимание, что с «реальными» данными коэффициент сжатия, вероятно, будет намного выше, поскольку ключи и значения, которые я использовал в тесте, очень похожи и, следовательно, очень хорошо сжимаются, а соотношение должно составлять около 50% с реальными данными).

+1

Впечатляющий анализ (я всегда восхищаюсь вашими ответами), но в этом конкретном случае кажется, что объясняет науку о ракетах ребенку, который спросил, почему его трехколесный велосипед немного медленный. ;-) Хотя я думаю, что у 'gob' определенно есть свои возможности, для такой простой задачи, с которой, похоже, работает OP, я уверен, что для правильной переопределения того, что уже сделано на C++, требуется. Еще одним подходом к этому подходу является то, что новый код будет сопоставим с его устаревшими данными. – kostix

+0

@kostix Это была моя первая мысль и впечатление об этом вопросе, но потом я увидел ее последнюю строку: «без ручного кодирования» _... Поэтому я решил остаться с «encoding/gob». – icza

3

«Ручное кодирование», которое вы так боитесь, тривиально сделано в Go, используя стандарт encoding/binary package.

Вы появляетесь для хранения значений длины строки в 32-разрядных целых чисел в формате обратного порядка файлов, так что вы можете просто пойти и сделать это в 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.

Обратите внимание, что в этом примере я пишу в буфер байт, но это только для демонстрации — поскольку encode() пишет к io.Writer, вы можете передать его открытый файл, сетевой сокет и все остальное, реализующий этот интерфейс.

+0

После вашего комментария я даже хотел предложить пойти и опубликовать «ручную» версию. +1. – icza

Смежные вопросы