Smart use of the guard @peerreynders.
Because I was interested, and since my own take on using Task
did not result in significant improvements, I tried using Flow. This resulted in significant speed and memory usage improvements.
The following is a comparison of the different approaches from sasajuric and peerreynders and the Flow modification. Benchee used for bench marking.
Name ips average deviation median 99th %
flow 1.46 0.68 s ±3.28% 0.69 s 0.72 s
flow+ets 0.70 1.44 s ±1.19% 1.44 s 1.46 s
stream 0.37 2.69 s ±1.71% 2.69 s 2.75 s
sync 0.36 2.81 s ±3.09% 2.81 s 2.91 s
Comparison:
flow 1.46
flow+ets 0.70 - 2.10x slower
stream 0.37 - 3.94x slower
sync 0.36 - 4.12x slower
Memory usage statistics:
Name average deviation median 99th %
flow 8.35 MB ±0.26% 8.34 MB 8.37 MB
flow+ets 0.22 MB ±0.00% 0.22 MB 0.22 MB
stream 944.08 MB ±0.00% 944.08 MB 944.08 MB
sync 374.68 MB ±0.00% 374.68 MB 374.68 MB
Comparison:
flow 8.34 MB
flow+ets 0.22 MB - 0.03x memory usage
stream 944.08 MB - 113.16x memory usage
sync 374.68 MB - 44.91x memory usage
CODE
defmodule Test do
defp make_all_sums(top),
do: &all_sums(&1, top, [])
defp all_sums(value, top, results) when top < value, # remaining sums are known duplicates
do: results
defp all_sums(value, top, rest),
do: all_sums(value, top - 1, [top + value | rest])
def stream_run(top) do
1..top
|> Stream.flat_map(make_all_sums(top))
|> Stream.uniq()
|> Enum.to_list()
end
def flow_run(top) do
1..top
|> Flow.from_enumerable()
|> Flow.flat_map(make_all_sums(top))
|> Flow.uniq()
|> Enum.uniq()
end
defp make_all_sums_ets(top, table),
do: &all_sums_ets(&1, top, table, [])
defp all_sums_ets(value, top, table, results) when top < value, # remaining sums are known duplicates
do: results
defp all_sums_ets(value, top, table, rest) do
:ets.insert(table, {top + value})
all_sums_ets(value, top - 1, table, [])
end
def flow_ets_run(top) do
table =
:ets.new(:unique_sums, [
:set,
:public,
read_concurrency: true,
write_concurrency: true
])
1..top
|> Flow.from_enumerable()
|> Flow.flat_map(make_all_sums_ets(top, table))
|> Enum.to_list()
:ets.select(table, [{{:"$1"}, [], [:"$1"]}])
end
def unique_sums(list) do
table = :ets.new(:unique_sums, [:set])
try do
collect_unique_sums(list, table)
:ets.select(table, [{{:"$1"}, [], [:"$1"]}])
after
:ets.delete(table)
end
end
defp collect_unique_sums([], _table), do: :ok
defp collect_unique_sums([el | rest], table) do
:ets.insert(table, {el + el})
Enum.each(rest, &:ets.insert(table, {&1 + el}))
collect_unique_sums(rest, table)
end
end