Are module attributes "read" multiple times in same function?

Greetings comrades,

According to: https://elixir-lang.org/getting-started/module-attributes.html#as-constants

Every time an attribute is read inside a function, Elixir takes a snapshot of its current value.

My question is:

In this function:

@money %{amount: 0, currency: :DOGE}

def reset_global_debt do
  for human <- Earth.list_humans() do
    update_financial_record_for_human(human, %{
      "total_amount" => @money.amount,
      "base_currency" => @money.currency
    })
  end
end

do 2 copies of @money get created, since it is “read” twice?

I hope not…

1 Like

No. The value is basically a static constant, and in fact won’t even take up memory in the process like a normal map, since it will be just a pointer to the shared constant pool.

Yes, the runtime memory is unaffected.
I’m wondering if there are 2 copies during compile time.

The same article says:

Therefore if you read the same attribute multiple times inside multiple functions, you may end-up making multiple copies of it. That’s usually not an issue, but if you are using functions to compute large module attributes, that can slow down compilation.

It is as if you hand typed that map in two places. This can affect compile times, but it generally requires very large constants to be an issue.

5 Likes

Perfect, thanks

To add onto the previous answer. The cost occurs when you make a change.

The following would be calculated twice. If we were doing expensive or numerous changes, it would affect the time taken to compile

@var1 1+1
@var1 1+2
1 Like

@benwilson512 as a tangent here, could you please teach me how to verify this? I’d love to know how.

1 Like

If you can read erlang you can use mix decompile to turn your compiled module into erlang source. And you will see that whenever an @attribute is used, the litteral value is present in the source.

Now if you cannot read erlang I guess you will recocgnize your own data enough to verify too.

2 Likes

the copies are inside your def. Note that it is not two copies only,
but 2 times the elements of Earth.list_humans()

if you don’t want to create copies, you may want want refactor your code:

@money %{amount: 0, currency: :DOGE}

def reset_global_debt do
  money = @money
  for human <- Earth.list_humans() do
    update_financial_record_for_human(human, %{
      "total_amount" => money.amount,
      "base_currency" => money.currency
    })
  end

you may even want to do

@money %{amount: 0, currency: :DOGE}

def reset_global_debt do
  amount = @money.amount
  currency = @money.currency

  for human <- Earth.list_humans() do
    update_financial_record_for_human(human, %{
      "total_amount" => amount,
      "base_currency" => currency
    })
  end

I don’t understand this at all, sorry.
How are the elements of Earth.list_humans() known at compile time?

1 Like

Sorry, You are right. I was wrong about the part about 2 * number of Earth.list_humans(). It is just two copies.

The rest still applies. as you save from duplicating the content of @money, and access the keys of the map for every element in list_humans at run time.

1 Like

Yup I understand now.
At compile-time, every module attribute usage is effectively “written out” in full in the code.
So it’s only a concern for very large constants, or if the attribute is accessed in a compile-time loop (e.g. in macros).
Thanks.

2 Likes

A good analogy is what happens when you create a guard with defguard, under the hood it creates two versions of the Macro. one that is executed when is called into the guard context, so it is really limited what you can do there, So no variable assignments is allowed,
so if you want to call two different functions on the same argument, you make two copies of it (similar to what you do in your initial example),
but if the macro is called out of the guard context, is it optimized, so each argument is assigned to a variable, and then it is referred to it, as in my version of your code.

For posterity, doing this isn’t better or worse. It’s exactly the same.
Doing money = @money is better, but only slightly. It might save 0.01 nanoseconds of compile time on a 1980s relic.

actually if you want to be performant, ( I initially wrote it like this, i just didn’t want to overcomplicate the example)

   money = @money.amount
  amount = money.amount
  currency = money.currency

The benefits are at runtime (you say two Map.get/3 calls for every element in humans), not compile time.

While at compile time a map of two elements is not going to make a difference, make it a map of 1 million entries, you may see the difference.

EDIT: proper way would be

 %{amount: amount, currency: currency}  = @money
1 Like

Are you 100% certain will be a difference at runtime between:

%{amount: @money.amount}

and

amount = @money.amount
%{amount: amount}

?

I don’t understand your example.
The difference at run time is that you save to access the element (you save two calls per loop).
Why would you want to access the element every time in the loop, when you already know it is amount and it is not going to change?
Store it in the variable, and assign it to your new map key.

1 Like

The compiler should change @money.amount to a constant. So it wouldn’t be a map “access”.

it doesn’t.
here’s your expanded code.

  def reset_global_debt() do
    for human <- Earth.list_humans() do
      update_financial_record_for_human(human, %{
        "total_amount" => %{amount: 0, currency: :DOGE}.amount,
        "base_currency" => %{amount: 0, currency: :DOGE}.currency
      })
    end
  end

And if @money would have 1million entries, you will be creating two copies of a map of one million entries, that are going to be stored in your .beam file,

1 Like

you can achieve this by evaluating the call at compile time, by doing:

unquote(@money.amount)

you can do it by calling unquoteonly since you are inside a macro (for)