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

Initial implementation of EEP 50 #2864

Merged
merged 1 commit into from Dec 7, 2020
Merged

Conversation

josevalim
Copy link
Contributor

@josevalim josevalim commented Nov 12, 2020

This pull request adds support for maps in the sets module. According to my benchmarks, using maps is faster in the majority of cases, sometimes by multiple orders of magnitude, and in the few cases it is slower, it is only by 1.1x-1.4x, which can be improved with the steps outlined below. Note that, although we are using maps, the type is still opaque.


Fully adopting EEP 50 will require multiple PRs. In particular, following PRs should:

  1. Remove cerl_sets

  2. Incorporate the intersection implementation in Add maps:merge_with/3, maps:intersect/2, and maps:intersect_with/2 #2797

  3. Write intersection, subtract, is_subset, is_disjoint, and from_list as NIFs so sets becomes the fastest set implementation in OTP on all possible scenarios

lib/stdlib/src/sets.erl Outdated Show resolved Hide resolved
@josevalim
Copy link
Contributor Author

I went ahead and tackled the pending issues. In particular, I chose for sets:new/0 and sets:from_list/1 to return maps but I can revert that if deemed too forward.

@IngelaAndin IngelaAndin added the team:VM Assigned to OTP team VM label Nov 16, 2020
lib/stdlib/src/sets.erl Outdated Show resolved Hide resolved
@josevalim
Copy link
Contributor Author

@jhogberg I have updated the PR to support a version flag. Docs have been updated too.

I have also benchmarked maps:intersect/2 and it is slower then the current cerl_sets implementation. You can see the benchmark here and the benchmark results here. For this reason, I decided to keep the cerl_sets implementation for intersection for now. If you want to give it a try yourself, you can clone the repo, run mix deps.get and then mix run bench/intersection.exs.

@jhogberg
Copy link
Contributor

jhogberg commented Nov 30, 2020

@jhogberg I have updated the PR to support a version flag. Docs have been updated too.

Great :)

I have also benchmarked maps:intersect/2 and it is slower then the current cerl_sets implementation. You can see the benchmark here and the benchmark results here. For this reason, I decided to keep the cerl_sets implementation for intersection for now. If you want to give it a try yourself, you can clone the repo, run mix deps.get and then mix run bench/intersection.exs.

The current implementation of maps:intersect/2 uses maps:intersect_with/3 which turned out to be more expensive than I thought. A quick-and-dirty specialization of maps:intersect/2 makes it much faster than the alternatives:

-spec intersect(Map1,Map2) -> Map3 when
    Map1 :: #{ Key => term() },
    Map2 :: #{ term() => Value2 },
    Map3 :: #{ Key => Value2 }.

intersect(Map1, Map2) when is_map(Map1), is_map(Map2) ->
    case map_size(Map1) < map_size(Map2) of
        true ->
            Iterator = maps:iterator(Map1),
            intersect_1(maps:next(Iterator), Map1, Map2);
        false ->
            Iterator = maps:iterator(Map2),
            intersect_2(maps:next(Iterator), Map2, Map1)
    end;
intersect(Map1, Map2) ->
    erlang:error(error_type_two_maps(Map1, Map2),
                 [Map1, Map2]).

intersect_1({Key, LHS_Value, Iterator}, LHS0, RHS) ->
    LHS = case RHS of
              #{ Key := LHS_Value } ->
                  LHS0;
              #{ Key := RHS_Value } ->
                  LHS0#{ Key := RHS_Value };
              _ ->
                  maps:remove(Key, LHS0)
          end,
    intersect_1(maps:next(Iterator), LHS, RHS);
intersect_1(none, Res, _) ->
    Res.

intersect_2({Key, _RHS_Value, Iterator}, RHS0, LHS) ->
    RHS = case LHS of
              #{ Key := _LHS_Value } ->
                  %% The value in RHS has precedence, so it doesn't need to
                  %% be updated even if they differ.
                  RHS0;
              _ ->
                  maps:remove(Key, RHS0)
          end,
    intersect_2(maps:next(Iterator), RHS, LHS);
intersect_2(none, Res, _) ->
    Res.
$ mix run bench/intersection.exs
Error trying to dermine erlang version enoent
Operating System: Linux
CPU Information: Intel(R) Core(TM) i7-9700K CPU @ 3.60GHz
Number of Available Cores: 8
Available memory: 31.17 GB
Elixir 1.12.0-dev
Erlang ok

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 5 s
memory time: 0 ns
parallel: 1
inputs: large eq bin, large eq int, medium eq bin, medium eq int, small eq bin, small eq int
Estimated total run time: 2.10 min

