(Ejemplo dado en Elixir).

Supongamos que tengo el siguiente código,

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

Que hasta donde yo sé crea tres tuplas {1, 2} en diferentes ubicaciones de memoria.

El uso de los operadores == o === para comparar cualquiera de las variables a siempre devuelve true. Esto es previsible, ya que estos dos operadores solo difieren al comparar tipos numéricos (es decir, 1 == 1.0 es diferente a 1 === 1.0).

Entonces, intenté comparar las estructuras mediante la coincidencia de patrones, usando el siguiente módulo (creado estrictamente para probar mi caso),

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

Pero llamar a Test.same?(a1, a3) también devuelve true.

¿Cómo puedo comparar dos estructuras usando la igualdad de puntero, para poder determinar si son la misma estructura en la memoria?

Gracias

7
mljrg 10 sep. 2018 a las 16:06

5 respuestas

La mejor respuesta

No hay una forma "oficial" de hacer esto, y diría que si crees que realmente necesitas hacer esto, estás haciendo algo mal y deberías hacer otra pregunta sobre cómo lograr el objetivo quieres lograr Entonces, esta respuesta se ofrece con el espíritu de diversión y exploración, con la esperanza de que difunda algunos conocimientos interesantes sobre la máquina virtual Erlang / Elixir.


Hay una función, erts_debug:size/1, que le dice cuántas "palabras" de memoria ocupa un término Erlang / Elixir. Esta tabla le indica cuántas palabras usan varios términos. En particular, una tupla usa 1 palabra, más 1 palabra para cada elemento, más el espacio de almacenamiento para cualquier elemento que sea "no inmediato". Estamos utilizando enteros pequeños como elementos, y son "inmediatos" y, por lo tanto, "gratuitos". Entonces esto se verifica:

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

Ahora hagamos una tupla que contenga dos de esas tuplas:

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

Eso tiene sentido: las dos tuplas internas son de 3 palabras cada una, y la tupla externa es de 1 + 2 palabras, para un total de 9 palabras.

Pero, ¿qué pasa si ponemos la tupla interna en una variable?

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

¡Mira, ahorramos 3 palabras! Esto se debe a que el contenido de x solo cuenta una vez; la tupla externa apunta a la misma tupla interna dos veces.

Así que escribamos una pequeña función que haga esto por nosotros:

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

Sistema funcionando? Parece ser:

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

Objetivo cumplido!


Sin embargo, supongamos que estamos tratando de llamar a esta función desde otra función en un módulo compilado, no desde el shell iex:

  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

Que imprime:

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

Esto se debe a que el compilador es lo suficientemente inteligente como para ver que esos literales son iguales y los fusiona a un término mientras compila.


Tenga en cuenta que este intercambio de términos se pierde cuando los términos se envían a otro proceso o se almacenan / recuperan de una tabla ETS. Consulte la sección Mensajes de proceso de la Guía de eficiencia de Erlang para obtener más información.

14
legoscia 10 sep. 2018 a las 13:57

Parece que no puede llegar a la ubicación de memoria de una variable en erlang: creo que es una noción clave en este tema. Por lo tanto, solo puede comparar los datos, no el puntero que apunta a esos datos .

Parece que cuando crea varias variables con el mismo valor, crea nuevos datos en la memoria, esos datos son el nombre de la variable y el enlace a los datos primarios (se parece mucho a un puntero). Erlang VM no duplica datos (estoy buscando alguna prueba de eso ... hasta ahora, es solo como lo veo)

2
Nathan Ripert 10 sep. 2018 a las 13:39

que yo sepa, crea tres tuplas {1, 2} en diferentes ubicaciones de memoria.

No, eso no es correcto. Erlang VM es lo suficientemente inteligente como para crear una sola tupla y referirse a ella.

Vale la pena mencionar, eso es posible porque todo es inmutable.

Además, si se encuentra realizando la tarea como se indicó anteriormente, lo está haciendo completamente mal.

2
Aleksei Matiushkin 10 sep. 2018 a las 13:12

Déjame responder mi pregunta:

No es necesario que los desarrolladores hagan una comparación de puntero explícitamente, porque Elixir ya lo hace internamente, en la coincidencia de patrones y en los operadores == y === (a través de los operadores Erlang correspondientes).

Por ejemplo, dado

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

En IEx tenemos

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

Es decir, x y y tienen el mismo contenido, pero la memoria es diferente, porque y ocupa menos memoria que x ya que comparte internamente la subestructura s.

En resumen, == y === hacen la comparación de contenido y puntero. La comparación de punteros es la forma más eficiente para que Erlang evite atravesar la misma subestructura en ambos lados de la comparación, ahorrando así mucho tiempo para grandes estructuras compartidas.

Ahora, si la duplicación estructural en dos estructuras es una preocupación, como cuando se cargan desde dos archivos grandes con contenido similar, entonces uno debe comprimirlos en dos nuevas estructuras que compartan las partes en las que tienen el mismo contenido. Este fue el caso de a1 y a2 que se comprimieron como b1 y b2.

7
mljrg 20 dic. 2018 a las 00:30

Erlang / OTP 22 (y posiblemente antes) proporciona :erts_debug.same/2, que le permitirá realizar la prueba de puntero de memoria deseada. Sin embargo, tenga en cuenta que la función no está documentada y está en un módulo llamado erts_debug, por lo que solo debe confiar en ella para depurar y probar, y nunca en el código de producción.

En mis casi 9 años usando Erlang / Elixir, solo lo he usado una vez, lo que es para probar que no estamos asignando estructuras innecesariamente en Ecto. Aquí está el commit para referencia.

4
José Valim 17 oct. 2019 a las 17:37