Why is &__MODULE__.local/n faster than &local/n?

I noticed that if I pass a captured function (e.g. &logger/4) as an argument to :telemetry.attach/4, I get a warning:

[info] The function passed as a handler with ID “…” is a local function.
This means that it is either an anonymous function or a capture of a function without a module specified. That may cause a performance penalty when calling that handler. For more details see the note in telemetry:attach/4 documentation.
telemetry — telemetry v1.2.1

… but if I change this to &__MODULE__.logger/4, I no longer get that warning. The link says:

Note: due to how anonymous functions are implemented in the Erlang VM, it is best to use function captures (i.e. fun mod:fun/4 in Erlang or &Mod.fun/4 in Elixir) as event handlers to achieve maximum performance. In other words, avoid using literal anonymous functions (fun(...) -> ... end or fn ... -> ... end) or local function captures (fun handle_event/4 or &handle_event/4 ) as event handlers.

Is there more documentation about what “due to how anonymous functions are implemented in the Erlang VM” is alluding to? I read the documentation for &/1 but it doesn’t go into detail about why &foo/n is different from &__MODULE__.foo/n, and I am curious!

The Erlang documentation on functions makes it sound like foo() and m:foo() should have the same performance (Functions — Erlang System Documentation v27.0):

This is a rough hierarchy of the performance of the different types of function calls:

  • Calls to local or external functions (foo(), m:foo()) are the fastest calls.
  • Calling or applying a fun (Fun(), apply(Fun, [])) is just a little slower than external calls.
  • Applying an exported function (Mod:Name(), apply(Mod, Name, [])) where the number of arguments is known at compile time is next.
  • Applying an exported function (apply(Mod, Name, Args)) where the number of arguments is not known at compile time is the least efficient.

Are local function captures (without the module name) compiled into Erlang Fun()s instead of local/external functions? If so, why?

1 Like

You might want to check these similar threads about this topic:

There is a proposal discussing this as well:
https://groups.google.com/g/elixir-lang-core/c/rW_BRCULsGk/m/9rrYEwwtAwAJ

2 Likes

So others can actually find the answer through search & don’t have to click through:

The difference comes into play with code loading. The local captures will reference the version of the module they were captured with, while remote/external captures will always reference the latest version of the module.

From: https://groups.google.com/g/elixir-lang-core/c/rW_BRCULsGk/m/3hYXyye6AAAJ

Made a rough draft of a change to the documentation here: Clarify what specifying the module name does with & by jyc · Pull Request #13668 · elixir-lang/elixir · GitHub

5 Likes