-
Notifications
You must be signed in to change notification settings - Fork 115
Packed Weaver
Packed components offer a way to lay out components contiguously in memory. This oftentimes involves a lot of boilerplate and inconvenient obfuscation from too many getters/setters/mutator methods.
@PackedWeaver
offers a simple way of achieving a more memory-friendly layout without introducing any of the boilerplate usually associated with these kind of optimizations.
- Structs for artemis.
- Completely transparent. No extra code required.
- Works with android too.
- Only primitive types are permitted for fields. Objects aren't welcome.
- Zero-argument/default constructor.
- Must be added to entities with
Entity#createComponent(Class<Component>)
.
Assuming artemis-odb-maven-plugin
is properly configured, simply annotate
the component with @PackedWeaver
:
@PackedWeaver
public class ExampleComponent extends Component {
public float x;
public float y;
public void set(Vec2f vec) {
this.x = vec.x;
this.y = vec.y;
}
}
Direct field access is permitted - even encouraged. By not using getters/setters for
accessing component values, one can write exampleComponent.x += 4
instead of
exampleComponent.setX(exampleComponent.getX() + 4)
.
During compilation (post-compile), each class accessing woven components are rewritten to act on the generated methods. Ergo, getters/setters are still invoked, it's just happening behind the scenes.
Most IDE editors are tightly coupled with the compiler. Eclipse tends to bail out once it notices that fields have gone missing, but still thinks that classes are trying to invoke those.
At the moment, it is recommended to only perform weaving when building the full project
(ie, building from CLI or similar). Add the following to the pom.xml
:
<properties>
<artemis.enableArtemisPlugin>true</artemis.enableArtemisPlugin>
</properties>
<profiles>
<profile>
<id>ide</id>
<properties>
<artemis.enableArtemisPlugin>false</artemis.enableArtemisPlugin>
</properties>
</profile>
</profiles>
<build>
<plugins>
<plugin>
<groupId>net.onedaybeard.artemis</groupId>
<artifactId>artemis-odb-maven-plugin</artifactId>
<version>0.6.0</version>
<executions>
<execution>
<goals>
<goal>artemis</goal> <!-- weaving -->
<goal>matrix</goal> <!-- CDM report -->
</goals>
<configuration>
<enableArtemisPlugin>${artemis.enablePlugin}</enableArtemisPlugin>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Then configure the IDE to always run with the ide
profile. In eclipse, it's under project properties -> Maven -> Active Maven Profiles (comma separated)
.
Each ComponentMapper
has its own instance of the PackedComponent
which it is acting on. This can be problematic when two simultaneous references are required inside the same scope. To overcome this either create a 2nd mapper of the same type or tuck away an extra instance of the component type with ComponentMapper#get(entity, forceNewInstance)
Reusing the example from the top of the page:
@PackedWeaver
public class ExampleComponent extends Component {
public float x;
public float y;
public void set(Vec2f vec) {
this.x,= vec.x;
this.y = vec.y;
}
}
The above class looks something like this after it's been compiled:
public class ExampleComponent extends PackedComponent {
private int $stride;
private static final int $_SIZE_OF = 8; // float + float = 8 bytes
private static ByteBuffer $data = ByteBuffer.allocateDirect(128 * $_SIZE_OF);
@Override
protected PackedComponent forEntity(Entity e) {
this.$stride = $_SIZE_OF * e.getId();
if (($data.capacity() - $_SIZE_OF) <= $stride) $grow();
return this;
}
@Override
protected void reset() {
$data.putFloat($stride + 0, 0);
$data.putFloat($stride + 4, 0);
}
private static void $grow() {
ByteBuffer newBuffer = ByteBuffer.allocateDirect($data.capacity() * 2);
for (int i = 0, s = $data.capacity(); s > i; i++)
newBuffer.put(i, $data.get(i));
$data = newBuffer;
}
public float x() {
return $data.getFloat($stride + 0);
}
public float y() {
return $data.getFloat($stride + 4);
}
public void x(float value) {
$data.putFloat($stride + 0, value);
}
public void y(float value) {
$data.putFloat($stride + 4, value);
}
public void set(Vec2f vec) {
$data.putFloat($stride + 0, v.x);
$data.putFloat($stride + 4, v.y);
}
}
Take the results with a grain of salt. There are no guarantees that the benchmark is correct, but hopefully. If you know your way around benchmarking, feel free to take a look at the code.
All benchmarks run World#process
- at every 100th iteration an entity is deleted and then
recreated. All benchmarks have an additional system updating a single component per entity,
except baseline - which only iterates all entities without actually reading/updating the
component.
plain, pooled and packed work with normal com.artemis.Component',
com.artemis.PooledComponent' and 'com.artemis.PackedComponent' respectively.
# VM invoker: /home/junkdog/opt/apps/jdk1.7.0_55/jre/bin/java
Benchmark
c.a.ComponentTypeBenchmark.baseline_world avgt 15 10.821 0.138 us/op
c.a.ComponentTypeBenchmark.packed_world avgt 15 35.276 0.533 us/op
c.a.ComponentTypeBenchmark.plain_world avgt 15 30.739 0.195 us/op
c.a.ComponentTypeBenchmark.pooled_world avgt 15 28.426 0.043 us/op
Reflecting on the results:
- The struct-like approach yields the worst performance; supposedly, due to the overhead from method calls vs direct field access.
-
EntitySystems don't internally impose any ordering on its entities - processing entities in ascendingFixed, improved performance throughout.entityId
order would reduce cache misses. #87 - ByteBuffer - injected by
@PackedWeaver
- performs boundary checks when invokingget
andput
methods. Weaving withsun.misc.Unsafe
should perform better. Support for choosing betweenjava.nio.ByteBuffer
andsun.misc.Unsafe
is planned for a post-0.6.0 release: #86 -
@PooledComponent
is likely to perform much worse in the real world. Be wary.
To manually run the benchmarks - from artemis' root folder:
mvn clean install
java -jar artemis-benchmark/target/microbenchmarks.jar -t 1 -f 5
To get less noisy results, consider:
- Rebooting the computer
- Stop any non-vital background processes
- Disable all network adapters.
- Adam Martin's Data Structures for Entity Systems: Contiguous memory.
- Overview
- Concepts
- Getting Started
- Using
- More guides
- Plugins
- Game Gallery
- Tools and Frameworks
- API reference