猿问

结构到磁盘的高效 Go 序列化

我的任务是将 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。


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


慕码人2483693
浏览 187回答 3
3回答

郎朗坤

如果您压缩一个名为a.txt包含文本"hello"(5 个字符)的文件,则结果 zip 大约为 115 个字节。这是否意味着 zip 格式无法有效压缩文本文件?当然不是。有一个开销。如果文件包含"hello"一百次(500 字节),压缩它会导致文件为120 字节!1x"hello"=> 115 字节,100x"hello"=> 120 字节!我们添加了 495 个字节,但压缩后的大小只增加了 5 个字节。encoding/gob包裹也发生了类似的事情:该实现为流中的每种数据类型编译自定义编解码器,并且在使用单个编码器传输值流时最有效,从而分摊编译成本。当您“首先”序列化类型的值时,还必须包含/传输类型的定义,因此解码器可以正确解释和解码流:一连串的gobs是自我描述的。流中的每个数据项之前都有其类型的规范,用一小组预定义类型表示。让我们回到你的例子:var buf bytes.Bufferenc := 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())现在输出是:6072在Go Playground上尝试一下。分析结果:相同Entry类型的附加值仅花费12 个字节,而第一个是48字节,因为还包括类型定义(大约 26 个字节),但这是一次性开销。所以基本上你传输 2 strings:"k1"并且"v1"是 4 个字节,并且strings 的长度也必须包括在内,使用4字节(int在 32 位架构上的大小)给你 12 个字节,这是“最小值”。(是的,您可以使用较小的类型来表示长度,但这有其局限性。对于小数字,可变长度编码将是更好的选择,请参阅encoding/binary包。)总而言之,encoding/gob可以很好地满足您的需求。不要被最初的印象所迷惑。如果这 12 个字节对Entry您来说“太多”,您始终可以将流包装到 acompress/flate或compress/gzipwriter 中以进一步减小大小(以换取较慢的编码/解码和进程的稍高内存要求)。示范:让我们测试以下 5 个解决方案:使用“裸”输出(无压缩)用于compress/flate压缩输出encoding/gob用于compress/zlib压缩输出encoding/gob用于compress/gzip压缩输出encoding/gob用于github.com/dsnet/compress/bzip2压缩输出encoding/gob我们将编写一千个条目,更改每个条目的键和值,如"k000"、"v000"、"k001"等"v001"。这意味着 an 的未压缩大小Entry为 4 字节 + 4 字节 + 4 字节 + 4 字节 = 16 字节(2x4 字节文本,2x4 字节长度)。代码如下所示:for _, name := range []string{"Naked", "flate", "zlib", "gzip", "bzip2"} {&nbsp; &nbsp; buf := &bytes.Buffer{}&nbsp; &nbsp; var out io.Writer&nbsp; &nbsp; switch name {&nbsp; &nbsp; case "Naked":&nbsp; &nbsp; &nbsp; &nbsp; out = buf&nbsp; &nbsp; case "flate":&nbsp; &nbsp; &nbsp; &nbsp; out, _ = flate.NewWriter(buf, flate.DefaultCompression)&nbsp; &nbsp; case "zlib":&nbsp; &nbsp; &nbsp; &nbsp; out, _ = zlib.NewWriterLevel(buf, zlib.DefaultCompression)&nbsp; &nbsp; case "gzip":&nbsp; &nbsp; &nbsp; &nbsp; out = gzip.NewWriter(buf)&nbsp; &nbsp; case "bzip2":&nbsp; &nbsp; &nbsp; &nbsp; out, _ = bzip2.NewWriter(buf, nil)&nbsp; &nbsp; }&nbsp; &nbsp; enc := gob.NewEncoder(out)&nbsp; &nbsp; e := Entry{}&nbsp; &nbsp; for i := 0; i < 1000; i++ {&nbsp; &nbsp; &nbsp; &nbsp; e.Key = fmt.Sprintf("k%3d", i)&nbsp; &nbsp; &nbsp; &nbsp; e.Val = fmt.Sprintf("v%3d", i)&nbsp; &nbsp; &nbsp; &nbsp; enc.Encode(e)&nbsp; &nbsp; }&nbsp; &nbsp; if c, ok := out.(io.Closer); ok {&nbsp; &nbsp; &nbsp; &nbsp; c.Close()&nbsp; &nbsp; }&nbsp; &nbsp; fmt.Printf("[%5s] Length: %5d, average: %5.2f / Entry\n",&nbsp; &nbsp; &nbsp; &nbsp; name, buf.Len(), float64(buf.Len())/1000)}输出:[Naked] Length: 16036, average: 16.04 / Entry[flate] Length:&nbsp; 4120, average:&nbsp; 4.12 / Entry[ zlib] Length:&nbsp; 4126, average:&nbsp; 4.13 / Entry[ gzip] Length:&nbsp; 4138, average:&nbsp; 4.14 / Entry[bzip2] Length:&nbsp; 2042, average:&nbsp; 2.04 / Entry在Go Playground上尝试一下。正如您所看到的:“裸”输出16.04 bytes/Entry仅略高于计算的大小(由于上面讨论的一次性微小开销)。当您使用 flate、zlib 或 gzip 压缩输出时,您可以将输出大小减小到约4.13 bytes/Entry,这大约是理论大小的 ~26%,我相信这会让您满意。如果没有,您可以使用提供更高效率压缩的库,例如 bzip2,在上面的示例中2.04 bytes/Entry,它的结果是理论大小的12.7% !(请注意,对于“真实”数据,压缩率可能会高很多,因为我在测试中使用的键和值非常相似,因此可压缩性非常好;对于真实数据,压缩率仍然应该在 50% 左右)。