Benchmarking cerl_sets with input large eq bin...
Benchmarking cerl_sets with input large eq int...
Benchmarking cerl_sets with input medium eq bin...
Benchmarking cerl_sets with input medium eq int...
Benchmarking cerl_sets with input small eq bin...
Benchmarking cerl_sets with input small eq int...
Benchmarking maps with input large eq bin...
Benchmarking maps with input large eq int...
Benchmarking maps with input medium eq bin...
Benchmarking maps with input medium eq int...
Benchmarking maps with input small eq bin...
Benchmarking maps with input small eq int...
Benchmarking sets with input large eq bin...
Benchmarking sets with input large eq int...
Benchmarking sets with input medium eq bin...
Benchmarking sets with input medium eq int...
Benchmarking sets with input small eq bin...
Benchmarking sets with input small eq int...

##### With input large eq bin #####
Name                ips        average  deviation         median         99th %
maps             166.72        6.00 ms     ±8.78%        5.95 ms        6.28 ms
sets              65.36       15.30 ms     ±5.28%       15.13 ms       16.82 ms
cerl_sets         45.05       22.20 ms     ±7.66%       22.59 ms       25.78 ms

Comparison: 
maps             166.72
sets              65.36 - 2.55x slower +9.30 ms
cerl_sets         45.05 - 3.70x slower +16.20 ms

##### With input large eq int #####
Name                ips        average  deviation         median         99th %
maps             251.17        3.98 ms     ±9.02%        3.97 ms        4.08 ms
sets             106.50        9.39 ms     ±4.05%        9.34 ms       10.02 ms
cerl_sets         49.25       20.31 ms     ±3.88%       20.21 ms       21.04 ms

Comparison: 
maps             251.17
sets             106.50 - 2.36x slower +5.41 ms
cerl_sets         49.25 - 5.10x slower +16.32 ms

##### With input medium eq bin #####
Name                ips        average  deviation         median         99th %
maps            19.80 K       50.51 μs    ±43.19%       49.99 μs       53.41 μs
sets             7.16 K      139.58 μs    ±25.66%      138.41 μs      187.03 μs
cerl_sets        6.85 K      145.93 μs    ±24.94%      143.19 μs      207.91 μs

Comparison: 
maps            19.80 K
sets             7.16 K - 2.76x slower +89.07 μs
cerl_sets        6.85 K - 2.89x slower +95.42 μs

##### With input medium eq int #####
Name                ips        average  deviation         median         99th %
maps            34.16 K       29.28 μs    ±29.85%       28.79 μs       34.87 μs
sets            11.64 K       85.88 μs    ±33.19%       84.88 μs      130.80 μs
cerl_sets        8.52 K      117.40 μs    ±28.48%      115.68 μs      167.77 μs

Comparison: 
maps            34.16 K
sets            11.64 K - 2.93x slower +56.60 μs
cerl_sets        8.52 K - 4.01x slower +88.12 μs

##### With input small eq bin #####
Name                ips        average  deviation         median         99th %
maps          1212.89 K        0.82 μs  ±2932.23%        0.75 μs        1.06 μs
cerl_sets      816.49 K        1.22 μs  ±1638.32%        1.10 μs        1.47 μs
sets           658.45 K        1.52 μs  ±1169.52%        1.40 μs        1.82 μs

Comparison: 
maps          1212.89 K
cerl_sets      816.49 K - 1.49x slower +0.40 μs
sets           658.45 K - 1.84x slower +0.69 μs

##### With input small eq int #####
Name                ips        average  deviation         median         99th %
maps             4.80 M      208.52 ns  ±9696.18%         147 ns         453 ns
cerl_sets        1.90 M      525.39 ns  ±4505.73%         405 ns         741 ns
sets             0.79 M     1270.84 ns  ±1164.76%        1138 ns        1556 ns

Comparison: 
maps             4.80 M
cerl_sets        1.90 M - 2.52x slower +316.87 ns
sets             0.79 M - 6.09x slower +1062.31 ns

Adding a fast-path for maps:intersect(Same, Same) makes it 10-30x faster on my machine, but may be slower in some cases so I've left that out for now.

Either way I'm happy with this PR as-is and can merge it now if you want me to, I can always switch to maps:intersect/2 in the branch that improves its performance.

This pull request adds support for maps in the
sets module. According to my [benchmarks][bench],
using maps is faster in the huge majority of cases,
sometimes by multiple orders of magnitude, and in
the few cases it is slower, it is by less than 10%.

[bench]: https://github.com/josevalim/sets_bench
@josevalim
Copy link
Contributor Author

@jhogberg awesome, i ran your branch too and got 3-4x solid improvement. I changed this PR to use maps:intersect, updated the commit message and pushed.

Next I will update the EEP and then send a separate PR to remove cerl_sets.

@jhogberg jhogberg added testing currently being tested, tag is used by OTP internal CI and removed testing currently being tested, tag is used by OTP internal CI labels Dec 2, 2020
@jhogberg jhogberg merged commit 25a13ce into erlang:master Dec 7, 2020
@jhogberg
Copy link
Contributor

jhogberg commented Dec 7, 2020

Merged, thanks for the PR!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
team:VM Assigned to OTP team VM
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants