3
$\begingroup$

During vertex processing with 4x4 matrices, we might stack multiple transformations like projection, model-world, world-camera, etc. by doing something like this:

$$v_{final} = T_N \cdot ... \cdot T_1 \cdot T_0 \cdot v$$

Now from the GLSL specs (section 5.1), I get that the operator associativity is "left to right". So this:

$$v_{final} = T_2 \cdot T_1 \cdot T_0 \cdot v$$

is equivalent to:

$$v_{final} = ((T_2 \cdot T_1) \cdot T_0) \cdot v$$

However, as matrix multiplication is associative, the result does not change if we write:

$$v_{final} = T_2 \cdot (T_1 \cdot (T_0 \cdot v))$$

While both versions yield the same result, the second version should (at least in theory) be much cheaper to compute since it incorporates 3 matrix-vector multiplications while the first version does 2 matrix-matrix multiplications and a final matrix-vector multiplication.

So my question is, does enforcing the order by adding extra parenthesis really boost performance or is the GLSL compiler usually smart enough to figure out, that the order of operations does not matter for the result and he can reorder the executions for performance even though the specs say "left to right" associativity?

Additional Note: I specially mentioned 4x4 matrices, because if you use 3x3 matrices or quaternions, you have to use parenthesis anyways because of the addition of the translations.

$\endgroup$
  • $\begingroup$ While both versions yield the same result <-- no, they really don't. Not with real float/double values which have limited precision. I can't say for certain that's the reason GLSL is specified that way however, which is why I'm not writing this as an answer. $\endgroup$ – Olivier May 24 at 18:06
  • $\begingroup$ @Olivier okay... this might be a reason that prevents the compiler from performing this optimization. So, apart from the fact that it is most likely not making any difference regarding the overall frame rate, using parenthesis should be faster. But you delivered another reason why it might make sense to use parenthesis to enforce order since less operations usually also mean less floating-point errors. $\endgroup$ – wychmaster May 24 at 18:24
  • $\begingroup$ The $Tv$ version should be faster just off the number of ops required (3*16 mults and 3*12 adds vs 2*64 + 16 mults and 2*48+12 adds). Since the operator associativity is left to right in glsl you can use $vT_0^TT_1^TT_2^T$ if you do not want to explicitly write parentheses, which will yield exactly the same result (up to round off error) as $T_2T_1T_0v$. You can send in your matrices pre-transposed. As already mentioned sending a premultiplied single matrix is even better (in your case it reduces the number of ops ~3 times). $\endgroup$ – lightxbulb Jun 25 at 10:06
  • $\begingroup$ @lightxbulb I know about this possibility and I am honestly considering it. However, since I have a strong engineering background I am used to the column vector (left multiply) notation and I am currently trying to figure out what bothers me more: Using a "weird" notation or "spamming" parenthesis. Thx for your input. $\endgroup$ – wychmaster Jun 25 at 10:36
3
$\begingroup$

As Olivier mentioned, it's not safe in general to apply algebraic optimizations to floating-point math, as floating point doesn't actually obey associativity rules due to roundoff error. $(a + b) - b$ will not be equal to $a$, if the intermediate result $(a + b)$ had to be rounded; nor will $a \cdot (b + c)$ be equal to $a \cdot b + a \cdot c$ in general, for the same reason.

That being said, shader compilers are typically very relaxed about floating-point math optimizations. They behave like using -ffast-math on a C++ compiler, where they make various simplifying assumptions—including that algebraic operations are associative—in order to optimize more aggressively. So it's not out of the question that a GLSL compiler could actually make the optimization you describe.

That said, GLSL compilers vary wildly in quality; there is no canonical GLSL compiler, every GPU vendor implements their own (for OpenGL at least—in the case of Vulkan there are a couple standardized options), and who knows what level of optimizations they might or might not apply under various circumstances. If you wish the code to be optimized in a particular way, better to write it explicitly that way.

One other point,

the second version should (at least in theory) be much cheaper to compute

It's only cheaper if you're transforming just a single vertex. If you want to transform thousands of vertices by the same matrix, it will be cheaper to multiply all the matrices together up front; then each vertex only needs to be transformed by a single 4×4 matrix instead of several.

| improve this answer | |
$\endgroup$
  • $\begingroup$ Thanks for your answer. Regarding your last point: That's true but I wasn't particularly interested in this specific case (vertex processing). It was just an example. For me, it was more interesting to know if I can simply write down a linear algebra equation and let the compiler figure out which execution order is the most performant (neglecting small floating-point variations) or if I have to enforce it. - Seems like I have to reactivate my brain before writing some equations in GLSL ;) $\endgroup$ – wychmaster May 25 at 7:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.