I’m trying to “translate” the following (from the Erlang docs Efficiency Guide) to Elixir:
6.2 Function Calls
This is an intentionally rough guide to the relative costs of different calls. It is based on benchmark figures run on Solaris/Sparc:
- Calls to local or external functions (foo(), m:foo()) are the fastest calls.
- Calling or applying a fun (Fun(), apply(Fun, )) is about three times as expensive as calling a local function.
- Applying an exported function (Mod:Name(), apply(Mod, Name, )) is about twice as expensive as calling a fun or about six times as expensive as calling a local function.
This is what I’ve got so far, but really, I don’t understand what an external function is (in the first bullet point).
- Calls to local or external functions (
M.foo() ) are the fastest calls.
- Calling or applying an anonymous function (
apply(fun, ) ) is about three times as expensive as calling a local function.
- Applying a public function from a module (
apply(Mod, :name, ) ) is about twice as expensive as calling an anonymous function or about six times as expensive as calling a local function.
An external function is a function defined in another module.
Or in Elixir
List.map(list, func) or
How does it differ from an exported function, then? Are they different somehow?
If they are the same thing, wouldn’t that make the first and third bullet-points contradictory (in the erlang version from the OP)?
Exported just means that function is visible outside the module. So if you are calling a function in another module it must be an exported function
Right, so, wouldn’t that also be an external function? However, there are two bullet points (the first and third) from the original text that seem to me to be contradictory if this is the case:
- Calls to local or EXTERNAL functions (foo(), m:foo()) are the fastest calls.
- Applying an EXPORTED function (Mod:Name(), apply(Mod, Name, )) is about twice as expensive as calling a fun or about six times as expensive as calling a local function.
Maybe there is a difference here between
Mod:Name() that I don’t understand? I feel like I’m missing something simple… Can somebody explain it like I’m 5?
In elixir parlance we often say “remote call” or “remote function”.
You look at it in bad way … if
exported means any public function (i.e.
def my_func(…)) then it applies as same to
__MODULE__ is compile-time alias for current module).
Anyway your confusion is about those calls:
my_func() - local function - looking only on such call we can’t say if it’s exported - could be, but don’t need to
MyModule.my_func() - if function is called with module and code compiles then that means such function is exported
apply(module, function, args) - this is run-time not optimized call - therefore few times more expensive, we can’t say if such function is exported or not even after compilation as calling here happens on run-time
Of course we always can look at those functions definitions.
From this all
apply/3 calls are much slower, because of no compile-time optimizations.
Since compiler doesn’t know about this dependency, it cannot track it, and tools like
xref cannot help either. At the same time, this provides higher level of decoupling between parts of the system. Thus, it’s not a way to structure your whole app, but it is useful for some subsystems inside a larger app.
Thanks for that.
Following those bullet points I posted above is this section which was feeding my confusion:
Calling and applying a fun does not involve any hash-table lookup. A fun contains an (indirect) pointer to the function that implements the fun.
apply/3 must look up the code for the function to execute in a hash table. It is therefore always slower than a direct call or a fun call.
It no longer matters (from a performance point of view) whether you write:
apply(Module, Function, [Arg1,Arg2])
The compiler internally rewrites the latter code into the former.
Not sure how calls works internally in
Erlang, but any atom (module names, function names etc.) is stored in atoms table:
An atom refers into an atom table, which also consumes memory. The atom text is stored once for each unique atom in this table. The atom table is not garbage-collected. By default, the maximum number of atoms is 1,048,576. This limit can be raised or lowered using the +t option.
Please look that you can use
:erlang atom for pattern-matching, but also using it you can call
:erlang module like:
If atom is written directly (i.e. except call like for example
Atom.to_string("runtime_atom")) the lookup happens on compile time.
Compiler in typical case works only on compile-time. Nothing stops you to compile/eval code, but generally all optimizations happen on compile time. As said
apply/3 happens on runtime.
Note: If the number of arguments are known at compile-time, the call is better written as Module:Function(Arg1, Arg2, …, ArgN).
If compiler would internally rewrite the code then tools like
xref would not have a problem. Please look that even
function are able to be passed dynamically which makes impossible for compiler to know what would be called and with what number of calls. This call is known once the code is evaluated i.e. on runtime and that’s why it’s slower than code which directly points to specific function.
I think the easiest way to understand it is:
- you export a function from a module to make it visible outside the module so it can be called externally
- you make an external call to a function using
mod:func(arg1, ...) which has been exported so it is visible outside its defining module
Note that if you make an external call to a function that function must be exported even if you are calling it from within the same module in which it is defined. So an external always goes “out” and then “in”. You typically don’t make external calls to functions in the same module as such calls can be affected by the code handling.