Skip to content

More function clauses for Range's Enumerable.member? implementation #12282

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

Closed
wants to merge 1 commit into from

Conversation

ryanwinchester
Copy link
Contributor

@ryanwinchester ryanwinchester commented Dec 4, 2022

Doing Advent of Code 2022, Day 4, I was using a value in range condition in my solution and then ran this benchmark for comparing the value against range.first and range.last instead, and the value in range version was around 4.6x slower for my usage (which has multiple range comparisons in a reduce).

I think if we make a special case for when step is +/-1 (which is probably the most common case), then we could see some improved performance.

These extra function matches should allow us to avoid

  • the Range.size/1 call (which does abs(div(last - first, step)) + 1), and
  • the rem(value - first, step) calls for our our +/-1 step ranges.

Thoughts?

@ryanwinchester ryanwinchester marked this pull request as ready for review December 4, 2022 08:12
@josevalim
Copy link
Member

josevalim commented Dec 4, 2022

Hi @ryanwinchester , thanks for the PR!

Membership is already optimized for ranges but you are likely paying the cost of protocol dispatch in your scripts/notebooks because protocols there are not consolidated. Can you please share your solution and let us know if adding Mix.install/2 to the top speeds it up? (You can do Mix.install([])).

@ryanwinchester
Copy link
Contributor Author

ryanwinchester commented Dec 4, 2022

Can you please share your solution and let us know if adding Mix.install/2 to the top speeds it up? (You can do Mix.install([])).

https://github.com/ryanwinchester/advent-of-code-elixir/blob/master/bench/day04.exs

I'm using Benchee with mix run so Mix.install/2 doesn't work

Update: I added Mix.install([{:benchee, "~> 1.0"}]) and ran it with elixir {filename} with same results

@josevalim
Copy link
Member

josevalim commented Dec 4, 2022

Thanks for sharing, that helps clarify a few things. My first reaction is that the algorithms you linked are different, so I would expect the performance to be different.

Can you try this?

defmodule Example do
  def member?(first..last//1, value) when first <= last and is_integer(value) do
    {:ok, first <= value and value <= last}
  end

  def member?(first..last//-1, value) when last <= first and is_integer(value) do
    {:ok, last <= value and value <= first}
  end

  def member?(first..last//1, _) when first > last, do: {:ok, false}
  def member?(first..last//-1, _) when first < last, do: {:ok, false}
end

And using it in your benchmarks to see how much faster/slower it becomes?

PS: to clarify, they are different because the first version is doing at least the double of comparisons, regardless of the implementation of member?.

@ryanwinchester
Copy link
Contributor Author

ryanwinchester commented Dec 4, 2022

PS: to clarify, they are different because the first version is doing at least the double of comparisons, regardless of the implementation of member?.

Ah yeah. You have a point. I want to know if either range completely contains the other, but using in twice for each check is not an ideal way to do that. I'll modify the comparison and run the benchmarks anyway, though...

@ryanwinchester
Copy link
Contributor Author

ryanwinchester commented Dec 4, 2022

@josevalim updated benchmark to be more "fair" and used the Example module

❯ elixir bench/extras/range.member.exs
Operating System: macOS
CPU Information: Intel(R) Core(TM) i5-9600K CPU @ 3.70GHz
Number of Available Cores: 6
Available memory: 32 GB
Elixir 1.14.2
Erlang 25.1.2

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 5 s
memory time: 0 ns
reduction time: 0 ns
parallel: 1
inputs: none specified
Estimated total run time: 28 s

Benchmarking Enumerable ...
Benchmarking NewMember ...
Benchmarking OriginalMember ...
Benchmarking in ...

Name                     ips        average  deviation         median         99th %
NewMember            1107.57        0.90 ms     ±5.79%        0.89 ms        1.08 ms
OriginalMember        659.54        1.52 ms     ±4.27%        1.50 ms        1.70 ms
in                    525.07        1.90 ms     ±4.07%        1.89 ms        2.11 ms
Enumerable            492.61        2.03 ms    ±17.31%        2.01 ms        2.50 ms

Comparison:
NewMember            1107.57
OriginalMember        659.54 - 1.68x slower +0.61 ms
in                    525.07 - 2.11x slower +1.00 ms
Enumerable            492.61 - 2.25x slower +1.13 ms

Also added more comparisons, including one without the protocol dispatch.

Link: https://github.com/ryanwinchester/advent-of-code-elixir/blob/master/bench/extras/range.member.exs

@josevalim
Copy link
Member

So it seems the issue is that the current member implementation is slow. We should optimize it instead. :)

@ryanwinchester
Copy link
Contributor Author

@josevalim looks good

❯ elixir bench/extras/range.member.exs
Operating System: macOS
CPU Information: Intel(R) Core(TM) i5-9600K CPU @ 3.70GHz
Number of Available Cores: 6
Available memory: 32 GB
Elixir 1.14.2
Erlang 25.1.2

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 5 s
memory time: 0 ns
reduction time: 0 ns
parallel: 1
inputs: none specified
Estimated total run time: 35 s

Benchmarking Enumerable ...
Benchmarking NewMember ...
Benchmarking OptimizedMember ...
Benchmarking OriginalMember ...
Benchmarking in ...

Name                      ips        average  deviation         median         99th %
NewMember             1124.24        0.89 ms     ±6.55%        0.88 ms        1.04 ms
OptimizedMember       1051.91        0.95 ms     ±5.10%        0.94 ms        1.10 ms
OriginalMember         674.34        1.48 ms     ±3.10%        1.48 ms        1.62 ms
in                     525.43        1.90 ms     ±3.17%        1.90 ms        2.06 ms
Enumerable             512.84        1.95 ms     ±3.08%        1.94 ms        2.11 ms

Comparison:
NewMember             1124.24
OptimizedMember       1051.91 - 1.07x slower +0.0612 ms
OriginalMember         674.34 - 1.67x slower +0.59 ms
in                     525.43 - 2.14x slower +1.01 ms
Enumerable             512.84 - 2.19x slower +1.06 ms

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants