Hi there!
This is just a query of interest rather than an actual problem I’m having. I’ve been experimenting with Data Orientated Design and have a created a (very) rudimentary benchmark app for timing some different approaches to normalizing an array of 3D vectors (sorry its fairly long):
import java.util.concurrent.TimeUnit
import kotlin.random.Random
import kotlin.reflect.KProperty
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.measureTime
import kotlin.time.toDuration
val rnd = Random(1234)
data class Vector(var x: Float, var y: Float, var z: Float) {
fun normalize() {
val l = x * x + y * y + z * z
x /= l
y /= l
z /= l
}
}
fun timeOop(count: Int): Duration {
val vectors = Array(count) { Vector(rnd.nextFloat(), rnd.nextFloat(), rnd.nextFloat()) }
return measureTime {
for (vector in vectors) {
vector.normalize()
}
}
}
private fun normalizeInArray(xs: FloatArray, ys: FloatArray, zs: FloatArray, i: Int) {
val x = xs[i]
val y = ys[i]
val z = zs[i]
val l = x * x + y * y + z * z
xs[i] /= l
ys[i] /= l
zs[i] /= l
}
fun timeDod(count: Int): Duration {
val xs = FloatArray(count) { rnd.nextFloat() }
val ys = FloatArray(count) { rnd.nextFloat() }
val zs = FloatArray(count) { rnd.nextFloat() }
return measureTime {
for (i in xs.indices) {
normalizeInArray(xs, ys, zs, i)
}
}
}
class IntoArrayDelegate(val array: FloatArray, val index: Int) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): Float {
return array[index]
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Float) {
array[index] = value
}
}
class DelegatedVector(xs: FloatArray, ys: FloatArray, zs: FloatArray, index: Int) {
var x: Float by IntoArrayDelegate(xs, index)
var y: Float by IntoArrayDelegate(ys, index)
var z: Float by IntoArrayDelegate(zs, index)
fun normalize() {
val l = x * x + y * y + z * z
x /= l
y /= l
z /= l
}
}
fun timeDodDelegated(count: Int): Duration {
val xs = FloatArray(count) { rnd.nextFloat() }
val ys = FloatArray(count) { rnd.nextFloat() }
val zs = FloatArray(count) { rnd.nextFloat() }
val vectors = Array(count) { i -> DelegatedVector(xs, ys, zs, i) }
return measureTime {
for (vector in vectors) {
vector.normalize()
}
}
}
class PropertyVector(
val xs: FloatArray,
val ys: FloatArray,
val zs: FloatArray,
val index: Int
) {
var x: Float
get() = xs[index]
set(value) {
xs[index] = value
}
var y: Float
get() = ys[index]
set(value) {
ys[index] = value
}
var z: Float
get() = zs[index]
set(value) {
zs[index] = value
}
fun normalize() {
val l = x * x + y * y + z * z
x /= l
y /= l
z /= l
}
}
class InlinedPropertyVector(
val xs: FloatArray,
val ys: FloatArray,
val zs: FloatArray,
val index: Int
) {
var x: Float
inline get() = xs[index]
inline set(value) {
xs[index] = value
}
var y: Float
inline get() = ys[index]
inline set(value) {
ys[index] = value
}
var z: Float
inline get() = zs[index]
inline set(value) {
zs[index] = value
}
inline fun normalize() {
val l = x * x + y * y + z * z
x /= l
y /= l
z /= l
}
}
fun timeDodProps(count: Int): Duration {
val xs = FloatArray(count) { rnd.nextFloat() }
val ys = FloatArray(count) { rnd.nextFloat() }
val zs = FloatArray(count) { rnd.nextFloat() }
val vectors = Array(count) { i -> PropertyVector(xs, ys, zs, i) }
return measureTime {
for (vector in vectors) {
vector.normalize()
}
}
}
fun timeDodPropsInlined(count: Int): Duration {
val xs = FloatArray(count) { rnd.nextFloat() }
val ys = FloatArray(count) { rnd.nextFloat() }
val zs = FloatArray(count) { rnd.nextFloat() }
val vectors = Array(count) { i -> InlinedPropertyVector(xs, ys, zs, i) }
return measureTime {
for (vector in vectors) {
vector.normalize()
}
}
}
fun main() {
println("%10s%10s%10s%10s%10s%10s".format("Count", "OOP", "DOD", "DODd", "DODp", "iDODp"))
for (count in 1_000_000..10_000_000 step 1_000_000) {
val oopDurations = ArrayList<Long>()
val dodDurations = ArrayList<Long>()
val dodDelegationDurations = ArrayList<Long>()
val dodPropsDurations = ArrayList<Long>()
val dodPropsInlinedDurations = ArrayList<Long>()
for (iteration in 0..10) {
System.gc()
oopDurations += timeOop(count).toLong(DurationUnit.NANOSECONDS)
dodDurations += timeDod(count).toLong(DurationUnit.NANOSECONDS)
dodDelegationDurations += timeDodDelegated(count).toLong(TimeUnit.NANOSECONDS)
dodPropsDurations += timeDodProps(count).toLong(DurationUnit.NANOSECONDS)
dodPropsInlinedDurations += timeDodPropsInlined(count).toLong(DurationUnit.NANOSECONDS)
}
val oopDuration = oopDurations.average().toDuration(DurationUnit.NANOSECONDS)
val dodDuration = dodDurations.average().toDuration(DurationUnit.NANOSECONDS)
val dodDelegationDuration = dodDelegationDurations.average().toDuration(DurationUnit.NANOSECONDS)
val dodPropsDuration = dodPropsDurations.average().toDuration(DurationUnit.NANOSECONDS)
val dodPropsInlinedDuration = dodPropsInlinedDurations.average().toDuration(DurationUnit.NANOSECONDS)
println("%10s%10s%10s%10s%10s%10s".format(count, oopDuration, dodDuration, dodDelegationDuration, dodPropsDuration, dodPropsInlinedDuration))
}
}
The output from this code on my machine is:
Count OOP DOD DODd DODp iDODp
1000000 3.01ms 1.60ms 10.9ms 6.46ms 5.81ms
2000000 5.17ms 1.12ms 16.9ms 8.85ms 10.1ms
3000000 7.73ms 1.86ms 26.4ms 14.3ms 15.1ms
4000000 10.8ms 2.81ms 35.1ms 21.9ms 18.5ms
5000000 13.7ms 3.82ms 43.6ms 23.1ms 25.2ms
6000000 16.6ms 4.80ms 51.3ms 26.6ms 30.6ms
7000000 19.3ms 5.19ms 61.0ms 32.8ms 34.1ms
8000000 22.0ms 5.96ms 76.7ms 37.3ms 40.6ms
9000000 24.8ms 6.62ms 79.2ms 45.9ms 40.7ms
10000000 27.0ms 7.30ms 91.7ms 47.3ms 49.4ms
In summary I’m normalizing vectors:
- [OOP] As an array of Vector (OOP data class) instances - timeOpp
- [DOD] As X,Y and Z component float arrays - timeDod (4-5x faster than OOP )
- [DODd] As component arrays with a Vector class facade using delegates - timeDodDelegated/IntoArrayDelegate/DelegatedVector
- [DODp] As component arrays with a Vector class facade using property getters/setters - timeDodProps/PropertyVector
- [iDODp] As component arrays with a Vector class facade using inline property getters/setters - timeDodPropsInlined/InlinedPropertyVector
My question is, why are the timings for the property and especially the inlined property facades so far off the vanilla DOD timings? I would have expected the inlined facade to give very similar results…