为啥这个函数会占用大量内存?
Posted
技术标签:
【中文标题】为啥这个函数会占用大量内存?【英文标题】:Why this function uses a lot of memory?为什么这个函数会占用大量内存? 【发布时间】:2014-08-13 11:55:00 【问题描述】:我正在尝试将 1.4 亿位的二进制向量解压缩到列表中。 我正在检查这个函数的内存使用情况,但它看起来很奇怪。内存使用量上升到 35GB(GB 而不是 MB)。如何减少内存使用量?
sub bin2list
# This sub translates a binary vector to a list of "1","0"
my $vector = shift;
my @unpacked = split //, (unpack "B*", $vector );
return @unpacked;
【问题讨论】:
Perl 标量需要several bytes。为什么需要解包向量? 我说的是 35GB,几个字节不算什么。我需要它有几个原因,但我无法更改它。对我来说主要问题是“@unpacked”数组的大小! 你在函数的第一行复制。 2日复制。回本。它的高级代码效率极低,因此内存使用是成本。描述您无法更改的确切内容,我们可能会找到解决方法。 我需要“0”和“1”数组中的二进制文件。我不能返回值。我不能说 $vector 大小约为 600MB(没关系),并且在运行此行“my @unpacked = split //, (unpack "B*", $vector );" 时,内存使用量增加到 35GB。我需要减少这个。 illguts 有关于标量格式的信息。 【参考方案1】:标量包含大量信息。
$ perl -MDevel::Peek -e'Dump("0")'
SV = PV(0x42a8330) at 0x42c57b8
REFCNT = 1
FLAGS = (PADTMP,POK,READONLY,pPOK)
PV = 0x42ce670 "0"\0
CUR = 1
LEN = 16
为了使它们尽可能小,一个标量由两个内存块[1]、一个固定大小的头部和一个可以“升级”以包含更多信息的主体组成.
可以包含字符串的最小标量类型(例如split
返回的标量)是SVt_PV
。 (通常叫PV
,但PV
也可以指指向字符串缓冲区的字段名,所以我就用常量名吧。)
第一个块是头部。
ANY
是指向正文的指针。
REFCNT
是一个引用计数,允许 Perl 知道何时可以释放标量。
FLAGS
包含有关标量实际包含的信息。 (例如SVf_POK
表示标量包含一个字符串。)
TYPE
包含标量类型的信息(它可以包含什么样的信息。)
对于SVt_PV
,最后一个字段指向字符串缓冲区。
第二个块是主体。 SVt_PV
的正文具有以下字段:
STASH
未在相关标量中使用,因为它们不是对象。
MAGIC
不用于有问题的标量。 Magic 允许在访问变量时调用代码。
CUR
是缓冲区中字符串的长度。
LEN
是字符串缓冲区的长度。 Perl 过度分配以加快连接速度。
右边的块是字符串缓冲区。您可能已经注意到,Perl 过度分配。这加快了连接速度。
忽略底部的块。它是特殊字符串(例如哈希键)的字符串缓冲区格式的替代方案。
加起来是多少?
$ perl -MDevel::Size=total_size -E'say total_size("0")'
28 # 32-bit Perl
56 # 64-bit Perl
这只是标量本身。没有考虑三个内存块的内存分配系统的开销。
这些标量在一个数组中。数组实际上只是一个标量。
所以听到了一个数组。
$ perl -MDevel::Size=total_size -E'say total_size([])'
56 # 32-bit Perl
64 # 64-bit Perl
这是一个空数组。你有 1.4 亿个标量,所以它需要一个可以包含 1.4 亿个指针的缓冲区。 (在这种特殊情况下,至少不会过度分配数组。)每个指针在 32 位系统上为 4 个字节,在 64 位系统上为 8 个字节。
这使总数达到:
32 位:56 + (4 + 28) * 140,000,000 = 4,480,000,056 64 位:64 + (8 + 56) * 140,000,000 = 8,960,000,064这并没有考虑内存分配开销,但它仍然与您提供的数字有很大不同。为什么?好吧,split
返回的标量实际上与数组内的标量不同。所以暂时,你实际上有 280,000,000 个标量在内存中!
内存的其余部分可能由当前未执行的 subs 中的词法变量持有。词法变量通常不会在范围退出时释放,因为预计 sub 在下次调用时将需要内存。这意味着bin2list
在退出后会继续使用 140MB 内存。
脚注
-
未定义的标量可以在没有主体的情况下消失,直到为其分配值。只包含整数的标量可以通过将整数存储在与
SVt_PV
存储指向字符串缓冲区的指针相同的字段中而无需为正文分配内存块。
图片来自illguts。它们受版权保护。
【讨论】:
感谢您的详尽回答。【参考方案2】:Perl 中的单个整数值将存储在SVt_IV
或SVt_UV
标量中,其大小将是四个机器大小的字 - 在 32 位机器上是 16 个字节。因此,其中 1.4 亿个数组将消耗 22 亿字节,假设它被密集地打包在一起。再加上用于引用它们的AvARRAY
中的SV *
指针,我们现在有28 亿字节。现在翻倍,因为您在返回数组时复制了该数组,而我们现在有 56 亿字节。
这当然是在 32 位机器上 - 在 64 位机器上,我们又翻了一番,所以 112 亿字节。这假定内存内部完全密集包装 - 实际上这将分阶段和块分配,因此 RAM 碎片将进一步增加。我可以想象总大小约为 350 亿字节。这听起来并不奇怪。
对于一种非常简单的方法来大量减少内存使用量(更不用说所需的 CPU 时间),而不是将数组本身作为列表返回,而是返回对它的引用。然后返回一个引用,而不是 1.4 亿个 SV 的巨大列表;这也避免了第二次复制。
sub bin2list
# This sub translates a binary vector to a list of "1","0"
my $vector = shift;
my @unpacked = split //, (unpack "B*", $vector );
return \@unpacked;
【讨论】:
试过\@unpacked。内存确实少,但不多,只有2GB。有没有办法计算二进制向量中1的数量?这可能是一种解决方法。 @user3787639 来自perldoc -f unpack
:“以下有效计算位向量中设置的位数:$setbits = unpack("%32b*", $selectmask);
”
Re "单个整数值",是的,但是 split
返回的字符串更大。以上是关于为啥这个函数会占用大量内存?的主要内容,如果未能解决你的问题,请参考以下文章
为啥Microsoft Visual Studio 的安装要占用如此大量的C盘空间