Skip to content

Commit a0d98c7

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

File tree

2 files changed

+173
-2
lines changed

2 files changed

+173
-2
lines changed

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

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

12841414
# Given two unions of maps, intersects each pair of maps.
12851415
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)