Skip to content
Adrian Papari edited this page Jun 1, 2014 · 39 revisions

Weaving off-heap faux structs with @PackedWeaver

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.

At a glance

  • Structs for artemis.
  • Completely transparent. No extra code required.
  • Works with android too.

Component requirements

  • 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>).

Begin weaving

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.

Playing nice with IDE:s

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).

Referencing several components of the same type at once

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)

Internal representation reference

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);
    }
}

Performance

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 ascending entityId order would reduce cache misses. #87 Fixed, improved performance throughout.
  • ByteBuffer - injected by @PackedWeaver - performs boundary checks when invoking get and put methods. Weaving with sun.misc.Unsafe should perform better. Support for choosing between java.nio.ByteBuffer and sun.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.

Resources

Clone this wiki locally