Skip to content

Commit

Permalink
perf: Avoid Record memory allocs, BucketULongArray to avoid array.cop…
Browse files Browse the repository at this point in the history
…yOf (#102)

perf: Use array implementation of CompId2ArchetypeMap, avoid null boxing using getOrElse
perf: Fully remove Record references (unnecessary memory allocs)
perf: Use BucketedULongArray to avoid array.copyOf when in ArrayTypeMap, useful when creating many entities at once
  • Loading branch information
0ffz authored Mar 21, 2024
1 parent defec11 commit 76c156b
Show file tree
Hide file tree
Showing 26 changed files with 389 additions and 259 deletions.
4 changes: 3 additions & 1 deletion geary-benchmarks/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ benchmark {
}

create("specific") {
include("Unpack6Benchmark")
// include("Unpack6Benchmark")
include("NewEntityBenchmark")
include("ManyComponentsBenchmark")
warmups = 1
iterations = 1
iterationTime = 3
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class ManyComponentsBenchmark {
}

@Benchmark
fun create1MilEntitiesWithUniqueComponentEach() {
fun createTenThousandEntitiesWithUniqueComponentEach() {
repeat(10000) {
entity {
addRelation<String>(it.toLong().toGeary())
Expand All @@ -38,5 +38,5 @@ class ManyComponentsBenchmark {

fun main() {
geary(TestEngineModule)
ManyComponentsBenchmark().create1MilEntitiesWithUniqueComponentEach()
ManyComponentsBenchmark().createTenThousandEntitiesWithUniqueComponentEach()
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,6 @@ class NewEntityBenchmark {
fun main() {
geary(TestEngineModule)
repeat(100) {
NewEntityBenchmark().create1MilEntitiesWith6Components()
NewEntityBenchmark().create1MilEntitiesWith6ComponentsWithoutComponentIdCalls()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ fun main() {
Unpack6Benchmark().apply {
setUp()
repeat(10000) {
unpack1of6CompNoDelegate()
// unpack6of6CompNoDelegate()
// unpack1of6CompNoDelegate()
unpack6of6CompNoDelegate()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.mineinabyss.geary.datatypes

private const val bucketSize: Int = 1024

class BucketedULongArray() {
private val buckets = mutableListOf<LongArray>()
var maxSupportedSize = 0
private set
var size = 0
private set

val lastIndex get() = size - 1

operator fun get(index: Int): ULong {
val bucketIndex = index / bucketSize
val bucket = buckets[bucketIndex]
return bucket[index % bucketSize].toULong()
}

fun ensureSize(including: Int) {
var maxSupportedSize = maxSupportedSize
while (including >= maxSupportedSize) {
buckets.add(LongArray(bucketSize))
maxSupportedSize += bucketSize
}
this.maxSupportedSize = maxSupportedSize
}

operator fun set(index: Int, value: ULong) {
val bucketIndex = index / bucketSize
ensureSize(index)
val bucket = buckets[bucketIndex]
if (index >= size) size = index + 1
bucket[index % bucketSize] = value.toLong()
}

fun add(value: ULong) {
val index = size
set(index, value)
}

fun getAll(): ULongArray {
return ULongArray(size) { get(it) }
}

fun removeLastOrNull(): ULong? {
if (size == 0) return null
return get(lastIndex).also {
size--
if (size % bucketSize == 0) buckets.removeLast()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ value class Entity(val id: EntityId) {
* @return Whether the component was present before removal.
*/
inline fun <reified T : Component> remove(): Boolean =
remove(componentId<T>()) || remove(componentId<T>() and ENTITY_MASK)
remove(componentId<T>())

/** Removes a component whose class is [kClass] from this entity. */
fun remove(kClass: KClass<*>): Boolean =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.mineinabyss.geary.datatypes
import kotlinx.serialization.Polymorphic

typealias GearyEntityType = EntityType
typealias GearyRecord = Record
typealias GearyRelation = Relation

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@ class IdList {

fun add(value: ULong) {
if (size == backingArr.size) {
backingArr = backingArr.copyOf(size * growFactor)
grow()
}
backingArr[size++] = value
}

fun grow(){
backingArr = backingArr.copyOf(size * growFactor)
}

fun removeLastOrNull(): ULong? {
if (size == 0) return null
return backingArr[--size]
Expand All @@ -31,7 +35,21 @@ class IdList {
return backingArr[--size]
}

internal fun removeAt(index: Int) {
if (index == -1) return
// replace with last
backingArr[index] = backingArr[--size]
}

fun getEntities(): Sequence<Entity> {
return backingArr.asSequence().take(size).map { it.toGeary() }
}

fun indexOf(value: ULong): Int {
var n = 0
val size = size
while (n < size)
if (backingArr[n++] == value) return n - 1
return -1
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
package com.mineinabyss.geary.datatypes

//can't make const because of the shl
val NO_ROLE: ULong = 0uL
val RELATION: ULong = 1uL shl 63
val HOLDS_DATA: ULong = 1uL shl 62
const val NO_ROLE: ULong = 0uL
const val RELATION: ULong = 0x8000000000000000uL // 1 shl 63
const val HOLDS_DATA: ULong = 0x4000000000000000uL // 1 shl 62
//4
//5
//5
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,62 @@
package com.mineinabyss.geary.datatypes.maps

import com.mineinabyss.geary.datatypes.BucketedULongArray
import com.mineinabyss.geary.datatypes.Entity
import com.mineinabyss.geary.datatypes.Record
import com.mineinabyss.geary.engine.archetypes.Archetype

class ArrayTypeMap : TypeMap {
private val map: ArrayList<Record?> = arrayListOf()

open class ArrayTypeMap : TypeMap {
@PublishedApi
internal val archList = arrayListOf<Archetype>()

// private val map: ArrayList<Record?> = arrayListOf()
// private var archIndexes = IntArray(10)
// private var rows = IntArray(10)
@PublishedApi
internal var archAndRow = BucketedULongArray()
var size = 0

// We don't return nullable record to avoid boxing.
// Accessing an entity that doesn't exist is indicative of a problem elsewhere and should be made obvious.
override fun get(entity: Entity): Record = map[entity.id.toInt()]
?: error("Tried to access components on an entity that no longer exists (${entity.id})")
// override fun get(entity: Entity): Record {
// val info = archAndRow[entity.id.toInt()]
// return Record(
// archList[(info shr 32).toInt()],
// info.toInt()
// )
// }
open fun getArchAndRow(entity: Entity): ULong {
return archAndRow[entity.id.toInt()]
}

override fun set(entity: Entity, record: Record) {
override fun set(entity: Entity, archetype: Archetype, row: Int) {
val id = entity.id.toInt()
if (map.size == id) {
map.add(record)
return
}
if (contains(entity)) error("Tried setting the record of an entity that already exists.")
while (map.size <= id) map.add(null)
map[id] = record
archAndRow[id] = (indexOrAdd(archetype).toULong() shl 32) or row.toULong()
}

fun indexOrAdd(archetype: Archetype): Int {
if (archetype.indexInRecords != -1) return archetype.indexInRecords
val index = archList.indexOf(archetype)
archetype.indexInRecords = index
return if (index == -1) {
archList.add(archetype)
archList.lastIndex
} else index
}

override fun remove(entity: Entity) {
map[entity.id.toInt()] = null
val id = entity.id.toInt()
archAndRow[id] = 0UL
}

override operator fun contains(entity: Entity): Boolean {
val id = entity.id.toInt()
return map.size > id && map[id] != null
return id < archAndRow.size && archAndRow[id] != 0uL
}


inline fun <T> runOn(entity: Entity, run: (archetype: Archetype, row: Int) -> T): T {
val info = getArchAndRow(entity)
return run(archList[(info shr 32).toInt()], info.toInt())
}
}
Original file line number Diff line number Diff line change
@@ -1,50 +1,66 @@
package com.mineinabyss.geary.datatypes.maps

import com.mineinabyss.geary.datatypes.GearyComponentId
import com.mineinabyss.geary.datatypes.IdList
import com.mineinabyss.geary.engine.archetypes.Archetype

/**
* Inlined class that acts as a map of components to archetypes. Uses archetype ids for better performance.
*/
expect class CompId2ArchetypeMap() {
operator fun get(id: GearyComponentId): Archetype?
operator fun set(id: GearyComponentId, archetype: Archetype)

fun entries(): Set<Map.Entry<ULong, Archetype>>

fun clear()

fun remove(id: GearyComponentId)

operator fun contains(id: GearyComponentId): Boolean

fun getOrSet(id: GearyComponentId, put: () -> Archetype): Archetype

val size: Int
}

class CompId2ArchetypeMapViaMutableMap {
val inner: MutableMap<ULong, Archetype> = mutableMapOf()
operator fun get(id: GearyComponentId): Archetype? = inner[id]
class CompId2ArchetypeMap {
// val inner = Long2ObjectArrayMap<Archetype>()
val ids = IdList()
val values = mutableListOf<Archetype>()
// actual operator fun get(id: GearyComponentId): Archetype? =
// values[entries.indexOf(id).also { if (it == -1) return null }]
operator fun set(id: GearyComponentId, archetype: Archetype) {
inner[id] = archetype
val index = ids.indexOf(id)
if (index == -1) {
ids.add(id)
values.add(archetype)
} else {
values[index] = archetype
}
}

fun entries(): Set<Map.Entry<ULong, Archetype>> = inner.entries

fun remove(id: GearyComponentId) {
inner.remove(id)
val index = ids.indexOf(id)
if (index != -1) {
ids.removeAt(index)
values[index] = values[values.lastIndex]
values.removeLast()
}
}

fun clear() {
inner.clear()
ids.size = 0
values.clear()
}

inline fun forEach(action: (ULong, Archetype) -> Unit) {
for(i in 0 until ids.size) {
action(ids[i], values[i])
}
}

val size: Int get() = inner.size
val size: Int get() = ids.size

operator fun contains(id: GearyComponentId): Boolean = ids.indexOf(id) != -1

operator fun contains(id: GearyComponentId): Boolean = inner.containsKey(id)
inline fun getOrElse(id: GearyComponentId, defaultValue: () -> Archetype): Archetype {
val index = ids.indexOf(id)
return if (index == -1) defaultValue() else values[index]
}

fun getOrSet(id: GearyComponentId, put: () -> Archetype): Archetype {
return inner[id] ?: put().also { inner[id] = it }
inline fun getOrSet(id: GearyComponentId, put: () -> Archetype): Archetype {
val index = ids.indexOf(id)
if (index == -1) {
val arc = put()
ids.add(id)
values.add(arc)
return arc
}
return values[index]
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package com.mineinabyss.geary.datatypes.maps

import com.mineinabyss.geary.datatypes.Entity
import com.mineinabyss.geary.datatypes.Record
import com.mineinabyss.geary.engine.archetypes.Archetype
import kotlinx.atomicfu.locks.SynchronizedObject
import kotlinx.atomicfu.locks.synchronized

class SynchronizedTypeMap(private val map: TypeMap) : TypeMap {
class SynchronizedArrayTypeMap() : ArrayTypeMap() {
private val lock = SynchronizedObject()

override fun get(entity: Entity): Record = synchronized(lock) { map[entity] }
override fun set(entity: Entity, record: Record) = synchronized(lock) { map[entity] = record }
override fun remove(entity: Entity) = synchronized(lock) { map.remove(entity) }
override fun contains(entity: Entity): Boolean = synchronized(lock) { map.contains(entity) }
override fun getArchAndRow(entity: Entity): ULong {
return synchronized(lock) { super.getArchAndRow(entity) }
}
override fun set(entity: Entity, archetype: Archetype, row: Int) {
synchronized(lock) { super.set(entity, archetype, row) }
}

override fun remove(entity: Entity) = synchronized(lock) { super.remove(entity) }
override fun contains(entity: Entity): Boolean = synchronized(lock) { super.contains(entity) }
}
Loading

0 comments on commit 76c156b

Please sign in to comment.