What is an "external function" in Erlang?

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 ( foo() , M.foo() ) are the fastest calls.
  • Calling or applying an anonymous function ( fun.() , apply(fun, []) ) is about three times as expensive as calling a local function.
  • Applying a public function from a module ( Mod.name() , apply(Mod, :name, []) ) is about twice as expensive as calling an anonymous function or about six times as expensive as calling a local function.
1 Like

An external function is a function defined in another module.

i.e. lists:map(Func, List)

Or in Elixir List.map(list, func) or :lists.map(func, list)

3 Likes

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

1 Like

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:

  1. Calls to local or EXTERNAL functions (foo(), m:foo()) are the fastest calls.
  2. 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 m:foo() and Mod:Name() that I don’t understand? I feel like I’m missing something simple… Can somebody explain it like I’m 5? :sweat_smile:

In elixir parlance we often say “remote call” or “remote function”.

1 Like

You look at it in bad way … if exported means any public function (i.e. def my_func(…)) then it applies as same to ExternalModule.my_func() as __MODULE__.my_func() (__MODULE__ is compile-time alias for current module).

Anyway your confusion is about those calls:

  1. my_func() - local function - looking only on such call we can’t say if it’s exported - could be, but don’t need to
  2. MyModule.my_func() - if function is called with module and code compiles then that means such function is exported
  3. 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. :slight_smile:

From this all apply/2 or 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.

Source:
https://medium.com/@lakret/elixirs-modularity-toolbox-398906988a60#37af

1 Like

Thanks for that.

Following those bullet points I posted above is this section which was feeding my confusion:

Notes and Implementation Details

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:

Module:Function(Arg1, Arg2)

or:

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.
Source: Erlang -- Advanced

Please look that you can use :erlang atom for pattern-matching, but also using it you can call :erlang module like:

:erlang.term_to_binary(term)

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.

From erlang documentation:

Note: If the number of arguments are known at compile-time, the call is better written as Module:Function(Arg1, Arg2, …, ArgN).
Source: Erlang -- erlang

If compiler would internally rewrite the code then tools like xref would not have a problem. Please look that even module and 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.

1 Like

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.

9 Likes