【问题标题】:Why does Perl sometimes recycle references?为什么 Perl 有时会回收引用?
【发布时间】:2020-03-31 06:11:11
【问题描述】:

以下代码:

use strict;
use warnings;

my @array = (0,1,2,3,4,5,6,7,8,9);
print "array ref is ".\@array."\n";

my @copy_refs;
for my $element(@array) {
    my @copy = @array;
    print "copy ref is ".\@copy."\n";
    push @copy_refs, \@copy;
}

如人们所料,产生以下输出:

array ref is ARRAY(0x21ae640)
copy ref is ARRAY(0x21e2a00)
copy ref is ARRAY(0x21d7368)
copy ref is ARRAY(0x21d71e8)
copy ref is ARRAY(0x21d70c8)
copy ref is ARRAY(0x21d6fa8)
copy ref is ARRAY(0x21d6e88)
copy ref is ARRAY(0x21d6d68)
copy ref is ARRAY(0x21d6c48)
copy ref is ARRAY(0x21cf8a0)
copy ref is ARRAY(0x21cf780)

但是,删除push @copy_refs, \@copy; 的相同代码会产生以下输出:

array ref is ARRAY(0x229e640)
copy ref is ARRAY(0x22d2a00)
copy ref is ARRAY(0x22d2a00)
copy ref is ARRAY(0x22d2a00)
copy ref is ARRAY(0x22d2a00)
copy ref is ARRAY(0x22d2a00)
copy ref is ARRAY(0x22d2a00)
copy ref is ARRAY(0x22d2a00)
copy ref is ARRAY(0x22d2a00)
copy ref is ARRAY(0x22d2a00)
copy ref is ARRAY(0x22d2a00)

这是为什么?

【问题讨论】:

  • 您的代码已经执行push @copy_refs, \@copy;。你的意思是“相同的代码,push @copy_refs, \@array”?
  • 如果您不为每次迭代保存对@copy 的引用,它将在每次循环迭代时被销毁。因此,下一个数组@copy 可能使用与前一个相同的内存地址
  • 你的评论很有道理哈康。我认为这是正确的答案。

标签: arrays perl reference


【解决方案1】:

Perl 使用引用计数来确定何时释放变量。一旦没有任何引用变量,它就会被释放。代码块(例如 subs 和 files)在变量处于作用域时维护对其中声明的词法变量的引用,因此变量通常在作用域退出时被释放。

换句话说,my @copy 分配了一个新数组。当范围退出时,@copy 如果没有其他引用它就会被释放。

当您执行push @copy_refs, \@copy; 时,@copy_refs 引用该数组,因此@copy 不会被释放。

当你省略push @copy_refs, \@copy; 时,没有任何东西引用@copy,所以它被释放了。这使得内存可以在下一次循环中重复使用。

有点。

作为一种优化,如果在变量超出范围时没有引用变量,它只会被清除而不是销毁和重新创建。所以@copy 不仅仅是在同一位置重新分配;它实际上是被清除的同一个数组。

