Using Array.Hamt and ports?

From a post on pushing large byte arrays between JS and elm:

I recently benchmarked the various options (thank you BrianHicks/elm-benchmark!). To summarize, if you want to get an array of numbers from Elm into JS, and you use Array.Hamt as your Array type in 0.18, you can’t do the obvious:

import Array.Hamt as Array
port sendAllTheInts : Array Int -> Cmd msg

This fails because there is no native support for Array.Hamt in port marshaling. (Aside, if Json.Encode were a typeclass, then Array.Hamt could have provided an implementation …).

The next obvious approach is:

import Array.Hamt as Array
sendAllTheInts : Array Int -> Cmd msg
sendAllTheInts = Array.toList >> sendAllTheInts_
port sendAllTheInts_ : List Int -> Cmd msg

But this is slower than this:

import Array.Hamt as Array
import Json.Encode as E
sendAllTheInts : Array Int -> Cmd msg
sendAllTheInts = Array.map E.int >> Array.toList >> E.list >> sendAllTheInts_
port sendAllTheInts_ : E.Value -> Cmd msg

Nice thing is that the slippery E.Value argument to the port is hidden within your module (don’t expose sendAllTheInts_). And, bonus, the JS code is exactly the same (save the name of the port) in all three cases!

I tried writing an Array.Hamt to Array conversion function, and so the port could marshal a native Array… but that was slower than the List based versions above.

6 Likes

glad you’re finding it useful! :heart:

How does flipping the arguments Array.toList >> Array.map E.int compare? That’s what I would have reached for naturally, so I’m curious how you came the opposite conclusion.

Also FWIW it’s really nice to know that aside from being safer, explicitly encoding and sending a Value is also faster. :grin:

Array.map is waaaaaaaay faster than List.map :slight_smile:

Combining both options in Array.foldr is faster still: Array.foldr (\e acc -> E.int e :: acc) [] arr

4 Likes

I believe you mean: Array.toList >> List.map E.int… Note that this is what is happening in my “obvious approach” example: Under the hood, a port declared List Int performs List.map E.int >> E.value to marshal.

Array.map E.int >> Array.toList >> E.value

is about 1.8x faster than

Array.toList >> List.map E.int >> E.value

So, faster this case, not “waaaaaaaay”, but better. I’ll benchmark the foldr method and see how that fairs.

Side by side benchmarks:

-- Array.Hamt.Array Int -> List E.Value      runs/sec     cc'd
Array.toList >> List.map E.int                  987      1,028
Array.map E.int >> Array.toList               1,671      1,789
Array.foldr (\e acc -> E.int e :: acc) []     2,907      2,633
Array.foldr (E.int >> (::)) []                  641        747

cc’d is the result after running the compiled elm through Closure Compiler.

Those are some pretty significant differences!

The saddest part is the last one, since, coming from Haskell, I write things like that all the time. In Haskell the compiler does a great job of those, but clearly the elm compiler doesn’t.

Yeah. In general, when performance is important, being explicit is key.

This has to do with how Elm handles currying, but is something I think will be improved with time. Keep in mind that the Elm compiler is still pretty young. It’ll get better.