henrique-marcomini-m
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?
Marked As Solved
OvermindDL1
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. ![]()
Also Liked
axelson
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
henrique-marcomini-m
Damn, you rock. Thank you a lot.








