Skip to content

Anti Patterns and Best Practices

Kai Burjack edited this page Oct 10, 2016 · 17 revisions

Notice: This article applies to the 1.8.0 release of JOML only. The next 1.8.1 release will contain performance improvements that will render some advices here wrong.


This page contains sub-optimal usage patterns which have been observed in various projects and forum posts using JOML. The items enumerated here should serve as best practice approaches when working with JOML and should be considered when striving for the best possible application runtime performance.

Use special-purpose transformations instead of mul()

Instead of using multiple Matrix4f instances to represent different transformations and multiplying them together via the generic matrix multiplication method Matrix4f.mul(), the special-purpose transformation methods should be used:

Wrong:

Matrix4f translation = new Matrix4f().translate(1, 2, 3);
Matrix4f rotation = new Matrix4f().rotateX((float) Math.toRadians(90));
Matrix4f scale = new Matrix4f().scale(2.0f);
Matrix4f result = new Matrix4f(translation).mul(rotation).mul(scale);

Right:

Matrix4f result = new Matrix4f()
  .translate(1, 2, 3)
  .rotateX((float) Math.toRadians(90))
  .scale(2.0f);

Apart from being easier on the eyes, this is also a big performance improvement. Firstly, only a single Matrix4f is allocated. Secondly the used transformation methods translate, rotateX and scale all perform an optimized matrix multiplication internally to apply their transformations to this, and therefore the general purpose mul method need not be used.

Use the best-fitting multiplication possible

This goes in hand with the above item. When you cannot directly apply a specific transformation such as translation or rotation to a matrix, but must actually multiply two matrices, you should still consider whether a matrix multiplication other than mul() applies. There are numerous special-case multiplication methods (beginning with mul...()) that assume certain properties about the matrices being multiplied. For example whenever you are dealing with affine matrices (which is the case for all affine transformations, like translation, rotation and scaling) you can use mulAffine().

Wrong:

Matrix4f viewMatrix = ...; // <- matrix built with translate/rotate/scale or lookAt
Matrix4f modelMatrix = ...; // <- some other affine matrix
Matrix4f result = new Matrix4f(viewMatrix).mul(modelMatrix);

Right:

Matrix4f viewMatrix = ...; // <- matrix built with translate/rotate/scale or lookAt
Matrix4f modelMatrix = ...; // <- some other affine matrix
Matrix4f result = new Matrix4f(viewMatrix).mulAffine(modelMatrix);

The following special-purpose matrix multiplication methods exist in Matrix4f:

  • mulAffine()
  • mulAffineR()
  • mulPerspectiveAffine()
  • mulOrthoAffine()

Make sure to read their JavaDocs to know exactly when any of these methods applies.

Know the cost of things

One noteworthy example of the above mentioned multiplication methods is mulPerspectiveAffine(). This method performs a matrix multiplication by assuming that this is a perspective projection matrix and the argument being an affine transformation matrix. This is a very typical scenario when you build a view-projection matrix. Typically, the method perspective() or setPerspective() would be used to build the perspective projection matrix and any following affine transformation, such as lookAt(), to apply the view transformation, like in the following example:

Matrix4f viewProjection = new Matrix4f()
  .setPerspective(...)
  .lookAt(...);

The above is perfectly fine, but when it comes to bare arithmetic performance, then in this case it is actually faster to do the following:

Matrix4f proj = new Matrix4f().setPerspective(...);
Matrix4f view = new Matrix4f().setLookAt(...);
Matrix4f viewProjection = new Matrix4f();
proj.mulPerspectiveAffine(view, viewProjection); // <- multiplication into 'viewProjection'

The reason is that Matrix4f.lookAt() cannot assume any special properties of the matrix it applies a lookAt-transformation to. Just like every other special-case transformation method (translate, rotate, scale, ...), also lookAt() will assume that the left-hand/this matrix contains any values possible.

For the very frequent use-case of applying a translation, rotation and scaling, for this very reason there exists the method Matrix4f.translationRotateScale() which is faster (about 1.7x faster) than the semantically equivalent form translation(...).rotate(...).scale(...).

In any case, it is always preferable to actually measure the performance with dedicated benchmarking tools, such as JMH.

Use non-post-multiplying methods when initializing a matrix

There are recurring cases when building a perspective projection matrix to use the Matrix4f.perspective() method.

Wrong:

Matrix4f proj = new Matrix4f().perspective(...);

Just like the affine tranformation methods mentioned above, perspective() also performs a post-multiplication. This can be avoided by instead using Matrix4f.setPerspective(...).

Right:

Matrix4f proj = new Matrix4f().setPerspective(...);

The same goes for ortho(), ortho2D(), orthoSymmetric(), frustum() and lookAt(). They all have corresponding non-multiplying set...() methods that are cheaper. An exception to this naming scheme are the affine transformation methods translate(), rotate...() and scale(). Their setter methods are called translation(), rotation...() and scaling(). The reason for this is that setTranslation() only updates the translation part of the matrix and does not set the whole matrix to a sole translation.

Use special-case matrix inversion methods

Just like the special-case matrix multiplication methods, there are also a number of matrix inversion methods that should be used over the general but costly Matrix4f.invert() method:

  • invertAffine()
  • invertAffineUnitScale() / invertLookAt()
  • invertPerspective()
  • invertFrustum()
  • invertOrtho()
  • invertPerspectiveView()

Make sure to read their JavaDocs to know exactly when any of these methods applies.

Favor destination arguments over allocating new objects

Even though Escape Analysis does a great job in modern JVMs, there is always the potential that it will not eliminate allocations in the hot path of your application. JOML supports and encourages a completely allocation-free programming style where every method supports a destination argument which will receive the computation result.

Wrong:

void frame() {
  Matrix4f m = new Matrix4f()
    .setPerspective(...)
    .lookAt(...)
    .translate(...).rotate(...).scale(...);
  Matrix4f inv = new Matrix4f(m).invert();
  // Upload to OpenGL
  ...
}

Right:

Matrix4f m = new Matrix4f();
Matrix4f inv = new Matrix4f();

void frame() {
  m.setPerspective(...)
   .lookAt(...)
   .translate(...).rotate(...).scale(...)
   .invert(inv); // <- invert into 'inv'
  ...
}

new Matrix4f() is already identity()

Calling Matrix4f.identity() on a newly instantiated Matrix4f object is unnecessary. The no-args constructor of Matrix4f will always initialize the matrix to identity. Calling identity() explicitly has however no performance impact.

Use direct NIO Buffers

The vector and matrix classes support writing to and reading their values from a java.nio.ByteBuffer (and typed views of them). JOML makes the assumption that the only reason why a client would want to transfer JOML objects from/to a NIO Buffer is in order to communicate with lower-level APIs and native code, such as OpenGL when using an OpenGL wrapper/binding library like JOGL or LWJGL.

Because of this, by default JOML only supports writing to and reading from direct NIO Buffers, as opposed to Buffers which are wrappers of Java primitive arrays.

Whenever it is necessary to transfer JOML object to/from a non-direct NIO Buffer, the JVM argument -Djoml.nounsafe must be used. This disables all efficient memory operations that are optimized for direct NIO Buffers and uses a much slower method of copying the data.

Therefore, in order to achieve maximum performance, only direct NIO Buffers should be used.