In this case, they are different because in one you send 30k fewer AST nodes to the compiler, so there should be no surprises why one is much faster than the other and it should not be generalized as case
being faster than def
. For example, you could also have generated 10k defp
without the guards and a single def
that has a guard and calls it, and the result would be the same as case
, but using heads. So the result fully boils down to how you write the code and not the constructs involved, it is not about case
in particular being immune to guards.
Another way to put this is that the titles of your benchmarks should not be case
vs heads
, it is more like 1 guard
vs 10000 guards
. 
EDIT: furthermore, the only case where you can do the “guards optimization” above (unifying all guards into a single one) is when the guards themselves are redundant. This is because you are generating this code:
def foo(x = 1) when is_integer(x)
def foo(x = 2) when is_integer(x)
where you can extract the guards out, but arguably you should get rid of them altogether. They are only adding unnecessary compiler work.
Instead, when you are generating multiple clauses, it is often because the guards themselves are different, such as this:
def foo(x) when x === 1
def foo(x) when x === 2
And then you can’t eliminate them, neither in def
nor in case
. What could happen is that you can partially extract some guards, such as this:
def foo(x, y) when x === 1 and is_integer(y)
def foo(x, y) when x === 2 and is_integer(y)
Where you could move is_integer(y)
out and have the remaining in a case
(or a defp
as established above). But I can’t see where you would be able to extract all guards out, as in your example.