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.
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.
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".
@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".