From b79608b960cb48c446bf8fb0d0656880e4e77ce6 Mon Sep 17 00:00:00 2001 From: Austin Ziegler Date: Wed, 8 Nov 2023 22:36:23 -0500 Subject: [PATCH] Omit unnecessary function captures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AS all of the conversion modules implement `convert/1`, it is unnecessary to perform a function capture. Executing a captured function, even a static capture is slower. I ran three slightly different tests with benchee. In all cases, there was a module that looked like this: ```elixir defmodule Mod do def target, do: nil def target(input), do: input def run_capture, do: exec_capture(&__MODULE__.target/0) def run_capture(input), do: exec_capture(input, &__MODULE__.target/0) def run_module, do: exec_module(__MODULE__) def run_module(input), do: exec_module(input, __MODULE__) end ``` 1. A list of 10,000 numbers: ```elixir list = Enum.to_list(1..10_0000) Benchee.run( %{ "cpature" => fn -> Mod.run_capture(list) end, "module" => fn -> Mod.run_module(list) end }, time: 10, memory_time: 2 ) ``` ```console $ elixir capturevsmodule.exs … Name ips average deviation median 99th % module 30.20 M 33.11 ns ±138.67% 42 ns 42 ns capture 25.07 M 39.88 ns ±61177.08% 42 ns 42 ns Comparison: module 30.20 M capture 25.07 M - 1.20x slower +6.77 ns … ``` 2. A short word: ```elixir word = "helloWorld" Benchee.run( %{ "cpature" => fn -> Mod.run_capture(word) end, "module" => fn -> Mod.run_module(word) end }, time: 10, memory_time: 2 ) ``` ```console $ elixir capturevsmodule.exs … Name ips average deviation median 99th % module 30.97 M 32.29 ns ±147.88% 42 ns 42 ns capture 28.35 M 35.27 ns ±34598.68% 42 ns 42 ns Comparison: module 30.97 M capture 28.35 M - 1.09x slower +2.98 ns … ``` 3: No input (just dispatch overhead): ```elixir Benchee.run( %{ "cpature" => fn -> Mod.run_capture() end, "module" => fn -> Mod.run_module() end }, time: 10, memory_time: 2 ) ``` ```console $ elixir capturevsmodule.exs … Name ips average deviation median 99th % module 30.13 M 33.19 ns ±136.81% 42 ns 42 ns capture 27.46 M 36.42 ns ±37965.35% 42 ns 42 ns Comparison: module 30.13 M capture 27.46 M - 1.10x slower +3.23 ns … ``` This will matter much more on `convert_nested` calls than on `convert_plain` calls, but either way, I see a 9–20% boost as an absolute win. --- lib/cozy_case.ex | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/lib/cozy_case.ex b/lib/cozy_case.ex index 900458d..41175f4 100644 --- a/lib/cozy_case.ex +++ b/lib/cozy_case.ex @@ -139,59 +139,55 @@ defmodule CozyCase do Converts other supported cases to snake case. """ @spec snake_case(accepted_data_types()) :: String.t() - def snake_case(term) when is_binary(term) or is_atom(term), do: convert_plain(term, &SnakeCase.convert/1) - def snake_case(term) when is_map(term) or is_list(term), do: convert_nest(term, &SnakeCase.convert/1) + def snake_case(term) when is_binary(term) or is_atom(term), do: convert_plain(term, SnakeCase) + def snake_case(term) when is_map(term) or is_list(term), do: convert_nest(term, SnakeCase) @doc """ Converts other supported cases to kebab case. """ @spec kebab_case(accepted_data_types()) :: String.t() - def kebab_case(term) when is_binary(term) or is_atom(term), do: convert_plain(term, &KebabCase.convert/1) - def kebab_case(term) when is_map(term) or is_list(term), do: convert_nest(term, &KebabCase.convert/1) + def kebab_case(term) when is_binary(term) or is_atom(term), do: convert_plain(term, KebabCase) + def kebab_case(term) when is_map(term) or is_list(term), do: convert_nest(term, KebabCase) @doc """ Converts other supported cases to camel case. """ @spec camel_case(accepted_data_types()) :: String.t() - def camel_case(term) when is_binary(term) or is_atom(term), do: convert_plain(term, &CamelCase.convert/1) - def camel_case(term) when is_map(term) or is_list(term), do: convert_nest(term, &CamelCase.convert/1) + def camel_case(term) when is_binary(term) or is_atom(term), do: convert_plain(term, CamelCase) + def camel_case(term) when is_map(term) or is_list(term), do: convert_nest(term, CamelCase) @doc """ Converts other supported cases to pascal case. """ @spec pascal_case(accepted_data_types()) :: String.t() - def pascal_case(term) when is_binary(term) or is_atom(term), do: convert_plain(term, &PascalCase.convert/1) - def pascal_case(term) when is_map(term) or is_list(term), do: convert_nest(term, &PascalCase.convert/1) + def pascal_case(term) when is_binary(term) or is_atom(term), do: convert_plain(term, PascalCase) + def pascal_case(term) when is_map(term) or is_list(term), do: convert_nest(term, PascalCase) - defp convert_plain(string, fun) when is_binary(string) do - fun.(string) - end + defp convert_plain(string, module) when is_binary(string), do: module.convert(string) - defp convert_plain(atom, fun) when is_atom(atom) do + defp convert_plain(atom, module) when is_atom(atom) do Atom.to_string(atom) |> case do "Elixir." <> rest -> rest string -> string end - |> fun.() + |> module.convert() end - defp convert_plain(any, _fun), do: any + defp convert_plain(any, _module), do: any - defp convert_nest(map, fun) when is_map(map) do + defp convert_nest(map, module) when is_map(map) do try do for {k, v} <- map, into: %{}, - do: {convert_plain(k, fun), convert_nest(v, fun)} + do: {convert_plain(k, module), convert_nest(v, module)} rescue # not Enumerable Protocol.UndefinedError -> map end end - defp convert_nest(list, fun) when is_list(list) do - Enum.map(list, &convert_nest(&1, fun)) - end + defp convert_nest(list, module) when is_list(list), do: Enum.map(list, &convert_nest(&1, module)) - defp convert_nest(any, _fun), do: any + defp convert_nest(any, _module), do: any end