【问题标题】:Re-using the same encoder/decoder for the same struct type in Go without creating a new one在 Go 中为相同的结构类型重新使用相同的编码器/解码器而不创建新的
【发布时间】:2021-11-08 03:49:50
【问题描述】:

我一直在寻找最快/有效的方式来存储数据结构以持久保存在文件系统上。我遇到了gob module,它允许设置编码器和解码器,以便将结构转换为可以存储的 []byte(二进制)。

这相对容易 - 这是一个解码示例:

// Per item get request
// binary = []byte for the encoded binary from database
// target = struct receiving what's being decoded
func Get(path string, target *SomeType) {
    binary = someFunctionToGetBinaryFromSomeDB(path)
    dec := gob.NewDecoder(bytes.NewReader(binary))
    dec.Decode(target)
}

但是,当我将其与 JSON 编码器/解码器进行基准测试时,我发现它的速度几乎是原来的两倍。当我创建一个循环来检索所有结构时,这一点尤其明显。经过进一步研究,我了解到每次创建一个新的解码器真的很昂贵。重新创建了大约 5000 个解码器。

// Imagine 5000 items in total
func GetAll(target *[]SomeType{}) {
    results = getAllBinaryStructsFromSomeDB()
    for results.next() {
        binary = results.getBinary()
        // Making a new decoder 5000 times
        dec := gob.NewDecoder(bytes.NewReader(binary))
        var target someType
        dec.Decode(target)
        // ... append target to []SomeType{}
    }
}

我被困在这里试图弄清楚如何回收(减少重复使用回收!)用于列表检索的解码器。了解解码器需要一个 io.Reader,我认为可以“重置” io.Reader 并在同一地址使用同一阅读器进行 new 结构检索,同时仍在使用同一个解码器。我不知道该怎么做,我想知道是否有人有任何想法可以阐明。我正在寻找的是这样的:

// Imagine 5000 items in total
func GetAll(target *[]SomeType{}) {
    // Set up some kind of recyclable reader
    var binary []byte
    reader := bytes.NewReader(binary)

    // Make decoder based on that reader
    dec := gob.NewDecoder(reader)

    results = getAllBinaryStructsFromSomeDB()
    for results.next() {
        // Insert some kind of binary / decoder reset
        // Then do something like:
        reader.WriteTo(results.nextBinary())

        var target someType
        dec.Decode(target) // except of course this won't work

        // ... append target to []SomeType{}
    }
}

谢谢!

【问题讨论】:

  • 您应该go.dev/blog/pprof 分析该代码 - 永远不要假设您知道您的周期在没有分析的情况下花费在哪里。始终用数据验证您的预感。

标签: go


【解决方案1】:

我一直在寻找最快/有效的方式来存储数据结构以持久保存在文件系统上

不是序列化您的结构,而是主要在适合您使用的预制数据存储中表示您的数据。然后在您的 Go 代码中对该数据进行建模。

这似乎是存储数据的艰难方式或漫长的方式,但它可以通过智能地索引数据并允许在无需大量文件系统访问的情况下完成过滤来解决您的性能问题。

我一直在寻找...要持久化的数据。

让我们从问题陈述开始。

gob 模块允许设置编码器和解码器,以便将结构转换为可以存储的 []byte(二进制)。 但是,……我发现它……很慢。

会的。您必须竭尽全力使数据存储速度变慢。您从存储中实例化的每个对象都必须来自文件系统读取。操作系统会很好地缓存这些小文件,但您仍然会每次都在读取数据。

每次更改都需要重写所有数据,或者巧妙地确定要写入磁盘的哪些数据。回想一下,文件没有“插入”操作;您将在文件中间添加字节之后重写所有字节。

当然,您可以同时执行此操作,并且 goroutine 可以很好地处理大量异步工作,例如文件系统读取。但是现在你必须开始考虑锁定了。

我的观点是,以尝试序列化结构的成本为代价,您可以更好地在持久层描述您的数据,并解决您甚至尚未解决的问题。

SQL 是一个非常明显的选择,因为您可以使其与 sqlite 以及其他可扩展的 sql 服务器一起使用;我听说最近 mongodb 很容易争吵,根据您对数据的处理方式,redis 有许多有吸引力的列表、集合和 k/v 操作,可以轻松地实现原子化和一致。

【讨论】:

  • 感谢您的回复!你所描述的实际上是我试图讽刺地摆脱的。我正在尝试制作一个非常轻量级的程序,所以大数据库(mongodb、mysql 等)是不可能的。目前我正在使用 SQLite,但我意识到 SQL 编译的巨大开销,以及定义表和所有这些东西、遍历行等所需的样板数量。我使用 BadgerDB 作为 K/V 存储,以及我现在正在做的已经比 SQLite 快 5 倍(包括预编译查询),并且每个结构的代码减少 5 倍。但我知道它可以更快!
  • 我只是觉得真的不需要SQL的开销。我所做的只是选择、删除、更新。我的整个应用程序中大约有两个 SQL 连接,坦率地说,它们是不必要的。如果我可以像在结构中使用映射一样存储非结构化数据,我也根本不需要这样做(无论如何我可以通过将它们编码为 BLOB 但那将只是 extra SQL 之上的开销)。去掉中间人并将结构作为 GOB 保存在像 Badger 这样的快速 KV 存储中会更有意义
  • @Pete 是的,将结构值存储为 blob 对于许多应用程序来说绰绰有余。 MongoDB、Redis、文件系统或其他 K/V 存储之间的选择取决于您的问题范围之外的要求。您还应该考虑消息包、JSON 或 BSON 编码。
  • 如果您是数据的唯一读者,则可能很容易识别数据何时发生变化。在启动时加载它,从内存中读取它,当它改变时,更新存储。这应该很快,除非数据变化很快。
【解决方案2】:

编码器和解码器旨在处理值流。编码器在传输类型的第一个值之前将描述 Go 类型的信息写入流一次。解码器保留接收到的类型信息,用于解码后续值。

编码器写入的类型信息取决于编码器遇到唯一类型的顺序、结构中字段的顺序等等。为了理解流,解码器必须读取由单个编码器写入的完整流。

由于类型信息的传输方式,无法回收解码器。

为了更具体,以下内容不起作用:

 var v1, v2 Type

 var buf bytes.Buffer
 gob.NewEncoder(&buf).Encode(v1)
 gob.NewEncoder(&buf).Encode(v2)

 var v3, v4 Type
 d := gob.NewDecoder(&buf)
 d.Decode(&v3)
 d.Decode(&v4)

每次调用 Encode 都会将有关 Type 的信息写入缓冲区。第二次调用 Decode 失败,因为收到了重复的类型。

【讨论】:

  • 我不确定您所说的“描述类型的信息由编码器写入一次流并由解码器保留。”......将数据库输出的迭代作为流?然后解码器可以像流一样读取哪个?或者你是说你只能写 one 结构(我认为你写的那行我不太清楚)?
  • 感谢 Gopher 更加清晰! :)
猜你喜欢
  • 2021-06-20
  • 2020-08-29
  • 1970-01-01
  • 2015-11-07
  • 2021-09-13
  • 2020-12-15
  • 1970-01-01
  • 2021-02-25
  • 2019-07-28
相关资源
最近更新 更多