Upgrading elixir/erlang from 1.14.3/24.3.2.8 to 1.16.2/26.2.5 resulted in 5x performance degradation

Overall, the performance in multiple places in the project I am working on has improved. However, there is one module that does a few operations that seems to - although it
doesn’t seem to - get slower between versions, actually experiencing 4-5x degradation (from 50-60 microseconds to 300 microseconds).

The example snippet of the code looks like


defmodule State do
  use Agent

  def start_link(opts \\ []) do
    state = %{
      id_availability: Keyword.get(opts, :available, true),
    }

    Agent.start_link(fn -> state end, name: Keyword.get(opts, :name, __MODULE__))
  end

  @spec available?() :: boolean()
  def available?(agent \\ __MODULE__), do: get_opts(agent)[:id_availability]

  @spec get_opts() :: map()
  def get_opts(agent \\ __MODULE__) do
    Agent.get(agent, & &1)
  end

  @spec set_opts(map()) :: :ok
  def set_opts(agent \\ __MODULE__, opts) when is_map(opts) do
    Agent.update(agent, fn _state -> opts end)
  end
end


defmodule A do
  def f1(struc, allowlist) do
    if needs_extra_elements?(struc) do
      %{
        lista: struc.lista ++ ["a"],
        listb: struc.listb ++ ["b", "c"],
        keys: Map.take(struc.keys, allowlist)
      }
    else
      struc
    end
  end

  def needs_extra_elements?(struc) do
    struc.has_lists? and String.ends_with?(struc.special_key, "aabbcc") and State.available?()
  end
end

I’m not sure how this could result in 5x performance degradation. Any help would be greatly appreciated.

1 Like

I cannot tell much about the performance degradation, but this isn’t a great way to use an agent. You should filter down the data within the agent callback and only return what is needed, which is then sent/copied to the caller. Currently you copy the whole agent state to the caller, to then there select a subset of the whole. In your example that likely doesn’t make a difference, but if your state is more involved in practise it might.

3 Likes

Have you factually established that it’s exactly calling these functions that got slower with time?

Not yet, I did the change that @LostKobrakai suggested, because it seems an improvement, but definitely doesn’t explain the problem.
However I notice that there is and call to

Plug.Conn.Cookies.decode(cookie)

which I am going to benchmark between the versions.

Actually after benchmark I can see
On the old elixir/erlang

Name                                      ips        average  deviation         median         99th %
Plug.Conn.Cookies.decode/1       12.54 K       79.74 μs    ±11.94%       75.71 μs      106.92 μs

Memory usage statistics:
Name                               Memory usage
Plug.Conn.Cookies.decode/1       134.95 KB

The new elixir/erlang

Name                                      ips        average  deviation         median         99th %
Plug.Conn.Cookies.decode/1        3.59 K      278.66 μs    ±10.54%      277.71 μs      360.84 μs

Memory usage statistics:
Name                               Memory usage
Plug.Conn.Cookies.decode/1       264.63 KB

It seems the problem is in Plug.Conn.Cookies.decode/1 with the same payload it gets 4x slower on the new version of elixir/erlang.

We are using :plug, "1.16.0"; :plug_cowboy, "2.6.1"; :plug_crypto, "2.1.0".

1 Like

@josevalim just released fix for that in plug, "1.16.1" (more info GH-issue). The newest version is 3x faster than 1.14.3/24.3.2.8 with plug, "1.16.0".

6 Likes