Skip to content

Commit

Permalink
Omit unnecessary function captures
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
halostatue committed Nov 9, 2023
1 parent c7150f3 commit b79608b
Showing 1 changed file with 16 additions and 20 deletions.
36 changes: 16 additions & 20 deletions lib/cozy_case.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit b79608b

Please sign in to comment.