Fixing uniqueness performance#264
Conversation
wstrinz
left a comment
There was a problem hiding this comment.
Got a bit nerd sniped by this now that Chat GPT can help me think through asymptotics 😅 https://chatgpt.com/share/e/68e53e47-d1c0-8007-9e8c-8f50945ceed3
Turns out List.extra actually had a similarly performant version but it was removed because it required a comparable input and the new version benched ok... on small lists 🤦
Also Chat thinks the new version is actually O(n log n) , which of course is an improvement and fine for our purposes, and apparently the best you can do without importing other libraries. But if we wanted to get it to O(n) we could potentially use HashSets with something like
import HashSet as HS
uniqueByHash : (a -> k) -> (k -> comparable) -> List a -> List a
uniqueByHash toKey hash =
let
step x (seen, acc) =
let k = toKey x in
if HS.member k seen then
( seen, acc )
else
( HS.insert k seen, x :: acc )
in
\xs -> xs |> List.foldl step (HS.empty hash, []) |> Tuple.second |> List.reverse
Oh no! 😭
You're totally right @wstrinz / ChatGPT. I thought that was what I put, but I guess not. 😅 |
🤦 Elm... it drives me crazy when languages/libraries opt you in automatically to the asymptotically worse but usually faster implementation I was hoping to find something like SortedSet for this use case but don't see anything compelling from Elm |
List.Extra.uniqueByapparently is not very performant with very long lists. The issue here is that the way it works is by iterating over every item in the listO(n)and then for each one, it adds it to aListof "known" values. If that value already exists in the known values (List.member) then it will NOT carry it over to the final list.List.member, however, is aO(n)operation itself, so that leads to this essentially beingO(n^2). This seems to tip over around 10-20k rows where it becomes noticeably slow. I was observing upwards of 40 seconds on my mac for lists of 100k.My alternative to this implementation is pretty simple, in fact it's the exact same algorithm, just swapping out the
Listof known values andList.membercheck with aSetof known values and aSet.membercheck.Without going into the Javascript implementation of core Elm, I'm guessing that Set membership is probably
O(n log n).For comparison, here's the old implementation:
List.Extra.uniqueBy
Note that they essentially follow the same algorithm aside from the presence check being a
List.member