我正在读取一个巨大的文件,以将数据存储在一个非常大的哈希中。我试图使 RAM 使用量尽可能小。
我有一个 MWE,它在 Perl 中表现出奇怪的行为:
#!/usr/bin/env perl
use 5.038;
use warnings FATAL => 'all';
use autodie ':default';
use DDP {output => 'STDOUT', array_max => 10, show_memsize => 1}; # pretty print with "p"
my @l = split /\s+/, 'OC Pimascovirales; Iridoviridae; Betairidovirinae; Iridovirus.';
p @l;
$_ =~ s/[\.;]$// foreach @l; # single line keeps code shorter
p @l;
它有输出:
[
[0] "OC",
[1] "Pimascovirales;",
[2] "Iridoviridae;",
[3] "Betairidovirinae;",
[4] "Iridovirus."
] (356B)
[
[0] "OC",
[1] "Pimascovirales",
[2] "Iridoviridae",
[3] "Betairidovirinae",
[4] "Iridovirus"
] (400B)
虽然这个示例非常小,但我将多次执行此操作,因此 RAM 管理非常重要。
减少字符串长度如何将该数组的 RAM 大小从 356B增加到400B?
如果可能的话,我可以避免这样的增加吗?
这是写时复制的结果。换句话说,在您开始更改字符串之前,Perl 只知道在原始字符串中查找它们的位置,但不会复制它们。
使用Devel::Peek查看:
替换前:
后:
所有元素(除了第一个元素)最初都有
IsCOW
标志。为什么OP修改后的数据结构使用更多内存?
s///
创建新标量而不是就地修改字符串,并且s///
碰巧创建了比split
OP示例中具有更大字符串缓冲区的新标量。我在下面更详细地解释了这两个问题,但这确实是这样。
为什么不
s///
就地修改字符串?至少在本文的重要方面,以下两个片段自 5.20 以来是等效的:
这里的关键点是现有的标量正在被新的标量所取代。
但情况并非总是如此。曾几何时,Perl 在使用 删除缓冲区末尾时会简单地减少缓冲区的已用大小
s///
,从而不会使用额外的内存。下面的简单程序证明了这一点:请注意,字符串缓冲区位于
0x55da3c08e7c0
之前和之后。仅缓冲区的使用量 (CUR
) 发生变化。跳到 5.20,你会得到一些不同的东西。
请注意,字符串缓冲区从
0x55ee06d4d530
移至0x55ee06d3acf0
。正在制作缓冲区的副本,这至少会导致暂时的额外内存使用。
改变的是 5.20 引入了写时复制(“COW”)机制。由于这种机制,包含字符串的标量副本不再复制字符串缓冲区。仅复制指向缓冲区的指针,并将字符串缓冲区标记为与该
IsCOW
标志共享。当您执行正则表达式匹配时,会生成所匹配标量的副本。该副本通过魔法附加到所有适用的捕获变量(
$1
等),包括$&
和类似的。但由于新的 COW 机制,不会对字符串缓冲区进行任何复制。原始文件和副本都共享相同的字符串缓冲区,直到其中之一发生更改。在我们的场景中,其中一个发生了更改,但过了一会儿,因为我们正在执行就地替换。
$_
因此获得一个新的缓冲区来保存修改后的值。这就是我在本答案开头描述的等价性。如果我们避免改变原始标量,我们可以看到 COW 机制正在发挥作用。
请注意,标量
IsCOW
在正则表达式匹配后设置了标志。它的 buffer(0x55b9dacf5790
) 与 关联的标量共享$&
。使用 COW 捕获变量使代码更清晰、修复了错误并提高了性能。
下次在同一范围内进行匹配时,匹配字符串的副本使用的内存将被释放,因此该副本“丢失”的内存不会累积。
@l
这意味着由此丢失的内存与OP示例中的长度无关。为什么
s///
创建具有比 更大的字符串缓冲区的标量split
?因为
s///
“构建”字符串,所以split
在为字符串创建标量之前,where 知道要返回的字符串。Perl 注重速度,但以牺牲(通常是大量)内存为代价。实现此目的的一种方法是分配大于所需的字符串缓冲区。在这种情况下,将使用更大的缓冲区创建新标量。
split
不会“构建”字符串。当它创建标量时,它知道要放入标量中的字符串的确切长度。s///r
不知道它将预先返回的字符串的最终长度。它通过附加到它创建的标量来“构建它”。当标量的字符串缓冲区变满时,它会经历大小扩展。字符串构建方式的差异导致了缓冲区大小的差异。
split
在 OP 的示例中分配缓冲区大小为 16、17、16、19、16 的标量。s///
在 OP 的示例中分配缓冲区大小为 16、40、16、40、16 的标量。要回答问题的第二部分:您可以使用
split /[;.\s]+/
,结果数组将为 354B,并包含您想要的值,无需进行后处理(也无需字符串复制)。假设除了单词末尾之外的任何地方都没有分号或点;如果这是不正确的,您可以使用不太漂亮的(并且可能稍微慢一些)
split /(?:[;.](?=\s))?\s+/
。