Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Omit unnecessary function captures #1

Merged
merged 1 commit into from
Nov 17, 2023

Conversation

halostatue
Copy link
Contributor

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:

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:

    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
    )
    $ 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:

    word = "helloWorld"
    
    Benchee.run(
     %{
       "cpature" => fn -> Mod.run_capture(word) end,
       "module" => fn -> Mod.run_module(word) end
     },
     time: 10,
     memory_time: 2
    )
    $ 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):

Benchee.run(
 %{
   "cpature" => fn -> Mod.run_capture() end,
   "module" => fn -> Mod.run_module() end
 },
 time: 10,
 memory_time: 2
)
$ 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.

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.
@c4710n c4710n merged commit b79608b into cozy-elixir:main Nov 17, 2023
8 checks passed
@c4710n
Copy link
Member

c4710n commented Nov 17, 2023

Thanks. ❤️

@halostatue halostatue deleted the omit-function-captures branch November 17, 2023 15:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants