【问题标题】:Efficient Go serialization of struct to disk结构到磁盘的高效 Go 序列化
【发布时间】:2016-06-03 15:35:53
【问题描述】:

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

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

【问题讨论】:

    标签: serialization go struct gob


    【解决方案1】:

    如果您压缩一个名为 a.txt 的文件,其中包含文本 "hello"(即 5 个字符),则压缩结果约为 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个strings:"k1""v1",它们是4个字节,strings的长度也必须包括在内,使用4字节(int的大小在 32 位架构上)为您提供 12 个字节,这是“最小值”。 (是的,您可以使用较小的类型来表示长度,但这有其局限性。对于较小的数字,可变长度编码将是更好的选择,请参阅 encoding/binary 包。)

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

    如果一个 Entry 的这 12 个字节对您来说太“多”,您可以随时将流包装到 compress/flatecompress/gzip 写入器中以进一步减小大小(以换取较慢的编码/解码和进程的内存要求稍高)。

    演示:

    让我们测试以下 5 个解决方案:

    • 使用“裸”输出(无压缩)
    • 使用compress/flate压缩encoding/gob的输出
    • 使用compress/zlib压缩encoding/gob的输出
    • 使用compress/gzip压缩encoding/gob的输出
    • 使用github.com/dsnet/compress/bzip2压缩encoding/gob的输出

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

    代码如下所示:

    for _, name := range []string{"Naked", "flate", "zlib", "gzip", "bzip2"} {
        buf := &bytes.Buffer{}
    
        var out io.Writer
        switch name {
        case "Naked":
            out = buf
        case "flate":
            out, _ = flate.NewWriter(buf, flate.DefaultCompression)
        case "zlib":
            out, _ = zlib.NewWriterLevel(buf, zlib.DefaultCompression)
        case "gzip":
            out = gzip.NewWriter(buf)
        case "bzip2":
            out, _ = bzip2.NewWriter(buf, nil)
        }
    
        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:  4120, average:  4.12 / Entry
    [ zlib] Length:  4126, average:  4.13 / Entry
    [ gzip] Length:  4138, average:  4.14 / Entry
    [bzip2] Length:  2042, average:  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% 左右-寿命数据)。

    【讨论】:

    • 令人印象深刻的分析(我一直很欣赏您的回答),但在这种特殊情况下,它似乎向一个问为什么他的三轮自行车有点慢的孩子解释火箭科学。 ;-) 虽然我认为gob 肯定有它的用途,但对于手头上的 OP 似乎有这样简单的任务,我敢肯定,有必要直接重新实现 C++ 中已经完成的工作。这种方法的另一个好处是新代码将与他们拥有的旧数据相媲美。
    • @kostix 这也是我对这个问题的第一个想法和印象,但后来我看到了它的最后一行:“没有手动编码”...所以这就是我决定的原因留在encoding/gob
    【解决方案2】:

    使用 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]
    

    【讨论】:

      【解决方案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,您可以将打开的文件、网络套接字和其他任何实现传递给它那个界面。

      【讨论】:

      • 在您发表评论后,我什至想建议您继续发布“手动”版本。 +1。
      猜你喜欢
      • 2016-01-03
      • 1970-01-01
      • 2014-01-21
      • 1970-01-01
      • 1970-01-01
      • 2011-04-12
      • 2018-10-03
      • 1970-01-01
      • 2011-01-09
      相关资源
      最近更新 更多