elm-geometry icon indicating copy to clipboard operation
elm-geometry copied to clipboard

Remove 'clever' optimization in rotation functions?

Open ianmackenzie opened this issue 5 years ago • 3 comments

Currently, rotateAround functions are optimized for partial application - they cache computed sines/cosines etc. in a returned lambda, which is then just immediately called and discarded if rotateAround is called with three arguments instead of two. This makes cases like

List.map (Point3d.rotateAround axis angle) points

more efficient since sines/cosines etc. are only calculated once, but

Point3d.rotateAround axis angle point

less efficent since a lambda function is allocated and then immediately discarded.

It may be better to switch to the 'naive' implementation, and use the 'frame trick' for efficient transformations where necessary. This would make efficient single calculations possible (currently there's no easy way to avoid the lambda-allocation overhead), and there would be a consistent story for optimization - just use frames!

ianmackenzie avatar Sep 17 '18 16:09 ianmackenzie

I stumbled across this and was interested in the numbers, so here is a quick benchmark

rotating

I'm not sure how to weigh this really, but it's interesting to know. Remarkably, the speed increase for optimized doesn't scale that much with the size of the list: it is ~40% for a 5-element list, but only about ~44% for a 1000-element list. Looks like there is some jit optimization/caching going on.

code (Doesn't actually run in ellie because of the elm: Map.!: given key is not an element in the map compiler bug)

folkertdev avatar Oct 21 '18 12:10 folkertdev

Very useful, thanks @folkertdev! I'm glad that it's not an order of magnitude difference either way, so if we make a change at some point it's not like anyone's performance is going to completely fall off a cliff.

I was thinking it would be really cool if the Elm compiler could apply this optimization automatically - if a function is ever partially applied, eagerly evaluate anything that only depends on the arguments that have been supplied, then cache those so they don't have to be re-evaluated each time the partially applied function is called. Then functions like rotateAround could be written in the "naive" way and the compiler would effectively produce the "optimized" version if they were partially applied.

ianmackenzie avatar Oct 21 '18 14:10 ianmackenzie

Possible approach: add Rotation2d and Rotation3d types which internally store pre-computed rotation matrix components. Then keep the existing rotateAround functions but add (e.g.)

Point3d.rotateBy : Rotation3d -> Point3d -> Point3d

with

Rotation3d.around : Axis3d -> Angle -> Rotation3d

-- Possibly also
Rotation3d.counterclockwiseAround : Axis3d -> Rotation3d
Rotation3d.clockwiseAround : Axis3d -> Rotation3d

then Point3d.rotateAround could be implemented as

rotateAround : Axis3d -> Angle -> Point3d -> Point3d
rotateAround axis angle point =
    rotateBy (Rotation3d.around axis angle) point

with the downside that this now requires a Rotation3d object allocation...perhaps keep the current implementation of rotateAround (minus the 'clever' optimization) for fixed-size types like points, line segments and triangles and redirect to rotateBy with a newly-constructed Rotation3d value for types like Polyline3d where the allocation is more likely to be worthwhile.

ianmackenzie avatar Nov 06 '18 20:11 ianmackenzie