mruoss

mruoss

Guards vs. generated functions: which is faster/more performant?

Hey there

Regarding performance, is there a difference between guards and generated functions using pattern matching? Or does the compiler generate the same byte code for both?

And if there is a difference, which one is more performant?

Using Guards

defmodule UsingGuards do
  def do_work(arg) when arg in big_list do
    # ...
  end
end

Generated Functions

defmodule UsingFunctionGeneration do
  for arg in big_list do
    def do_work(unquote(arg)) do
      # ...
    end
  end
end

Most Liked

mruoss

mruoss

So I wrote the following simple Elixir script that uses benchee. I did not give it much thought to be honest. Let me know what you think.

TLDR; Performance for both approaches indeed seem to be equivalent.

Script

defmodule Test do
  @numbers Range.to_list(1..1000)

  def work(input, fun) do
    Enum.map(input, fun)
  end

  for number <- @numbers do
    def do_work_generated(unquote(number)), do: unquote(number)
  end

  def do_work_guards(number) when number in @numbers, do: number
end

Mix.install([:benchee, :benchee_markdown])
list_of_random_numbers = fn -> Enum.map(1..500_000, fn _ -> :rand.uniform(1000) end) end

inputs =
  list_of_random_numbers
  |> Stream.repeatedly()
  |> Stream.take(5)
  |> Stream.with_index()
  |> Stream.map(fn {numbers, idx} -> {"Batch #{idx + 1}", numbers} end)

Benchee.run(
  %{
    "Generated Functions" => fn input -> Test.work(input, &Test.do_work_generated/1) end,
    "Guards" => fn input -> Test.work(input, &Test.do_work_guards/1) end
  },
  memory_time: 60,
  inputs: inputs,
  formatters: [
    {Benchee.Formatters.Markdown, file: "BENCHMARK.md"},
    Benchee.Formatters.Console
  ]
)

Output

Configuration

Benchmark suite executing with the following configuration:

:time 5 s
:parallel 1
:warmup 2 s

Statistics

Input: Batch 1

Run Time

Name IPS Average Devitation Median 99th %
Guards 177.07 5.65 ms ±4.15% 5.62 ms 5.87 ms
Generated Functions 175.18 5.71 ms ±4.30% 5.67 ms 6.30 ms

Run Time Comparison

Name IPS Slower
Guards 177.07  
Generated Functions 175.18 1.01x

Memory Usage

Name Average Factor
Guards 7.63 MB  
Generated Functions 7.63 MB 1.0x

Input: Batch 2

Run Time

Name IPS Average Devitation Median 99th %
Guards 174.66 5.73 ms ±4.56% 5.69 ms 6.14 ms
Generated Functions 174.12 5.74 ms ±4.46% 5.71 ms 6.19 ms

Run Time Comparison

Name IPS Slower
Guards 174.66  
Generated Functions 174.12 1.0x

Memory Usage

Name Average Factor
Guards 7.63 MB  
Generated Functions 7.63 MB 1.0x

Input: Batch 3

Run Time

Name IPS Average Devitation Median 99th %
Guards 174.93 5.72 ms ±4.77% 5.68 ms 6.23 ms
Generated Functions 174.21 5.74 ms ±4.07% 5.71 ms 5.96 ms

Run Time Comparison

Name IPS Slower
Guards 174.93  
Generated Functions 174.21 1.0x

Memory Usage

Name Average Factor
Guards 7.63 MB  
Generated Functions 7.63 MB 1.0x

Input: Batch 4

Run Time

Name IPS Average Devitation Median 99th %
Guards 174.18 5.74 ms ±4.48% 5.71 ms 6.16 ms
Generated Functions 172.05 5.81 ms ±4.21% 5.79 ms 6.14 ms

Run Time Comparison

Name IPS Slower
Guards 174.18  
Generated Functions 172.05 1.01x

Memory Usage

Name Average Factor
Guards 7.63 MB  
Generated Functions 7.63 MB 1.0x

