高效将struct序列化为磁盘

bit*_*ner 7 serialization struct go 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())
}
Run Code Online (Sandbox Code Playgroud)

这会产生很多我不需要的臃肿:

[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] 
Run Code Online (Sandbox Code Playgroud)

我想序列化每个字符串的len,然后是原始字节,如:

[0 0 0 2 107 49 0 0 0 2 118 49]
Run Code Online (Sandbox Code Playgroud)

我正在保存数百万个条目,因此编码中的额外膨胀会使文件大小增加大约x10.

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

icz*_*cza 21

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

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

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

当您"首先"序列化类型的值时,还必须包含/传输该类型的定义,因此解码器可以正确地解释和解码流:

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

让我们回到你的例子:

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

它打印:

48
Run Code Online (Sandbox Code Playgroud)

现在让我们编码一些相同的类型:

enc.Encode(e)
fmt.Println(buf.Len())
enc.Encode(e)
fmt.Println(buf.Len())
Run Code Online (Sandbox Code Playgroud)

现在的输出是:

60
72
Run Code Online (Sandbox Code Playgroud)

Go Playground尝试一下.

分析结果:

相同Entry类型的附加值仅花费12个字节,而第一个是48字节,因为还包括类型定义(约26个字节),但这是一次性开销.

所以基本上你发送2个stringS:"k1""v1"其是4个字节,长度stringS还必须包括,使用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个字节(2×4字节的文本,2×4字节的长度).

代码如下所示:

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)
}
Run Code Online (Sandbox Code Playgroud)

输出:

[Naked] Length: 16036, average: 16.04 / Entry
[flate] Length:  4123, average:  4.12 / Entry
[ gzip] Length:  4141, average:  4.14 / Entry
Run Code Online (Sandbox Code Playgroud)

Go Playground尝试一下.

正如您所看到的那样:"裸"输出16.04 bytes/Entry仅略微超过计算的大小(由于上面讨论的一次性微小开销).

当你使用flate或gzip来压缩输出时,你可以将输出大小减小到大约4.13 bytes/Entry,理论大小约为26%,我相信你满意.(请注意,对于"实际"数据,压缩比可能会高很多,因为我在测试中使用的键和值非常相似,因此非常可压缩;实际数据仍然应该是50%左右).

  • 令人印象深刻的分析(我一直很欣赏你的回答)但在这种特殊情况下,这似乎是在向一个问为什么他的三轮自行车有点慢的孩子解释火箭科学。;-) 虽然我认为 `gob` 肯定有它的用途,对于 OP 似乎手头上的这么简单的任务,我确信直接重新实现在 C++ 中已经完成的工作是有必要的。这种方法的另一个好处是新代码将与他们拥有的遗留数据进行比较。 (4认同)

jrw*_*ren 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)
}
Run Code Online (Sandbox Code Playgroud)

你创建一个文件,example.proto像这样:

package main;

message Entry {
    required string Key = 1;
    required string Val = 2;
}
Run Code Online (Sandbox Code Playgroud)

您可以通过运行以下命令从proto文件生成go代码:

$ protoc --go_out=. *.proto
Run Code Online (Sandbox Code Playgroud)

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

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

$ go run *.go
[10 2 107 49 18 2 118 49]
Run Code Online (Sandbox Code Playgroud)