【问题标题】:How to compare two structures via pointer equality in Elixir / Erlang如何通过 Elixir / Erlang 中的指针相等比较两个结构
【发布时间】:2018-09-10 13:06:28
【问题描述】:

(Elixir 中给出的示例。)

假设我有以下代码,

x = {1, 2}
a1 = {"a", {1, 2}}
a2 = {"a", {1, 2}}
a3 = {"a", x}

据我所知,它在不同的内存位置创建了三个元组{1, 2}

使用运算符===== 比较任何a 变量总是返回true。这是意料之中的,因为这两个运算符仅在比较数字类型时有所不同(即,1 == 1.01 === 1.0 不同)。

然后,我尝试通过模式匹配比较结构,使用以下模块(严格创建以测试我的案例),

defmodule Test do
  def same?({x, y}, {x, y}), do: true
  def same?(_, _), do: false
end

但调用Test.same?(a1, a3) 也会返回true

如何使用指针相等来比较两个结构,以便确定它们在内存中是否是相同的结构?

谢谢

【问题讨论】:

  • 这是一个非常有趣的问题,我希望看到一个答案,并参考 Erlang VM 的工作方式。它将对 Elixir/erlang 机制有所了解。
  • @NathanRipert 我想我已经回答了你正在寻找的方式。
  • 您可能会发现:github.com/happi/theBeamBook 有助于回答此类问题。简短的回答:你不应该关心。 Erlang 被有意设计为隐藏这些实现细节。
  • @OnorioCatenacci 是的,我不应该在意。但我应该注意大型结构之间的重复,特别是当重复可能非常大时。正是在这种情况下,为了消除这种重复,我提出了这个问题。请参阅我自己对以下问题的回答。
  • @OnorioCatenacci 如果我理解你担心的事情(谢谢,我很感激),我知道 Erlang 会复制除大型二进制文件之外的所有内容,这意味着它将复制大型结构,从而产生重复再一次,对吧?但是,如果可能具有重复子树的结构始终存在于同一进程中,那么在进程中删除此类重复可以节省大量内存。我说的是在同一进程中压缩大量重复数据。

标签: erlang elixir


【解决方案1】:

没有“官方”的方式来做到这一点,我想说,如果你认为你确实需要这样做,那么你做错了什么,应该问另一个问题关于如何达到你想要达到的目标。所以这个答案是本着有趣和探索的精神提供的,希望它能传播一些关于 Erlang/Elixir VM 的有趣知识。


有一个函数,erts_debug:size/1,它告诉你一个 Erlang/Elixir 术语占用了多少内存“单词”。 This table 告诉您各种术语使用了多少字。特别是,元组使用 1 个单词,每个元素加上 1 个单词,再加上任何“非立即”元素的存储空间。我们使用小整数作为元素,它们是“立即数”,因此是“免费的”。所以这检查出来:

> :erts_debug.size({1,2})
3

现在让我们创建一个包含其中两个元组的元组:

> :erts_debug.size({{1,2}, {1,2}})
9

有道理:两个内元组各3个词,外元组1+2个词,一共9个词。

但是如果我们把内部元组放在一个变量中呢?

> x = {1, 2}
{1, 2}
> :erts_debug.size({x, x})
6

看,我们节省了 3 个单词!那是因为x的内容只计算一次;外部元组两次指向同一个内部元组。

所以让我们编写一个小函数来为我们做这件事:

defmodule Test do
  def same?(a, b) do
    a_size = :erts_debug.size(a)
    b_size = :erts_debug.size(b)
    # Three words for the outer tuple; everything else is shared
    a_size == b_size and :erts_debug.size({a,b}) == a_size + 3
  end
end

系统工作?好像是:

> Test.same? x, {1,2}
false
> Test.same? x, x
true

目标完成!


但是,假设我们试图从已编译模块中的另一个函数调用此函数,而不是从 iex shell:

  def try_it() do
    x = {1, 2}
    a1 = {"a", {1, 2}}
    a2 = {"a", {1, 2}}
    a3 = {"a", x}

    IO.puts "a1 and a2 same? #{same?(a1,a2)}"
    IO.puts "a1 and a3 same? #{same?(a1,a3)}"
    IO.puts "a3 and a2 same? #{same?(a3,a2)}"
  end

打印出来的:

> Test.try_it
a1 and a2 same? true
a1 and a3 same? true
a3 and a2 same? true

这是因为编译器足够聪明,可以看到这些文字是相等的,并在编译时将它们合并为一个术语。


请注意,当术语被发送到另一个进程或存储在 ETS 表中/从 ETS 表中检索时,这种术语共享会丢失。详情请见the Process Messages section of the Erlang Efficiency Guide