Input: Batch 5

Run Time

Name IPS Average Devitation Median 99th %
Generated Functions 174.62 5.73 ms ±4.37% 5.69 ms 6.19 ms
Guards 174.34 5.74 ms ±4.52% 5.71 ms 6.10 ms

Run Time Comparison

Name IPS Slower
Generated Functions 174.62  
Guards 174.34 1.0x

Memory Usage

Name Average Factor
Generated Functions 7.63 MB  
Guards 7.63 MB 1.0x
LostKobrakai

LostKobrakai

in/2 doesn’t compile to an Enum.member?/2 call when used in a guard:

https://hexdocs.pm/elixir/Kernel.html#in/2-guards

LostKobrakai

LostKobrakai

You can use GitHub - michalmuskala/decompile to take a look how things will look at lower levels.

hst337

hst337

This exact test case has nothing to do for “guards vs generated functions”

when x in 1..100 gets compiled into when x > 1 and x < 100, so it is just faster.

So, to answer the original question “Performance Question: guards vs. generated functions: which is faster?”: none is faster, both are a different tools used for different tasks. The task you’ve brought up in the original post is made up and is actually solved not by x in y guard or generated clauses, but just by x >= left and x <= right

mruoss

mruoss

Yeah it looks like the compiler is able to optimize better if facts are known at compile time.

But here’s something interesting: I have adapted the code a bit because eventually, I’m gonna be working with strings. And if we add binary pattern matching to the mix, performance seems to be different.

Side note: Reading the documentation of Kernel.in/2 I read the following:

However, this construct will be inefficient for large lists. In such cases, it is best to stop using guards and use a more appropriate data structure, such as MapSet.

I therefore added a test using MapSets (which, afaict, is equivalent to @al2o3cr’s test using is_map_key).

TLDR;

In this case, guards seem to make the race, being twice as fast as generated functions and 2.5x faster than MapSets

Test

defmodule Test do
  @chars Range.to_list(1000..1)
  @chars_map_set MapSet.new(1000..1)

  def work(input, fun) do
    Enum.map(input, fun)
  end

  for char <- @chars do
    def do_work_generated(<<unquote(char)::utf8>>), do: unquote(char)

    def do_work_generated(<<unquote(char)::utf8, rest::binary>>) do
      do_work_generated(rest)
    end
  end

  def do_work_guards(<<char::utf8>>) when char in @chars, do: char

  def do_work_guards(<<char::utf8, rest::binary>>) when char in @chars do
    do_work_guards(rest)
  end

  def do_work_mapset(<<char::utf8>>) do
    if MapSet.member?(@chars_map_set, char) do
      char
    else
      :error
    end
  end

  def do_work_mapset(<<char::utf8, rest::binary>>) do
    if MapSet.member?(@chars_map_set, char) do
      do_work_mapset(rest)
    else
      :error
    end
  end
end

Mix.install([:benchee, :benchee_markdown])

list_of_random_strings = fn ->
  Enum.map(1..500_000, fn _ ->
    for _ <- 1..10,
        into: "",
        do: <<Enum.random(Range.to_list(?0..?9) ++ Range.to_list(?A..?z))>>
  end)
end

inputs =
  list_of_random_strings
  |> Stream.repeatedly()
  |> Stream.take(5)
  |> Stream.with_index()
  |> Stream.map(fn {strings, idx} -> {"Batch #{idx + 1}", strings} end)

Benchee.run(
  %{
    "Generated Functions" => fn input -> Test.work(input, &Test.do_work_generated/1) end,
    "Guards" => fn input -> Test.work(input, &Test.do_work_guards/1) end,
    "MapSet" => fn input -> Test.work(input, &Test.do_work_mapset/1) end
  },
  memory_time: 60,
  inputs: inputs,
  parallel: 10,
  formatters: [
    {Benchee.Formatters.Markdown, file: "BENCHMARK_STRINGS.md"},
    Benchee.Formatters.Console
  ]
)

Output

Statistics

