【问题标题】:Does erlang implement record copy-and-modify in any clever way?erlang 是否以任何巧妙的方式实现记录复制和修改?
【发布时间】:2011-11-04 16:37:52
【问题描述】:

给定:

-record(foo, {a, b, c}).

我会这样做:

Thing = #foo{a={1,2}, b={3,4}, c={5,6}},
Thing1 = Thing#foo{a={7,8}}.

从语义上看,Thing 和 Thing1 是唯一的实体。但是,从语言实现的角度来看,制作 Thing 的完整副本来生成 Thing1 会非常浪费。例如,如果记录的大小为 1 MB,而我制作了 1000 个“副本”,每个“副本”都修改了几个字节,那么我就烧掉了 1 GB。如果内部结构跟踪父结构的表示,并且每个派生以指示其自己的更改但保留其他所有人的版本的方式标记该父级,则可以以最小的内存开销创建派生。

我的问题是:erlang 是否在内部做任何聪明的事情来保持通常的 erlang 涂鸦的开销;

Thing = #ridiculously_large_record,
Thing1 = make_modified_copy(Thing),
Thing2 = make_modified_copy(Thing1),
Thing3 = make_modified_copy(Thing2),
Thing4 = make_modified_copy(Thing3),
Thing5 = make_modified_copy(Thing4)

...至少?

我问是因为如果是这种情况,我进行跨进程通信的方式会有很多变化。

【问题讨论】:

    标签: memory erlang record internals


    【解决方案1】:

    垃圾收集和内存分配的确切工作原理只有少数人知道。值得庆幸的是,他们非常乐意分享他们的知识,以下内容基于我从 erlang-questions 邮件列表中以及与 OTP 开发人员的讨论中学到的知识。

    在进程之间进行消息传递时,始终会复制内容,因为进程之间没有共享堆。唯一的例外是大于 64 字节的二进制文件,其中只复制一个引用。

    在一个进程中执行代码时,仅更新部分。让我们分析元组,因为这就是您提供的示例。

    元组实际上是一种结构,它保留对堆上某处的实际数据的引用(除了小整数,也许还有一种我不记得的数据类型)。当您更新元组时,例如使用setelement/3,将创建一个新的元组并替换给定的元素,但是对于所有其他元素,仅复制引用。有one exception,我从来没有利用过。

    垃圾收集器跟踪每个元组,并了解何时可以安全地回收任何不再使用的元组。可能是元组引用的数据仍在使用中,在这种情况下,不会收集数据本身。

    与往常一样,Erlang 为您提供了一些工具来准确了解正在发生的事情。 The efficiency guide 详细说明了如何使用 erts_debug:size/1erts_debug:flat_size/1 在进程内部使用和复制时了解数据结构的大小。跟踪工具还可以让您了解垃圾收集的时间、内容和数量。

    【讨论】:

    • 函数erts:size/1erts:flat_size/1应该是erts_debug:size/1erts_debug:flat_size/1
    • 嗨,你说你不能利用的例外是什么?谢谢!
    【解决方案2】:

    记录 foo 的数量为四(包含四个字),但整个结构的大小为 14 个字。任何立即数(pids、端口、小整数、原子、catch 和 nil)都可以直接存储在元组数组中。任何其他无法放入单词的术语,例如其他元组,都不会直接存储,而是由盒装指针引用(盒装指针是一个 erlang 术语,具有到真实 eterm 的转发地址......只是内部)。

    在您的情况下,创建了一个具有相同数量的新元组,原子 foo 和所有指针都从前一个元组复制,除了索引二 a 指向新元组 {7,8} 构成3个字。在堆上总共创建了 5 + 3 个新词,并且仅从旧元组中复制了 3 个词,其他 9 个词不受影响。

    不建议使用过大的元组。更新元组时,需要复制整个元组,即数组而不是深层内容,然后在其他中更新以保留持久数据结构。这也会产生更多的垃圾,迫使垃圾收集器升温,这也会损害性能。由于这个原因,dictarray 模块避免使用大元组,而是使用浅元组树。

    【讨论】:

    • 这很有帮助。一个问题 - 在这种情况下,“catch and nil”是什么意思?
    • nil 是空列表,即最后一个缺点[]。 catch 也是立即数,用于错误处理,但不适用于此上下文。
    【解决方案3】:

    总结:

    Thing = #foo{a={1,2}, b={3,4}, c={5,6}},
    Thing1 = Thing#foo{a={7,8}}.
    

    这里,如果Thing 不再使用,它​​可能会被更新到位并且避免复制元组,正如效率指南所说。 (我认为元组和记录语法被编译成 setelement 之类的东西)

    Thing = #ridiculously_large_record,
    Thing1 = make_modified_copy(Thing),
    Thing2 = make_modified_copy(Thing1),
    ...
    

    这里实际上每次都复制元组。

    我想理论上可以对此进行有趣的优化。如果编译器可以对make_modified_copy 的返回值进行转义分析,并检测到对它的唯一引用是返回的那个,则可以保存有关该函数的这些信息。当它遇到对该函数的调用时,它会知道在适当的位置修改返回值是安全的。

    由于代码替换功能,这只能在模块间调用中执行。

    也许有一天我们会拥有它。

    【讨论】:

      【解决方案4】:

      我绝对可以验证人们已经指出了什么:

      • 记录只是一个元组,记录名称作为第一个元素,所有字段都只是后面的元组元素
      • 当元组的元素发生变化时,在您的情况下更新记录中的字段,只有顶级元组是新的,所有元素都被重用

      这只是因为我们有不可变数据。因此,在您的示例中,每次更新 #foo 记录中的值时,都不会复制元素中的任何数据,只会创建一个新的 4 元素元组(5 个单词)。 Erlang 永远不会在这种类型的操作中或在函数调用中传递参数时进行深度复制。

      【讨论】:

        猜你喜欢
        • 2015-08-02
        • 1970-01-01
        • 1970-01-01
        • 2022-01-04
        • 1970-01-01
        • 1970-01-01
        • 2011-05-11
        • 2020-03-15
        • 1970-01-01
        相关资源
        最近更新 更多