为什么这个函数使用了大量内存?

时间:2014-08-13 11:55:01

标签: perl memory binary unpack

我试图将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;

}

2 个答案:

答案 0 :(得分:6)

Scalars包含大量信息。

$ 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] 组成,一个固定大小的头部,一个可以升级的主体&#34 ;包含更多信息。

可以包含字符串的最小类型的标量(例如split返回的标量)是SVt_PV。 (它通常称为PV,但PV也可以引用指向字符串缓冲区的字段的名称,因此我将使用常量的名称。 )

SVt_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

这仅仅是标量本身。它没有考虑三个内存块的内存分配系统的开销。


这些标量位于数组中。数组实际上只是一个标量。

SVt_AV

因此无法听到数组。

$ 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个标量!


内存的其余部分可能由当前正在执行的子中的词汇变量保留。词典变量通常不会在范围退出时释放,因为它预期子将在下次调用时需要内存。这意味着bin2list退出后会继续消耗140MB的内存。


脚注

  1. 未定义的标量可以在没有正文的情况下离开,直到为它们分配值。仅包含整数的标量可以通过将整数存储在与SVt_PV相同的字段中而不为主体分配内存块而离开,而是将指针存储到字符串缓冲区。

  2. 图片来自illguts。它们受版权保护。

答案 1 :(得分:3)

Perl中的单个整数值将存储在SVt_IVSVt_UV标量中,其大小将是四个机器大小的单词 - 因此在32位机器上,16个字节。因此,其中1.4亿个阵列将消耗22亿字节,假设它密集在一起。再加上用于引用它们的SV *中的AvARRAY指针,我们现在的数量为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;
}