Odd Compilation Performance

Hey folks!

We’re in the midst of re-working the Absinthe schema internals, and are running into some odd compiler performance stuff. Consider the following code:

defmodule Foo do
  defmacro foo() do
    quote do
      @foo "asdf"
      # @bar @foo
    end
  end
end

defmodule Bar do
  require Foo
  Foo.foo()
  Foo.foo()
  # ... 1000 lines of Foo.foo()
end

Why do we want 1000 Foo.foo()s? Schemas can get very large, and this shows the problem in the simplest way. In any case, if you compile this it takes ~0.1 seconds. If you uncomment the # @bar @foo line it jumps to 1 second. Here’s a table up to 8 total @bar @foo lines:

0	0.1
1	1
2	2.5
3	6.3
4	11.5
5	18.2
6	22
7	30
8	37

It’s vaguely exponential, and rather inconvenient from a compile time perspective. It isn’t simply a matter of having a lot of lines, cause if you change the @bar @foo line to the @bar :bar then the compile time returns to a more normal 0.7 seconds.

Thoughts?

3 Likes

Is the behaviour the same when you do not wrap the attributes in a macro?

And is this for “normal attributes” or are they registered as cumulative?

Which versions of elixir do you see this odd behaviour in?

Hey @NobbZ

The behaviour is the same if I replace the 1000 Foo.foo() calls with:

  @foo "asdf"
  @bar @foo
  @bar @foo
  @bar @foo
  @bar @foo

# above 5 lines repeated 1000 times

The macro just makes it a lot easier to adjust what gets repeated.

They are entirely normal attributes, the code I have here is exactly and exhaustively what I’m running. Elixir versions 1.4.5, 1.5.2, and 1.6.0 all show the same characteristics. I didn’t test earlier than 1.4, but anecdotally we ran into similar issues back in early versions of Absinthe that would have been in around Elixir ~1.2.

I’d imagine the cost is in the environment lookup to inline @foo from just a cursory guess. I’d probably check how Elixir handles that in this case? It might be optimizeable

Ultimately though the @foo is almost a red herring. If you change the @bar to something else like @bar Application.get_env(:absinthe, :foo) it’s also super slow in an exponential fashion.

Something about running functions in a module body is 1) very slow and 2) slows down in a non linear fashion WRT the size of the module body. Does defmodule do some N^2 walk of the module body AST or something?

1 Like

I’ve further simplified some of the examples.

1 Like