【讨论】:

    【解决方案2】:

    好的,my @copy = @array 创建名为 @copy 的新数组,并从 @array 复制每个元素。

    如果你push @copy_refs, \@copy,那么数组@copy在循环步骤结束时不会被破坏,在循环的新循环中@copy继续存在,新的@copy数组被创建它占用新的内存中的位置(内存空间地址改变了)。

    如果您不使用push @copy_refs, \@copy,则数组@copy 在循环循环步骤结束时被销毁,并在循环的下一个循环步骤中在可能相同的内存位置重新创建(您可以看到它的地址与上一个相同循环的步骤)。

    某些变量占用的内存仅在代码停止使用它时才被释放 - 在您的情况下,push @copy_refs, \@copy 仍然使用数组并防止它被破坏(内存仍然被数组占用)。

    对不起,可能我的解释不是很直观,也不是很透明。但想法是@copy_refs 保留@copy 数组的位置,以防止其被破坏。

    【讨论】:

    • 我认为你的答案几乎是正确的,但是“颠倒了”。您会看到第一组输出来自所显示的代码。这是移除了推送的第二组输出。我想如果你按照 Hakon Haegland 对我的问题的评论修改你的答案,我可以接受。
    • 对不起,你是对的——我的想法是对的,但写得不好。谢谢,我更正了我的答案。
    • 虽然这个答案与实际实现有一些明显的偏差,但它确实传达了正确的想法,而且偏差只在人们不应该关心的极端情况下才有意义。
    【解决方案3】:

    这是对 Perl 虚拟机的优化。 (请注意,这也可能是由于“机会”,但在这种情况下不是)

    免责声明:我不知道 Perl 如何优化其内存管理的细节。不过,我对一般虚拟机的内存管理很了解。

    几个初步的词,在同一页上:Perl 通过garbage collector 使用自动memory management(准确地说是reference counting)。这个(激进的?)句子的意思是,在 Perl 中,当你想创建一个对象时,你不需要手动向操作系统请求内存,也不需要显式销毁你的对象以将内存返回给操作系统。

    当你这样做时

    my @copy = @array;
    

    Perl 分配一个数组,并将@array 复制到其中。天真地,分配一个数组意味着向操作系统请求一些内存,并跟踪该内存对应于变量@copy。但是,向操作系统请求内存非常慢。

    此外,如果 for 循环的每次迭代都在新位置分配一个新数组,它将使用大量内存:在您的情况下,是 @array 大小的 10 倍(因为您循环了 10 次)。这会很糟糕,特别是因为在迭代 n+1 中,您无法访问在迭代 n 中分配的 @copy(因为您没有保留对它的引用)。

    视觉表示可能有助于理解。取下面的内存表示

    +-----------------------------------------------------------
    +                       empty
    +-----------------------------------------------------------
    

    在循环的第一次迭代中分配@copy 后,您将获得:

    +-----------------------------------------------------------
    +  @copy |                     empty
    +-----------------------------------------------------------
    

    现在,在第一次迭代之后,无法再访问@copy。然后 Perl 可以销毁它,然后你的记忆就会变成:

    +-----------------------------------------------------------
    +  empty |                   empty 
    +-----------------------------------------------------------
    

    然后第二次迭代会在前一次迭代之后分配@copy

    +-----------------------------------------------------------
    +  empty |  @copy  |                empty
    +-----------------------------------------------------------
    

    在第三次迭代中,您会得到:

    +-----------------------------------------------------------
    +  empty |  empty  | @copy  |            empty
    +-----------------------------------------------------------
    

    等你十次迭代。很明显,拥有这些empty 点并没有什么好处。这可以通过两种方式进行优化:

    • 首先,当释放@copy 时,Perl 可以看到两个empty 块彼此相邻,并将它们合并。这意味着释放@copy后,你会从
    +-------------------------------------------------- ---------- + @复制 |空的 +-------------------------------------------------- ----------

    +-------------------------------------------------- ---------- + 空 |空的 +-------------------------------------------------- ----------

    +-------------------------------------------------- ---------- + 空 +-------------------------------------------------- ----------

    现在,当下一次迭代分配 @copy 时,它将转到上一次迭代中的相同位置。因此,您将在所有迭代中为所有 @copy 获得相同的引用。

    • Perl 可以简单地看到,在您的循环中,您分配了@copy,但没有保留对它的引用,因此只分配了一次内存,这些内存将用于@copy 的所有迭代。在这种情况下,您的所有@copy 都会获得相同的引用。

    我不知道 Perl 是否使用这两种机制中的任何一种,但是,它肯定使用了产生类似结果的东西(结果是“保持低内存”,其结果是“所有迭代中的所有 @copy有相同的地址)。根据池上的answer,真正发生的事情类似于我的第二个要点。


    请注意,我在该答案中假设 Perl 不会为其分配的每个对象从操作系统 (= malloc) 请求一些新内存,而是请求大量内存(直接使用 mmap , 或大号malloc)。如果 Perl 为其分配的每个对象使用单个 malloc,那么重用每个 @copy 的内存就变得更加重要:在每次迭代中调用 mallocfree 会非常慢。

    【讨论】:

    • Re "因此只分配一次内存",它就是这样做的。事实上,它把这一点发挥到了极致。在范围退出时,词法变量要么被清除(如果没有引用它),要么被新分配的 var 替换。是的,你没看错,Perl 在 exit 范围内 分配 词法变量。
    • @ikegami 谢谢,很有趣! (不会猜到,但它是有道理的):)
    • 这就是为什么人们使用sub foo { my $var if 0; ... } 创建一个在调用foo 之间不明确的变量。 Perl 知道在范围退出时清除/重新分配变量,因为my 具有在堆栈上放置指令以执行此操作的运行时效果。通过跳过my 的运行时效果,您将获得一个永远不会清除的变量。 (由于my 的编译时效应,它仍然是词法范围的。)也就是说,这是官方未定义的行为,应该改用sub foo { state $x; ... }{ my $x; sub foo { ... } }
    • @ikegami 哇,太好了!我从没见过my $var if 0,但是我见过人们(错误地)做my $var = x if y,并且一直认为接受这种形式是Perl的一个错误(因为直觉上,你可能认为它相当于my $var = $y && $x ,但不是)。再次感谢:)
    【解决方案4】:

    你可能打算这样做

    use strict;
    use warnings;
    
    use Data::Dumper;
    
    my $debug = 1;
    
    my @array = (0,1,2,3,4,5,6,7,8,9);
    
    print "array ref is ".\@array."\n";
    
    my @copy_refs;
    
    for my $element(@array) {
        {                                       # new scope opens
            my @copy = @array;                  # new @copy array in new scope
            print "copy ref is ".\@copy."\n";
            push @copy_refs, \@copy;            # due new scope we have new array ref
        }                                       # and here scope closes 
                                                # @copy would be destroyed 
                                                # if it was not stored in @copy_ref
    }
    
    print Dumper(\@copy_refs) if $debug;
    

    输出 ($debug = 0)

    array ref is ARRAY(0x2664388)
    copy ref is ARRAY(0x26ecb10)
    copy ref is ARRAY(0x2663b00)
    copy ref is ARRAY(0x2663d40)
    copy ref is ARRAY(0x2663f80)
    copy ref is ARRAY(0x26641c0)
    copy ref is ARRAY(0x2664400)
    copy ref is ARRAY(0x2664640)
    copy ref is ARRAY(0x26e7250)
    copy ref is ARRAY(0x27e5608)
    copy ref is ARRAY(0x26889f0)
    

    【讨论】:

    • 谢谢,但我并没有试图用这段代码实现任何目标。我只是对这种行为感到好奇。
    • 嗯,但有时了解under the hood/code 发生的事情会很有用,而且它可以用于优势。
    猜你喜欢
    • 1970-01-01
    • 2010-12-11
    • 2015-01-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-12-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多