Serialization 结构到磁盘的高效Go序列化 我已经被替换来替换C++代码,而且我对GoAPI非常陌生。我使用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()) }
这会产生很多我不需要的膨胀:Serialization 结构到磁盘的高效Go序列化 我已经被替换来替换C++代码,而且我对GoAPI非常陌生。我使用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()) },serialization,go,struct,gob,Serialization,Go,Struct,Gob,这会产生很多我不需要的膨胀: [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 如何
[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
如何在不进行手动编码的情况下将其序列化为后者?使用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)
}
package main;
message Entry {
required string Key = 1;
required string Val = 2;
}
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)
}
创建一个文件example.proto,如下所示:
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)
}
package main;
message Entry {
required string Key = 1;
required string Val = 2;
}
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)
}
您可以通过运行以下命令从proto文件生成go代码:
$ protoc --go_out=. *.proto
如果愿意,可以检查生成的文件
您可以运行并查看结果输出:
$ go run *.go
[10 2 107 49 18 2 118 49]
如果压缩名为
a.txt
的文件,其中包含文本“hello”
(5个字符),则压缩结果大约为115字节。这是否意味着zip格式不能有效地压缩文本文件?当然不是。头顶上有一根绳子。如果文件包含100次(500字节)的“hello”(你好),压缩它将导致文件120字节<代码>1x“你好”=>115字节,100x“你好”
=>120字节!我们添加了495个字节,但压缩大小只增加了5个字节
包中也出现了类似的情况:
该实现为流中的每种数据类型编译自定义编解码器,并且在使用单个编码器传输值流时效率最高,从而分摊编译成本
当您“首先”序列化类型的值时,还必须包含/传输类型的定义,以便解码器能够正确解释和解码流:
溪流是自我描述的。流中的每个数据项前面都有其类型的规范,以一小组预定义类型表示
让我们回到您的示例:
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
e := Entry{"k1", "v1"}
enc.Encode(e)
fmt.Println(buf.Len())
它打印:
48
现在,让我们对更多相同类型的代码进行编码:
现在输出为:
60
72
试穿一下
分析结果:
同一条目
类型的附加值仅花费12字节,而第一个值为48字节,因为还包括类型定义(约26字节),但这是一次性开销
所以基本上你传输2个string
s:“k1”和“v1”都是4个字节,而且string
s的长度也必须包括在内,使用4
字节(32位体系结构上int
的大小)给你12个字节,这是“最小值”。(是的,您可以使用较小的长度类型,但这有其局限性。对于较小的数字,可变长度编码将是更好的选择,请参阅软件包。)
总之,encoding/gob
非常适合您的需要。不要被最初的印象所愚弄
如果一个条目的这12个字节对您来说太“多”,您可以始终将流包装到一个or writer中以进一步减小大小(以换取较慢的编码/解码速度和略高的进程内存需求)
演示:
让我们测试3种解决方案:
- 使用“裸”输出(无压缩)
- 使用
compress/flate
压缩encoding/gob的输出
- 使用
compress/gzip
压缩encoding/gob的输出
我们将编写一千个条目,更改每个条目的键和值,即“k000”
,“v000”
,“k001”
,“v001”
等。这意味着条目的未压缩大小为4字节+4字节+4字节=16字节(2x4字节文本,2x4字节长度)
代码如下所示:
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)
}
package main;
message Entry {
required string Key = 1;
required string Val = 2;
}
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)
}
试穿一下
正如您所看到的:“裸”输出是16.04字节/条目
,略高于计算的大小(由于上面讨论的一次性开销很小)
使用flate或gzip压缩输出时,可以将输出大小减少到约4.13字节/条目
,约为理论大小的26%,我相信这会让您满意。(请注意,对于“真实”数据,压缩比可能会高得多,因为我在测试中使用的键和值非常相似,因此可压缩性非常好;对于真实数据,静态压缩比应该在50%左右)。您非常害怕的是,“手动编码”在使用标准的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())
}
请注意,在本例中,我正在写入字节缓冲区,但这只是出于演示目的,因为encode()
写入io.Writer
,您可以将打开的文件、网络套接字和实现该接口的任何其他内容传递给它。令人印象深刻的分析(我总是钦佩您的答案)但在这种特殊情况下,这似乎是在向一个问为什么他的三轮自行车有点慢的孩子解释火箭科学虽然我认为 GOB肯定有它的用途,但是OP似乎有这样简单的任务,我确信C++中已经完成的一个简单的重新实现是有必要的。这种方法的另一个好处是,新代码可以与它们拥有的遗留数据进行比较。@kostix这也是我对这个问题的第一个想法和印象,但后来我看到了它的最后一行:“没有手动编码”。。。这就是为什么我决定继续使用encoding/gob
。在您的评论之后,我甚至想建议继续发布“手册”版本+1.