Is using apply a good way to approach dinamic calls?

TL;DR

Is there a more performant and clean way to call methods when I only know which methods (and modules) I will call after a response from the database


This question uses phoenix but is not about phoenix

I have a endpoint (lets call it legacy_reports) that return report files metadata to the user,like name, size, and download link.

My original problem was that when this endpoint was made, it was made for a specif report generator and to refactor everything in the back and in the front would take about 3 or 5 days. So I tweaked a bit the controller and the view of legacy_reports and added the new report to it.

To not suffer from the same problem in the future, I implemented a behaviour to extract the data from the report, and added the following code to the view.

def render("index.json", %{legacy_reports: leagacy_reports, new_reports: new_reports}) do
    %{
      legacy_reports:
        render_many(legacy_reports, LegacyReportView, "legacy_report.json", as: :legacy_report) ++
          render_many(new_reports, LegacyReportView, "new_report.json", as: :new_report)
    }
  end

  def render("show.json", %{legacy_report:legacy_report}) do
    %{legacy_report: render_one(legacy_report, LegacyReportView, "Legact_report.json")}
  end

  def render("legacy_report.json", %{new_report: new_report}) do

    module_atom =
      case new_report.service do
        "SomeService" -> :"Elixir.MyApp.Reports.SomeService"
        _ -> nil
      end

    %{
      some_key: apply(module_atom, :extract_some_key, [new_report]),
      ...
    }
  end

  def render("legacy_report.json", %{legacy_report: legacy_report}) do
    {start_date, end_date} = decode_report_info(legacy_report.report_info)

    %{
      some_key: legacy_report.some_key,
     ...
    }
  end

...

But since this a dynamic way to use apply, I’m afraid it will have some performance drawbacks. Is there a better way to do it without using a whole lot of cases?

Here, a quick benchmark that I used to test various forms of direct and indirect calls that I made in the past that I just ran again on the latest elixir/beam:

╰─➤  mix bench module_calls

Benchmarking Classifier: simple
===============================

Operating System: Linux"
CPU Information: AMD Phenom(tm) II X6 1090T Processor
Number of Available Cores: 6
Available memory: 15.67 GB
Elixir 1.8.1
Erlang 21.2.2

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 2 s
memory time: 2 s
parallel: 1
inputs: types
Estimated total run time: 42 s


Benchmarking Anon-Closure with input types...
Benchmarking Anon-Empty with input types...
Benchmarking Anon-Module with input types...
Benchmarking Direct with input types...
Benchmarking Direct-Apply with input types...
Benchmarking Indirect with input types...
Benchmarking Indirect-Apply with input types...

##### With input types #####
Name                     ips        average  deviation         median         99th %
Direct               23.04 M      0.0434 μs     ±7.60%      0.0420 μs      0.0620 μs
Direct-Apply         22.99 M      0.0435 μs     ±9.82%      0.0420 μs      0.0520 μs
Anon-Module          15.59 M      0.0641 μs   ±139.50%      0.0600 μs       0.100 μs
Anon-Closure         15.25 M      0.0656 μs   ±211.37%      0.0600 μs       0.100 μs
Anon-Empty           13.71 M      0.0729 μs     ±8.78%      0.0690 μs      0.0920 μs
Indirect-Apply       12.78 M      0.0783 μs     ±6.61%      0.0760 μs      0.0900 μs
Indirect             12.59 M      0.0794 μs     ±8.74%      0.0760 μs       0.100 μs

Comparison: 
Direct               23.04 M
Direct-Apply         22.99 M - 1.00x slower
Anon-Module          15.59 M - 1.48x slower
Anon-Closure         15.25 M - 1.51x slower
Anon-Empty           13.71 M - 1.68x slower
Indirect-Apply       12.78 M - 1.80x slower
Indirect             12.59 M - 1.83x slower

Memory usage statistics:

Name              Memory usage
Direct                   280 B
Direct-Apply             280 B - 1.00x memory usage
Anon-Module              280 B - 1.00x memory usage
Anon-Closure             280 B - 1.00x memory usage
Anon-Empty               280 B - 1.00x memory usage
Indirect-Apply           280 B - 1.00x memory usage
Indirect                 280 B - 1.00x memory usage

**All measurements for memory usage were the same**

A ‘direct’ call is when the module is known at compile-time, an indirect call is when the module is not known at compile-time, an anon-module is something like &Mod.blah/2, an anon-closure is like fn _ -> an_external_var end and an anon-empty is like fn _ -> nil end.

What this says is that both SomeMod.blah(1) and apply(SomeMod, :blah, [1]) have the same cost, because they actually do get lowered down to the same beam instructions. bound_mod.blah(1) and apply(bound_mod, :blah, [1]) are the same speed as well, and still faster than half the speed of a direct call. And anonymous calls are about the same as indirect calls too.

So 2 direct calls is barely slower than a single indirect call, so an indirect call you can consider is about equal to 2 direct calls, which are already way plenty fast. So it’s not something to worry about in other words, feel free to use apply. :slight_smile:

5 Likes

Damn, you rock. Thank you a lot.

1 Like

I do have a quick note about your code. Since :Elixir.MyApp.Reports.SomeService == MyApp.Reports.SomeService I would change:

    module_atom =
      case new_report.service do
        "SomeService" -> :"Elixir.MyApp.Reports.SomeService"
        _ -> nil
      end

to:

    module_atom =
      case new_report.service do
        "SomeService" -> MyApp.Reports.SomeService
        _ -> nil
      end

Actually I would probably extract that whole snippet to a new private function

3 Likes