Skip to content

Commit e3fb8ed

Browse files
committed
Optimize map unions to avoid building long lists
1 parent 8a5ed11 commit e3fb8ed

File tree

2 files changed

+165
-2
lines changed

2 files changed

+165
-2
lines changed

lib/elixir/lib/module/types/descr.ex

Lines changed: 124 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1278,8 +1278,130 @@ defmodule Module.Types.Descr do
12781278

12791279
defp map_only?(descr), do: empty?(Map.delete(descr, :map))
12801280

1281-
# Union is list concatenation
1282-
defp map_union(dnf1, dnf2), do: dnf1 ++ (dnf2 -- dnf1)
1281+
defp map_union(dnf1, dnf2) do
1282+
# Union is just concatenation, but we rely on some optimization strategies to
1283+
# avoid the list to grow when possible
1284+
1285+
# first pass trying to identify patterns where two maps can be fused as one
1286+
with [{tag1, pos1, []}] <- dnf1,
1287+
[{tag2, pos2, []}] <- dnf2,
1288+
strategy when strategy != nil <- map_union_optimization_strategy(tag1, pos1, tag2, pos2) do
1289+
case strategy do
1290+
:all_equal ->
1291+
dnf1
1292+
1293+
:any_map ->
1294+
[{:open, %{}, []}]
1295+
1296+
{:one_key_difference, key, v1, v2} ->
1297+
new_pos = Map.put(pos1, key, union(v1, v2))
1298+
[{tag1, new_pos, []}]
1299+
1300+
:left_subtype_of_right ->
1301+
dnf2
1302+
1303+
:right_subtype_of_left ->
1304+
dnf1
1305+
end
1306+
else
1307+
# otherwise we just concatenate and remove structural duplicates
1308+
_ -> dnf1 ++ (dnf2 -- dnf1)
1309+
end
1310+
end
1311+
1312+
defp map_union_optimization_strategy(tag1, pos1, tag2, pos2)
1313+
defp map_union_optimization_strategy(tag, pos, tag, pos), do: :all_equal
1314+
defp map_union_optimization_strategy(:open, empty, _, _) when empty == %{}, do: :any_map
1315+
defp map_union_optimization_strategy(_, _, :open, empty) when empty == %{}, do: :any_map
1316+
1317+
defp map_union_optimization_strategy(tag, pos1, tag, pos2)
1318+
when map_size(pos1) == map_size(pos2) do
1319+
:maps.iterator(pos1)
1320+
|> :maps.next()
1321+
|> do_map_union_optimization_strategy(pos2, :all_equal)
1322+
end
1323+
1324+
defp map_union_optimization_strategy(:open, pos1, _, pos2)
1325+
when map_size(pos1) <= map_size(pos2) do
1326+
:maps.iterator(pos1)
1327+
|> :maps.next()
1328+
|> do_map_union_optimization_strategy(pos2, :right_subtype_of_left)
1329+
end
1330+
1331+
defp map_union_optimization_strategy(_, pos1, :open, pos2)
1332+
when map_size(pos1) >= map_size(pos2) do
1333+
:maps.iterator(pos2)
1334+
|> :maps.next()
1335+
|> do_map_union_optimization_strategy(pos1, :right_subtype_of_left)
1336+
|> case do
1337+
:right_subtype_of_left -> :left_subtype_of_right
1338+
nil -> nil
1339+
end
1340+
end
1341+
1342+
defp map_union_optimization_strategy(_, _, _, _), do: nil
1343+
1344+
defp do_map_union_optimization_strategy(:none, _, status), do: status
1345+
1346+
defp do_map_union_optimization_strategy({key, v1, iterator}, pos2, status) do
1347+
with %{^key => v2} <- pos2,
1348+
next_status when next_status != nil <- map_union_next_strategy(key, v1, v2, status) do
1349+
do_map_union_optimization_strategy(:maps.next(iterator), pos2, next_status)
1350+
else
1351+
_ -> nil
1352+
end
1353+
end
1354+
1355+
defp map_union_next_strategy(key, v1, v2, status)
1356+
1357+
# structurally equal values do not impact the ongoing strategy
1358+
defp map_union_next_strategy(_key, same, same, status), do: status
1359+
1360+
defp map_union_next_strategy(key, v1, v2, :all_equal) do
1361+
if key != :__struct__, do: {:one_key_difference, key, v1, v2}
1362+
end
1363+
1364+
defp map_union_next_strategy(_key, v1, v2, {:one_key_difference, _, d1, d2}) do
1365+
# we have at least two key differences now, we switch strategy
1366+
# if both are subtypes in one direction, keep checking
1367+
cond do
1368+
trivial_subtype?(d1, d2) and trivial_subtype?(v1, v2) -> :left_subtype_of_right
1369+
trivial_subtype?(d2, d1) and trivial_subtype?(v2, v1) -> :right_subtype_of_left
1370+
true -> nil
1371+
end
1372+
end
1373+
1374+
defp map_union_next_strategy(_key, v1, v2, :left_subtype_of_right) do
1375+
if trivial_subtype?(v1, v2), do: :left_subtype_of_right
1376+
end
1377+
1378+
defp map_union_next_strategy(_key, v1, v2, :right_subtype_of_left) do
1379+
if trivial_subtype?(v2, v1), do: :right_subtype_of_left
1380+
end
1381+
1382+
# cheap to compute sub-typing
1383+
# a trivial subtype is always a subtype, but not all subtypes are subtypes
1384+
defp trivial_subtype?(_, :term), do: true
1385+
defp trivial_subtype?(same, same), do: true
1386+
1387+
defp trivial_subtype?(%{} = left, %{} = right)
1388+
when map_size(left) == 1 and map_size(right) == 1 do
1389+
case {left, right} do
1390+
{%{atom: _}, %{atom: {:negation, neg}}} when neg == %{} ->
1391+
true
1392+
1393+
{%{map: _}, %{map: [{:open, pos, []}]}} when pos == %{} ->
1394+
true
1395+
1396+
{%{bitmap: bitmap1}, %{bitmap: bitmap2}} ->
1397+
(bitmap1 &&& bitmap2) === bitmap2
1398+
1399+
_ ->
1400+
false
1401+
end
1402+
end
1403+
1404+
defp trivial_subtype?(_, _), do: false
12831405