米脂

使用 protobuf 有效地编码您的数据。https://github.com/golang/protobuf你的主要看起来像这样:package mainimport (&nbsp; &nbsp; "fmt"&nbsp; &nbsp; "log"&nbsp; &nbsp; "github.com/golang/protobuf/proto")func main() {&nbsp; &nbsp; e := &Entry{&nbsp; &nbsp; &nbsp; &nbsp; Key: proto.String("k1"),&nbsp; &nbsp; &nbsp; &nbsp; Val: proto.String("v1"),&nbsp; &nbsp; }&nbsp; &nbsp; data, err := proto.Marshal(e)&nbsp; &nbsp; if err != nil {&nbsp; &nbsp; &nbsp; &nbsp; log.Fatal("marshaling error: ", err)&nbsp; &nbsp; }&nbsp; &nbsp; fmt.Println(data)}您创建一个文件,example.proto,如下所示:package main;message Entry {&nbsp; &nbsp; required string Key = 1;&nbsp; &nbsp; required string Val = 2;}您可以通过运行从 proto 文件生成 go 代码:$ protoc --go_out=. *.proto如果您愿意,可以检查生成的文件。您可以运行并查看结果输出:$ go run *.go[10 2 107 49 18 2 118 49]

陪伴而非守候

您非常害怕的“手动编码”在 Go 中使用标准encoding/binary包轻松完成。您似乎将字符串长度值存储为大端格式的 32 位整数,因此您可以继续在 Go 中执行此操作:package mainimport (&nbsp; &nbsp; "bytes"&nbsp; &nbsp; "encoding/binary"&nbsp; &nbsp; "fmt"&nbsp; &nbsp; "io")func encode(w io.Writer, s string) (n int, err error) {&nbsp; &nbsp; var hdr [4]byte&nbsp; &nbsp; binary.BigEndian.PutUint32(hdr[:], uint32(len(s)))&nbsp; &nbsp; n, err = w.Write(hdr[:])&nbsp; &nbsp; if err != nil {&nbsp; &nbsp; &nbsp; &nbsp; return&nbsp; &nbsp; }&nbsp; &nbsp; n2, err := io.WriteString(w, s)&nbsp; &nbsp; n += n2&nbsp; &nbsp; return}func main() {&nbsp; &nbsp; var buf bytes.Buffer&nbsp; &nbsp; for _, s := range []string{&nbsp; &nbsp; &nbsp; &nbsp; "ab",&nbsp; &nbsp; &nbsp; &nbsp; "cd",&nbsp; &nbsp; &nbsp; &nbsp; "de",&nbsp; &nbsp; } {&nbsp; &nbsp; &nbsp; &nbsp; _, err := encode(&buf, s)&nbsp; &nbsp; &nbsp; &nbsp; if err != nil {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; panic(err)&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }&nbsp; &nbsp; fmt.Printf("%v\n", buf.Bytes())}请注意,在此示例中,我正在写入字节缓冲区,但这仅用于演示目的 - 由于encode()写入io.Writer,您可以将打开的文件、网络套接字和其他任何实现该接口的文件传递给它。
随时随地看视频慕课网APP

相关分类

Go
我要回答