```class Particles( val positions: Array<an...
# compose
u
Copy code
class Particles(
    val positions: Array<androidx.compose.ui.geometry.Offset>,
    val velocities: Array<androidx.compose.ui.geometry.Offset>
)
For educational purposes, I tried a SoA of
Offsets
, but looking at byte code it autoboxes, so no performance gains. Is there a way to convince the compiler to give me
LongArray
under the hood? (The type is convenient, ideally I'd not like to lose it)
s
There isn’t a straightforward out-of-the-box solution, but you can use a
LongArray
with direct raw values of Offset. To access them, you’ll need to use packing and unpacking functions, which are publicly available.
☝️ 1
☝🏾 1
u
Do you mean at access-time call
Offset(val packedValue: Long)
, where the instantiation would be optimized away?
What do you mean?
u
Copy code
class Particles(
   private val positions: LongArray
) {
    fun positionAt(index): Offset = Offset(positions[index])
}
something like this
s
There is no instantiation here, the class is a value class. You can use directly pack and unpack functions as they used in the Offset class
☝️ 1
r
For particles you should just use a FloatArray that contains the x and y pairs of all particles
u
yes yes, im just going through all the levels to see how much each optimization matters
r
It’s not even about performance, it’s data modeling. A velocity is not an offset
u
I mean, I only use it as a x,y tuple really 😄
r
I understand
There’s a FloatPair object for that though
🤔 1
I think I optimized too
But both adds unnecessary operations vs just using floats
1
u
yes but I like the operators on it,
offset + offset
, makes math neater but yes ultimately I'll decompose it, just seeing how much a difference a packed value class vs decomposed float arrays make
r
Offset + Offset has to unpack 4 floats, add them, and repack
Some of the operations are implemented without unpacking (like unaryMinus or abs)
I really need to write that blog post on how Offset was optimized
(In the end it lost 50% of its instructions after compilation)
u
yes its nice, but
OffsetArray
would be nice as well 😄 to avoid boxing and actually benefit
r
But offset was not designed for what you are doing
1
And an offset array would still have those costs
u
im looking at all the types in compose, I'm noticing there is literally
Velocity
😄 eh
r
Plus like Serhil said you can use a long array and the pack functions
u
yea thats im trying to do right now, thank you all
r
Yeah and Velocity is implemented just like Offset
u
btw I noticed bunch of these
fastXYZ()
functions, why? will they get upstreamed to kotlin if they're simply faster?
r
They won’t. They are either highly specific to the Android runtime, or they change developer ergonomics, or they are approximations
For the changes that made sense I did send patches/filed bugs
u
should app developer prefer them?
r
Probably not. The fastForEach is nice because it avoids allocations but the other ones are only useful in tight loops
fastCoerceIn for instance skips the range check which adds a branch per call (and wasn’t inline)
Stuff like that
u
one thing im missing for my gravity sim is
faster sqrt
. is that the best jvm can do? if I dont sqrt in my math, I get 20fps more, sqrt is so heavy its surprising
r
Yeah sqrt is expensive
Relative to other math functions that is
You could try a pow(x, 0.5) in case sqrt is implemented as an inverseSqrt
And if that’s not enough you could use the Quake III sqrt approximation (Compose has a cubic root approximation, the comments explain how to derive your own approximation for any power)
u
thats what I wanted to say, I faintly remember some john carmac stuff on this :D
r
You can read the other fast functions in there to see what they do
u
oh neat.. ballpark how inaccurate is it?
r
The comment on the function gives that info
u
oh..thank you!
r
• The maximum error compared to [kotlin.math.cbrt] is: ◦ - 5.9604645E-7 in the range -1f..1f ◦ - 4.7683716E-6 in the range -256f..256f ◦ - 3.8146973E-5 in the range -65_536f..65_536f ◦ - 1.5258789E-4 in the range -16_777_216..16_777_216
So it’s pretty good actually
You can increase/reduce accuracy by adding/removing rounds of Newton-Rhapson
There’s also approximations for cos and sin
u
ill give it a go and see how much it helps, thank you!
r
Also don’t measure fps
Measure missed frames, and make sure your clocks are locked, etc.
That 20fps difference is suspicious
u
whats a locked clock? `withFrameMillis`˛vs
withFrameNanos
?
r
No, the CPU clocks. To make sure that you are comparing at the same cpu frequency
Using the benchmark library will automatically stabilize the device as much as possible to give more relevant timing data
u
do you mean to make sure cpu is boosting?
i see
r
No, just to make sure it’s the same frequency. Usually you want to lock at a lower frequency not the max to avoid thermal throttling
Also you need to make sure you are not measuring in interpreted mode, or during jitting
The benchmark library takes care of that
u
hmm, im mostly using this to get intuition about stuff, so it doesnt have to be super exact im aware release compose is way faster, but would you say the improvement is somehow non linear over debug?
i.e. "R8 will give me +30% constant-ish" (which I could just imagine-away & not have to wait for R8 while iterating)
> Measure missed frames.. do you mean literally to manually measure how much each update math takes, and count those are above 16ms if on 60hz display? or is there something neater for this
s
r
Also, using perfetto
It'll show the CPU frequency and accurate timing. Importantly it will also show the time spent on the GPU (there are APIs on Android to see that as well)
u
would that work with compose desktop? (it's where my big gpu is)
r
Ah yeah, no
If you see a drop of 20 fps because of a sqrt on desktop, the issue is definitely not sqrt (or you are doing something really strange 😅)
u
yea..i think whats happening is that the acceleration is then way bigger, and particles fly off screen and
Canvas
seems to optimize away off screen draw calls so yea youre right
r
It does, yes
That's why you'll want to measure your drawing time separately from your computation time
u
hmm, come to think of it, i am measuring only the computation, but the manual fps seem to increase with less particles on screen odd thanks!
I mean its literally just this
Copy code
var fps by remember { mutableFloatStateOf(0f) }
//    var dummyCounter by remember { mutableStateOf(0) } // To cause redraw (also needs to be accessed in canvas)

LaunchedEffect(Unit) {
    var lastFrameTimestamp = 0L
    var frameCount = 0
    var elapsedTimeNanos = 0L
    while (isActive) {
        val frameTimestamp = withFrameNanos { it }
        val deltaTimeNanos = if (lastFrameTimestamp == 0L) {
            0
        } else {
            frameTimestamp - lastFrameTimestamp
        }

        val deltaTimeSeconds = deltaTimeNanos / 1_000_000_000f
        updateParticles(particles, deltaTimeSeconds)

        fps = frameCount / elapsedTimeNanos.toFloat() * 1_000_000_000f
        frameCount++
        elapsedTimeNanos += deltaTimeNanos
        lastFrameTimestamp = frameTimestamp
        // dummyCounter++
    }
}
r
withFrameNanos()
triggers a vsync (don't know if they made it work on Desktop), so the framerate should always be your display refresh rate
Also your fps counter is not averaged over one second, it's the average over the lifetime of the rendering loop
👍 1
u
I'll fix the math, but are you saying this approach is wrong? or are you only saying my fps will at most be my display refresh rate?
I'm curious what you think of this one, it seems that order of operations seem to make a difference Granted yea im measuring manually and my measurement is flawed, but even so I notice a difference
Copy code
val xForce = ff / distanceCubed * xDeltaPosition
val yForce = ff / distanceCubed * yDeltaPosition
vs
Copy code
val xForce = ff * xDeltaPosition / distanceCubed
val yForce = ff * yDeltaPosition / distanceCubed
the first one is faster, as if it saw the redundant calculation but im running in debug (no r8), so..who is doing the optimizing? is it jit? would you expect this, or is it some side effect of my bad measurement?
r
Hard to say without looking at the assembly. The only thing I could think of that would show a measurable difference is if you are on x86 and generating denormals
Otherwise probably your measurement
👍 1