12841406
# Given two unions of maps, intersects each pair of maps.
12851407
defp map_intersection(dnf1, dnf2) do

lib/elixir/test/elixir/module/types/descr_test.exs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,47 @@ defmodule Module.Types.DescrTest do
105105
assert union(difference(list(term()), list(integer())), list(integer()))
106106
|> equal?(list(term()))
107107
end
108+
109+
test "optimizations" do
110+
# The tests are checking the actual implementation, not the semantics.
111+
# This is why we are using structural comparisons.
112+
# It's fine to remove these if the implementation changes, but breaking
113+
# these might have an important impact on compile times.
114+
115+
# Optimization one: same tags, all but one key are structurally equal
116+
assert union(
117+
open_map(a: float(), b: atom()),
118+
open_map(a: integer(), b: atom())
119+
) == open_map(a: union(float(), integer()), b: atom())
120+
121+
assert union(
122+
closed_map(a: float(), b: atom()),
123+
closed_map(a: integer(), b: atom())
124+
) == closed_map(a: union(float(), integer()), b: atom())
125+
126+
# Optimization two: we can tell that one map is a trivial subtype of the other:
127+
128+
assert union(
129+
closed_map(a: term(), b: term()),
130+
closed_map(a: float(), b: binary())
131+
) == closed_map(a: term(), b: term())
132+
133+
assert union(
134+
open_map(a: term()),
135+
closed_map(a: float(), b: binary())
136+
) == open_map(a: term())
137+
138+
assert union(
139+
closed_map(a: float(), b: binary()),
140+
open_map(a: term())
141+
) == open_map(a: term())
142+
143+
# Do we want this want to pass or keep shallow checks only?
144+
# assert union(
145+
# closed_map(a: term(), b: tuple([term(), term()])),
146+
# closed_map(a: float(), b: tuple([atom(), binary()]))
147+
# ) == closed_map(a: term(), b: term())
148+
end
108149
end
109150

110151
describe "intersection" do

0 commit comments

Comments
 (0)