go compress/flate: 非常内存密集

368yc8dk  于 5个月前  发布在  Go
关注(0)|答案(9)|浏览(56)

$x_{1a0b1}x$

如果你运行那个,你会得到:

$x_{1a1b1}x$

每名写入者占用1.2MB,每名读取者占用45KB,这很多了,尤其是在使用WebSockets时,大部分消息都很小,平均大约是512字节。为什么compress/flate会分配这么多内存,有没有方法可以减少它?

gzip(虽然我没有在基准测试中包含它,但是zlib使用的内存要少得多)。

相关:

  1. $x_{1e0f1}x$

  2. $x_{1e1f1}x$

cfh9epnr

cfh9epnr1#

在我的手机上,但是大多数gzip(也许还有flate?)类型都有一个Reset方法,你可以调用它来允许它们的重用。这应该会有很大帮助。如果有原因你不能使用Reset,或者一个关键类型缺少一个Reset选项,请详细说明。

inkz8wg9

inkz8wg92#

在我的手机上,但大多数gzip(也许还有flate?)类型都有一个Reset方法,你可以调用它来允许它们的重用。这应该会有很大帮助。如果有原因你不能使用Reset,或者一个关键类型缺少Reset选项,请详细说明。
是的,它们两者都确实有一个Reset方法,并且确实有所帮助。
然而,与compress/gzip或compress/zlib相比,flate使用的内存仍然非常多。

yfjy0ee7

yfjy0ee73#

然而,与compress/gzip或compress/zlib相比,flate使用的内存仍然非常多。由于gzip和zlib都使用deflate并存储*flate.Writer,你的基准测试具有误导性。gzipzlib在数据写入流之前不会分配压缩器,所以如果你只写入一次,你会看到相同的数字。而且在实际使用中,结果也是一样的。

但是,让我们把它放在上下文中考虑。deflate压缩确实会进行大量的预先分配。这些分配是为了在不进行额外分配的情况下进行标准操作,以及为什么“重置”可用来重用。

正如我在gorilla ticket上写的:对于压缩级别1(最快),这也意味着许多不必要的分配。几年前,我进行了一个实验,以更有选择地进行分配。这主要是为了在使用Reset时不使用它,但它将产生较少分配的副作用。

未完成的PR在这里:klauspost/compress#70 - 注意,“级别2”相当于stdlib级别1。

一个更简单的优化可能是:对于级别1、0和-2,压缩器中的以下数组不需要:hashHead(512KB) hashPrev(128KB)。将其添加到仅在需要时分配的结构中应该是相当简单的。hashMatch(1KB)也不需要,但现在我们开始涉及到小事情了。

lztngnrs

lztngnrs4#

由于gzip和zlib都使用deflate算法并存储*flate.Writer,所以你的基准测试具有误导性。gzip和zlib在数据写入流之前不会分配压缩器,因此如果你只进行一次写入操作,你会看到相同的数字。在实际应用中,结果也是一样的。
这很有道理。
但是,让我们将这个问题放在上下文中考虑。deflate压缩确实会进行大量的预先分配。这些分配是为了在不进行额外分配的情况下进行标准操作,以及为什么'Reset'可用来重用。
对于写入如此小的消息,我认为1.2 MB是一个非常高的价格。我不是压缩算法的Maven,但缓冲区是否可以动态调整以根据需要增长,而不是总是分配这么多?
一个更简单的优化可能是:对于级别1、0和-2,压缩器中不需要以下数组:hashHead(512KB)和hashPrev(128KB)。将其添加到仅在需要时分配的结构中应该很简单。hashMatch(1KB)也不需要,但现在我们开始讨论细节了。
这将产生巨大的差异,但仍然为每个写入者留下560 KB的空间。在我看来,这对于WebSocket的使用场景仍然显得非常过分。

tmb3ates

tmb3ates5#

以下是上述PR的简化版本:

- [klauspost/compress#107](https://github.com/klauspost/compress/pull/107)
+ - [klauspost/compress#107](https://github.com/klauspost/compress/pull/107)
     @@ -2,6 +2,7 @@
     # This is a comment

     def foo():
         pass
+        return True
     }
xa9qqrwz

xa9qqrwz6#

缓冲区是否可以动态调整以满足需求?
这将带来巨大的性能损失。大分配包括哈希表和链表。哈希表是一种map[uint16]int32查找,但它稀疏地填充,因为这允许非常快的查找,无需边界检查,因为编译器知道它的大小。
在stdlib "level 1"使用自己的(较小的,128KB)哈希表,因此为更昂贵的级别分配的资源未被使用。
这将产生巨大的差异,但仍为每个写入者留下560 KB。在我看来,这对于WebSocket用例仍然显得非常过分。
让我们分解剩下的部分:
64KB在d.window中分配。这是累积输入,直到我们有足够的数据块。这可以是预先分配较少的空间,但这意味着随着内容被写入而分配空间。
64KB用于“tokens”,即压缩阶段的输出。这可以少一些,但也会导致压缩过程中的分配。
无论输入有多大,都需要Huffman树和直方图。只有“level -2”(HuffmanOnly)可以使用略少的空间。
最后,我想出的是输出缓冲区,大小为256字节。在那里没有什么可获得的。
让我看看我能否修复你的基准测试,以获得一些实际数字。

4smxwvx5

4smxwvx57#

io.Writer接口使得更详细的优化变得困难。即使你只发送几个字节,我们也无法知道你不会发送更多。
一个合理的补充是Encode(src, dst []byte) []byte,它允许你发送整个想要压缩的内容。这将允许压缩器选择合适的压缩方案并共享压缩器。

du7egjpx

du7egjpx8#

我已经更新了 klauspost/compress#107,其中包含了剩余的实际数字,以下是一个实际基准的摘要:https://gist.github.com/klauspost/f5df3a3522ac4bcb3bcde448872dffe6
大部分剩余的分配用于Huffman表生成器,这在无论输入大小的情况下都是不可避免的。再次注意,我的库中的“level 2”是stdlib中的“level 1”。所以,stdlib的基线大约是540K。如果你切换到我的库,那么对于level 1来说,大约是340KB。

w46czmvw

w46czmvw9#

来自@klauspost的gorilla/websocket#203(评论)的一些令人兴奋的更新。

相关问题