Maps performance question - test[:foo] vs test |> Map.has_key?(:foo)

Morning! We noticed that if you got a map and you try to use a non-existing key, you will receive an:

** (KeyError) key :foo not found

So we can use this:

test[:foo] which returns nil

or this:

test |> Map.has_key?(:foo)

The question is:
Is it the same performance on runtime? There is some way to measure that?

Thanks!

1 Like

Good question!

In these situations I like to turn to Benchee, it’s a great tool for answering these questions.

4 Likes

just a note (i haven’t done the benchmark)

test[:foo] and Map.has_key?(map, :foo) are not used for the same reason in my opinion.

In the first case, you also need to check if test[:foo] is nil or not in order to determine if the map has the key
I would benchmark Map.get/2 instead of Map.has_key?/2 if i wanted to compare with test[:foo]

1 Like

Nitpick: these aren’t quite the same thing if your input map can have nils as values:

test = %{foo: nil}

test[:foo] # => nil

Map.has_key?(test, :foo) # => true
3 Likes

You are right, but this real case is a parameter received on a component, so if the key is not present I set a default, and so on. I won’t ever pass the key => nil parameter to this component.

So, I’ve tested these:

case(testmap[:foo2]) do nil -> IO.puts("Key does not exists") end

VS

case(testmap |> Map.has_key?(testmap)) do false -> IO.puts("Key does not exists") end

Results:

using Map.has_key       89.90 K       11.12 μs   ±125.00%       10.04 μs       19.79 μs
using []                86.37 K       11.58 μs   ±125.56%       10.45 μs       20.64 μs

Comparison: 
using Map.has_key       89.90 K
using []                86.37 K - 1.04x slower +0.45 μs

Thanks for this tool @Ipil, maybe in this case can sound exaggerated but could be very useful in other situations.

2 Likes

You could benchmark against pattern matching too.

1 Like

I think you’re talking about the assigns, if so this is exactly what assign_new/3 is for :slightly_smiling_face:

Also while we are benchmarking (which I love), I think it’s worth pointing out that you do not need to IO.puts anything in your benchmarks. You don’t really need the case either, you’re just testing map[:foo] vs. Map.has_key?(:foo), otherwise the tests are equivalent so you can strip out the rest and just test the functions you care about, e.g. fn -> map[:foo] end. On my machine the code runs >50 times faster after stripping out the IO.

And this code testmap |> Map.has_key?(testmap) is equivalent to Map.has_key?(testmap, testmap) (checking if the map has itself as a key) and will really skew your tests against testmap[:foo] as the size of your test map grows. But it’s a good philosophical question, can a map have itself as a key? Maybe the epitome of recursion if it’s an infinitely nested map.

Finally in my tests Map.get/2 beat both of these comfortably so you may consider adding that to your benchmarks. Happy benchmarking!

2 Likes

Sorry about the delay. I made some time to test the pattern matching

image

Pattern Matching sounds good :wink: