高效将struct序列化为磁盘

时间:2016-06-03 15:35:54

标签: serialization go struct gob

我的任务是将C ++代码替换为Go,而我是Go API的新手。我使用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。

如何在没有手动编码的情况下将其序列化为后者?

3 个答案:

答案 0 :(得分:20)

如果您压缩包含文本a.txt(5个字符)的名为"hello"的文件,则结果zip将大约为115个字节。这是否意味着zip格式压缩文本文件效率不高?当然不是。有一个开销。如果文件包含"hello"一百次(500字节),压缩它将导致文件 120字节1x"hello" => 115字节,100x"hello" => 120个字节!我们添加了495个字节,但压缩后的大小只增加了5个字节。

encoding/gob包发生了类似的事情:

  

该实现为流中的每种数据类型编译自定义编解码器,并且在使用单个编码器传输值流时,最有效,分摊编译成本。

当你"第一"序列化一个类型的值,该类型的定义也必须被包含/传输,因此解码器可以正确地解释和解码流:

  

gobs流是自我描述的。流中的每个数据项前面都有一个类型的规范,用一小组预定义类型表示。

让我们回到你的榜样:

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 s的长度也必须包括在内,使用{{ 1}}字节(32位体系结构上4的大小)为您提供12个字节,即"最小值"。 (是的,您可以使用较小的类型作为长度,但这有其局限性。对于小数字,可变长度编码将是更好的选择,请参阅encoding/binary包。)

总而言之,int可以很好地满足您的需求。不要被最初的印象所迷惑。

如果一个encoding/gob的这12个字节太多"很多"对于您来说,您始终可以将流包装到compress/flatecompress/gzip编写器中以进一步缩小大小(以换取较慢的编码/解码以及对该过程稍高的内存要求)。

<强>演示:

让我们测试3个解决方案:

  • 使用&#34;裸体&#34;输出(无压缩)
  • 使用Entry压缩compress/flate
  • 的输出
  • 使用encoding/gob压缩compress/gzip
  • 的输出

我们将编写一千个条目,更改每个条目的键和值,为encoding/gob"k000""v000""k001"等。这意味着未压缩的大小"v001"是4字节+4字节+4字节+ 4字节= 16字节(2x 4字节文本,2x4字节长度)。

代码如下所示:

Entry

输出:

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

Go Playground上尝试。

正如你所看到的那样:&#34;裸体&#34;输出为[Naked] Length: 16036, average: 16.04 / Entry [flate] Length: 4123, average: 4.12 / Entry [ gzip] Length: 4141, average: 4.14 / Entry ,仅略高于计算的大小(由于上面讨论过的一次性微小开销)。

使用flate或gzip压缩输出时,可以将输出大小减小到约16.04 bytes/Entry,约为理论大小的约26%,我确信满足您的要求。 (请注意,使用&#34;现实生活&#34;数据压缩率可能要高很多,因为我在测试中使用的键和值非常相似,因此非常可压缩;仍然比率应该在50%左右现实生活中的数据)。

答案 1 :(得分:9)

使用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;
}

通过运行:

从proto文件生成go代码
$ protoc --go_out=. *.proto

如果愿意,您可以检查生成的文件。

您可以运行并查看结果输出:

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

答案 2 :(得分:3)

&#34;手动编码&#34;,您非常害怕,使用标准encoding/binary package在Go中轻松完成。

您似乎将字符串长度值以big-endian格式存储为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,您可以将其传递给打开文件,网络套接字以及实现该接口的任何其他内容。