【讨论】:

  • “这种术语共享丢失了”——这并不完全正确。例如。原子是全局的。
  • @legoscia 所以这意味着如果两个文件中的相同内容必然导致两个结构中的内存重复,这是我所期望的(也是正常的)。
  • 我假设运算符===== 将在内部检测两个子结构何时是内存中的相同元素,并立即将它们与true 进行比较,而不是全部向下搜索相同的子结构比较时。你知道这是真的吗?
  • 是的,发生在这里:github.com/erlang/otp/blob/…
  • 感谢您抽出宝贵时间,特别是您告诉我有关函数 :erts_debug.size(x) 的信息,这对我理解这个主题至关重要。
【解决方案2】:

Erlang/OTP 22(可能更早)提供:erts_debug.same/2,这将允许您进行所需的内存指针测试。但是,请注意该函数未记录在一个名为 erts_debug 的模块中,因此您应该只依赖它进行调试和测试,而不是在生产代码中。

在我使用 Erlang/Elixir 将近 9 年的时间里,我只使用过一次,这是为了测试我们是否没有在 Ecto 中不必要地分配结构。这里是the commit for reference

【讨论】:

    【解决方案3】:

    让我回答我的问题:

    开发人员无需显式进行指针比较,因为 Elixir 已经在内部,在模式匹配和运算符 =====(通过相应的 Erlang 运算符)中这样做了。

    例如,给定

    a1 = {0, {1, 2}}
    a2 = {1, {1, 2}}
    x = {a1, a2}
    s = {1, 2}
    b1 = {0, s}
    b2 = {1, s}
    y = {b1, b2}
    

    在IEx我们有

    Interactive Elixir (1.7.3) - press Ctrl+C to exit (type h() ENTER for help)
    iex(1)> a1 = {0, {1, 2}}
    {0, {1, 2}}
    iex(2)> a2 = {1, {1, 2}}
    {1, {1, 2}}
    iex(3)> x = {a1, a2}
    {{0, {1, 2}}, {1, {1, 2}}}
    iex(4)> s = {1, 2}
    {1, 2}
    iex(5)> b1 = {0, s}
    {0, {1, 2}}
    iex(6)> b2 = {1, s}
    {1, {1, 2}}
    iex(7)> y = {b1, b2}
    {{0, {1, 2}}, {1, {1, 2}}}
    iex(8)> :erts_debug.size(x)
    15
    iex(9)> :erts_debug.size(y)
    12
    iex(10)> x == y
    true
    iex(11)> x === y
    true
    

    也就是说,xy 内容相同,但内存不同,因为 y 占用的内存少于 x,因为它在内部共享子结构 s

    简而言之,===== 进行内容和指针比较。指针比较是 Erlang 避免在比较两侧遍历相同子结构的最有效方法,从而为大型共享子结构节省了大量时间。

    现在,如果需要考虑跨两个结构的结构重复,例如从两个具有相似内容的大文件中加载它们时,则必须将它们压缩成两个新结构,共享它们内容相同的部分。 a1a2 就是这种情况,它们被压缩为 b1b2

    【讨论】:

    • 仅供参考:=== 映射到 erlang 的 =:=比较类型并委托给 ==,如果类型相同。由于它执行了一项额外的操作,因此无论如何它都不会更有效。
    • @mudasobwa 这不是完全正确的描述:例如这意味着{1.0, 1.0} === {1, 1} 是真的,因为它们都是元组并且== 对于它们来说是真的。相反,它像 == 一样是递归的,除了有一个不同的基本情况:它从不将浮点数和整数视为相等。
    • @AlexeyRomanov 是的,确实,我错过了递归部分,谢谢。
    • @AlexeyRomanov 所以这两个运算符仅在浮点数和整数方面有所不同。
    • @mljrg 是的,这是唯一的区别。
    【解决方案4】:

    据我所知,在不同的内存位置创建三个元组{1, 2}

    不,这是不正确的。 Erlang VM 足够聪明,可以创建单个元组并引用它。

    值得一提,这是可能的,因为一切都是不可变的。

    另外,如果你发现自己完成了上述任务,那你就大错特错了。

    【讨论】:

    • 你希望我在这里输入“Erlang VM in a nutshell”一书吗?
    • 如何在同一个 Erlang 进程的不同内存位置创建同一个元组?
    • 你不能,也不应该。
    • 假设我在同一进程中从两个文件创建结构,并且这些文件具有共性。这些结构是否会“自动”共享与文件中相同内容对应的结构的相同部分?如果没有,我该如何检测?
    • 也许是,也许不是。你不能依赖它,这就是重点。
    【解决方案5】:

    您似乎无法访问memory location of a variable in erlang:我认为这是本主题中的一个关键概念。因此,您只能比较数据,而不是指向这些数据的指针

    看起来,当您创建多个具有相同值的变量时,它会在内存中创建新数据,这些数据是变量的名称以及与主数据的绑定(看起来很像一个指针)。 Erlang VM 不会复制数据(我正在寻找一些证据……到目前为止,这就是我所看到的)

    【讨论】:

    • 如果你找到了证据,请写在这里。
    • @mljrg 我会的。我被好奇心迷住了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-12-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-08-23
    • 2012-03-27
    相关资源
    最近更新 更多