Struct instances do not share the key tuple

It appears that structures created with the %Struct{...} syntax do not share the key tuple.

Compare:

iex> Enum.map(1..1000, &%URI{port: &1}) |> :erts_debug.size()
24000
iex(68)> Enum.map(1..1000, &struct!(URI, port: &1)) |> :erts_debug.size()
14010

The difference in memory usage seems significant. Is it a flaw or by design?

3 Likes

That’s interesting. I thought %Struct{...} would just call Struct.__struct__(...), but it must be doing something else as well. Since it is special syntax, I’m not sure where to look for the implementation in the Elixir source code.

1 Like

The %URL{} version expands the struct definition at compile-time. You can demonstrate this with the @compile :S flag:

defmodule Foo do
  defstruct [:bar, :baz, huh: 69]
end

defmodule Bar do
  @compile :S

  def literal_version(x) do
    %Foo{bar: x, baz: 42}
  end

  def struct_version(x) do
    struct(Foo, bar: x, baz: 42)
  end
end

Running elixir on this file fails, but also produces a new file with the extension .S that has BEAM assembly in it.

literal_version compiles to an explicit map operation:

{function, literal_version, 1, 11}.
  {label,10}.
    {line,[{location,"struct_demo.ex",8}]}.
    {func_info,{atom,'Elixir.Bar'},{atom,literal_version},1}.
  {label,11}.
    {put_map_assoc,{f,0},
                   {literal,#{}},
                   {x,0},
                   1,
                   {list,[{atom,'__struct__'},
                          {atom,'Elixir.Foo'},
                          {atom,bar},
                          {x,0},
                          {atom,baz},
                          {integer,42},
                          {atom,huh},
                          {integer,69}]}}.
    return.

Versus the struct version:

{function, struct_version, 1, 13}.
  {label,12}.
    {line,[{location,"struct_demo.ex",12}]}.
    {func_info,{atom,'Elixir.Bar'},{atom,struct_version},1}.
  {label,13}.
    {test_heap,5,1}.
    {put_tuple2,{x,0},{list,[{atom,bar},{x,0}]}}.
    {put_list,{x,0},{literal,[{baz,42}]},{x,1}}.
    {move,{atom,'Elixir.Foo'},{x,0}}.
    {line,[{location,"struct_demo.ex",13}]}.
    {call_ext_only,2,{extfunc,'Elixir.Kernel',struct,2}}.
2 Likes

That makes sense for performance. I also just tried putting the original code into a module and now both examples have the exact same result. So this must be related to IEx and doesn’t make a difference in regular modules.

1 Like

Yeah, you always want to test performance related stuff on modules not ad hoc evaluated code.

2 Likes