File 4361-erts-Optimize-maps-merge-2-of-small-maps.patch of Package erlang
From c684d1a7a295df555c5d1aef81ce566d92d573fe Mon Sep 17 00:00:00 2001
From: Sverker Eriksson <sverker@erlang.org>
Date: Thu, 9 Mar 2023 17:54:24 +0100
Subject: [PATCH] erts: Optimize maps:merge/2 of small maps
If 2nd map has all keys return it as-is.
If 1st map has all keys reuse its key tuple.
---
erts/emulator/beam/erl_map.c | 55 ++++++++++++++++------------
erts/emulator/test/map_SUITE.erl | 39 +++++++++++++++++---
system/doc/efficiency_guide/maps.xml | 20 ++++++++--
3 files changed, 81 insertions(+), 33 deletions(-)
diff --git a/erts/emulator/beam/erl_map.c b/erts/emulator/beam/erl_map.c
index c4ddee1436..4cd9d65839 100644
--- a/erts/emulator/beam/erl_map.c
+++ b/erts/emulator/beam/erl_map.c
@@ -1279,34 +1279,31 @@ BIF_RETTYPE maps_merge_2(BIF_ALIST_2) {
BIF_ERROR(BIF_P, BADMAP);
}
-static Eterm flatmap_merge(Process *p, Eterm nodeA, Eterm nodeB) {
+static Eterm flatmap_merge(Process *p, Eterm map1, Eterm map2) {
Eterm *hp,*thp;
- Eterm tup;
Eterm *ks,*vs,*ks1,*vs1,*ks2,*vs2;
flatmap_t *mp1,*mp2,*mp_new;
Uint n,n1,n2,i1,i2,need,unused_size=0;
Sint c = 0;
- mp1 = (flatmap_t*)flatmap_val(nodeA);
- mp2 = (flatmap_t*)flatmap_val(nodeB);
+ mp1 = (flatmap_t*)flatmap_val(map1);
+ mp2 = (flatmap_t*)flatmap_val(map2);
n1 = flatmap_get_size(mp1);
n2 = flatmap_get_size(mp2);
- if (n1 == 0) return nodeB;
- if (n2 == 0) return nodeA;
+ if (n1 == 0) return map2;
+ if (n2 == 0) return map1;
need = MAP_HEADER_FLATMAP_SZ + 1 + 2 * (n1 + n2);
hp = HAlloc(p, need);
- thp = hp;
- tup = make_tuple(thp);
- ks = hp + 1; hp += 1 + n1 + n2;
mp_new = (flatmap_t*)hp; hp += MAP_HEADER_FLATMAP_SZ;
vs = hp; hp += n1 + n2;
+ thp = hp;
+ ks = hp + 1; hp += 1 + n1 + n2;
mp_new->thing_word = MAP_HEADER_FLATMAP;
- mp_new->size = 0;
- mp_new->keys = tup;
+ mp_new->keys = make_tuple(thp);
i1 = 0; i2 = 0;
ks1 = flatmap_get_keys(mp1);
@@ -1346,20 +1343,32 @@ static Eterm flatmap_merge(Process *p, Eterm nodeA, Eterm nodeB) {
i2++;
}
- if (unused_size) {
- /* the key tuple is embedded in the heap, write a bignum to clear it.
- *
- * release values as normal since they are on the top of the heap
- * size = n1 + n1 - unused_size
- */
-
- *ks = make_pos_bignum_header(unused_size - 1);
- HRelease(p, vs + unused_size, vs);
- }
-
n = n1 + n2 - unused_size;
- *thp = make_arityval(n);
mp_new->size = n;
+ *thp = make_arityval(n);
+
+ if (unused_size ) {
+ Eterm* hp_release;
+
+ if (n == n2) {
+ /* Reuse entire map2 */
+ HRelease(p, hp, (Eterm *)mp_new);
+ return map2;
+ }
+ else if (n == n1) {
+ /* Reuse key tuple of map1 */
+ mp_new->keys = mp1->keys;
+ /* Release key tuple and unused values */
+ hp_release = thp - unused_size;
+ }
+ else {
+ /* Unused values are embedded in the heap, write bignum to clear them */
+ *vs = make_pos_bignum_header(unused_size - 1);
+ /* Release unused keys */
+ hp_release = ks;
+ }
+ HRelease(p, hp, hp_release);
+ }
/* Reshape map to a hashmap if the map exceeds the limit */
diff --git a/erts/emulator/test/map_SUITE.erl b/erts/emulator/test/map_SUITE.erl
index 5707a8e56c..043a3f3260 100644
--- a/erts/emulator/test/map_SUITE.erl
+++ b/erts/emulator/test/map_SUITE.erl
@@ -1981,12 +1981,7 @@ t_bif_map_merge(Config) when is_list(Config) ->
M0 = #{ "hi" => "hello", int => 3, <<"key">> => <<"value">>,
4 => number, 18446744073709551629 => wat},
-
- #{ "hi" := "hello", int := 3, <<"key">> := <<"value">>,
- 4 := number, 18446744073709551629 := wat} = maps:merge(#{}, M0),
-
- #{ "hi" := "hello", int := 3, <<"key">> := <<"value">>,
- 4 := number, 18446744073709551629 := wat} = maps:merge(M0, #{}),
+ merge_with_empty(M0),
M1 = #{ "hi" => "hello again", float => 3.3, {1,2} => "tuple", 4 => integer },
@@ -2001,6 +1996,7 @@ t_bif_map_merge(Config) when is_list(Config) ->
Is = lists:seq(1,N),
M2 = maps:from_list([{I,I}||I<-Is]),
150000 = maps:size(M2),
+ merge_with_empty(M2),
M3 = maps:from_list([{<<I:32>>,I}||I<-Is]),
150000 = maps:size(M3),
M4 = maps:merge(M2,M3),
@@ -2039,6 +2035,28 @@ t_bif_map_merge(Config) when is_list(Config) ->
M11 = maps:merge(M9,M10),
ok = check_keys_exist(Ks1 ++ Ks2, M11),
+ %% Verify map and/or key tuple reuse
+
+ MS = #{ "hi" => "hello", int => 3, <<"key">> => <<"value">>},
+ merge_with_empty(MS),
+ MS_keys = erts_internal:map_to_tuple_keys(MS),
+
+ %% key tuple reuse
+ MS_a = maps:merge(MS, #{int => 4}),
+ true = erts_debug:same(erts_internal:map_to_tuple_keys(MS_a), MS_keys),
+ %% map reuse
+ MS_b = maps:merge(#{int => 4}, MS),
+ true = erts_debug:same(MS_b, MS),
+
+ %% mutated map reuse with literal key tuple
+ MS_c = maps:put(int, 4, maps:remove(int, MS)),
+ false = erts_debug:same(erts_internal:map_to_tuple_keys(MS_c), MS_keys),
+ MS_cc = maps:merge(MS, MS_c),
+ true = erts_debug:same(MS_cc, MS_c),
+
+ MS_d = maps:merge(MS_c, MS),
+ true = erts_debug:same(MS_d, MS),
+
%% error case
do_badmap(fun(T) ->
{'EXIT',{{badmap,T},[{maps,merge,_,_}|_]}} =
@@ -2050,6 +2068,15 @@ t_bif_map_merge(Config) when is_list(Config) ->
end),
ok.
+merge_with_empty(M0) ->
+ M0_1 = maps:merge(#{}, M0),
+ M0 = M0_1,
+ true = erts_debug:same(M0, M0_1),
+
+ M0_2 = maps:merge(M0, #{}),
+ M0 = M0_2,
+ true = erts_debug:same(M0, M0_2),
+ ok.
t_bif_map_put(Config) when is_list(Config) ->
M0 = #{ "hi" => "hello", int => 3, <<"key">> => <<"value">>,
diff --git a/system/doc/efficiency_guide/maps.xml b/system/doc/efficiency_guide/maps.xml
index 140bdd549a..a2e004af59 100644
--- a/system/doc/efficiency_guide/maps.xml
+++ b/system/doc/efficiency_guide/maps.xml
@@ -37,6 +37,7 @@
finally the functions in the <seeerl
marker="stdlib:maps">maps</seeerl> module.</p>
+ <marker id="terminology"/>
<p>Terminology used in this chapter:</p>
<list type="bulleted">
<item>A map with at most 32 elements will informally be called a
@@ -500,7 +501,8 @@ get(Key, Map, Default) ->
<p>Therefore, a call <c>maps:get/3</c> is more expensive than a
call to <c>maps:get/2</c>.</p>
- <p>If a small map is used as alternative to using a record,
+ <p>If a <seeguide marker="#terminology">small map</seeguide>
+ is used as alternative to using a record,
instead of calling <c>maps:get/3</c> multiple times to handle
default values, consider putting the default values in a map and
merging that map with the other map:</p>
@@ -580,7 +582,17 @@ get(Key, Map, Default) ->
<section>
<title>maps:merge/2</title>
<p><seemfa marker="stdlib:maps#merge/2">maps:merge/2</seemfa>
- is implemented in C.</p>
+ is implemented in C. For <seeguide marker="#terminology">small
+ maps</seeguide>, the key tuple may be shared with any of the argument
+ maps if that argument map contains all the keys. Literal key tuples are
+ prefered if possible.</p>
+ <note>
+ <p>
+ The sharing of key tuples by <c>maps:merge/2</c> was introduced in
+ OTP 26.0. Older versions always contructed a new key tuple on
+ the callers heap.
+ </p>
+ </note>
</section>
<section>
@@ -613,7 +625,7 @@ get(Key, Map, Default) ->
<p>If the keys are constants known at compile-time, using the
map update syntax with the <c>=></c> operator is more
efficient than multiple calls to <c>maps:put/3</c>,
- especially for small maps.</p>
+ especially for <seeguide marker="#terminology">small maps</seeguide>.</p>
</section>
<section>
@@ -660,7 +672,7 @@ get(Key, Map, Default) ->
<p>If the keys are constants known at compile-time, using the
map update syntax with the <c>:=</c> operator is more
efficient than multiple calls to <c>maps:update/3</c>,
- especially for small maps.</p>
+ especially for <seeguide marker="#terminology">small maps</seeguide>.</p>
</section>
<section>
--
2.35.3