Statistics

Input: Batch 1

Run Time

Name IPS Average Devitation Median 99th %
Guards 31.07 32.19 ms ±8.67% 32.09 ms 46.84 ms
Generated Functions 15.54 64.36 ms ±5.44% 63.96 ms 90.67 ms
MapSet 10.99 91.02 ms ±5.33% 90.73 ms 123.52 ms

Run Time Comparison

Name IPS Slower
Guards 31.07  
Generated Functions 15.54 2.0x
MapSet 10.99 2.83x

Memory Usage

Name Average Factor
Guards 26.70 MB  
Generated Functions 26.70 MB 1.0x
MapSet 26.70 MB 1.0x

Input: Batch 2

Run Time

Name IPS Average Devitation Median 99th %
Guards 31.57 31.68 ms ±8.91% 31.97 ms 45.93 ms
Generated Functions 15.64 63.94 ms ±5.55% 64.00 ms 91.14 ms
MapSet 11.10 90.06 ms ±4.90% 90.00 ms 119.82 ms

Run Time Comparison

Name IPS Slower
Guards 31.57  
Generated Functions 15.64 2.02x
MapSet 11.10 2.84x

Memory Usage

Name Average Factor
Guards 26.70 MB  
Generated Functions 26.70 MB 1.0x
MapSet 26.70 MB 1.0x

Input: Batch 3

Run Time

Name IPS Average Devitation Median 99th %
Guards 31.43 31.81 ms ±8.78% 31.89 ms 46.68 ms
Generated Functions 15.40 64.95 ms ±5.93% 64.84 ms 93.51 ms
MapSet 11.04 90.54 ms ±4.83% 90.49 ms 118.91 ms

Run Time Comparison

Name IPS Slower
Guards 31.43  
Generated Functions 15.40 2.04x
MapSet 11.04 2.85x

Memory Usage

Name Average Factor
Guards 26.70 MB  
Generated Functions 26.70 MB 1.0x
MapSet 26.70 MB 1.0x

Input: Batch 4

Run Time

Name IPS Average Devitation Median 99th %
Guards 31.08 32.18 ms ±8.48% 32.34 ms 46.62 ms
Generated Functions 15.25 65.58 ms ±6.42% 65.17 ms 97.31 ms
MapSet 12.31 81.22 ms ±7.78% 79.51 ms 108.75 ms

Run Time Comparison

Name IPS Slower
Guards 31.08  
Generated Functions 15.25 2.04x
MapSet 12.31 2.52x

Memory Usage

Name Average Factor
Guards 26.70 MB  
Generated Functions 26.70 MB 1.0x
MapSet 26.70 MB 1.0x

Input: Batch 5

Run Time

Name IPS Average Devitation Median 99th %
Guards 29.61 33.78 ms ±8.29% 33.12 ms 50.84 ms
Generated Functions 15.13 66.08 ms ±5.99% 65.88 ms 97.77 ms
MapSet 11.20 89.32 ms ±53.75% 82.30 ms 440.40 ms

Run Time Comparison

Name IPS Slower
Guards 29.61  
Generated Functions 15.13 1.96x
MapSet 11.20 2.64x

Memory Usage

Name Average Factor
Guards 26.70 MB  
Generated Functions 26.70 MB 1.0x
MapSet 26.70 MB 1.0x

Where Next?

Popular in Questions Top

aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
pgiesin
This should be a simple problem but I just can’t seem to figure it out. I have a standalone Elixir app that won’t find the database. Dep...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
Phillipp
Hey, I have a NanoPi-M3 and try to install Elixir on their Ubuntu image. I followed the Raspberry Pi installation instructions from the ...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
sabri
Can someone explain the settings of pool_size of Ecto in config file? and what is the recommend size? Thanks
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
idi527
I’ve been re-reading swift book again and noticed that multiline strings there don’t have a trailing line break, unlike in elixir iex(2)...
New

Other popular topics Top

dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

We're in Beta

About us Mission Statement