DOTS//CORE local · not synced

m03 · Burst · reading · 8 min

Unity.Mathematics — writing in vector width on purpose

Vectors as first-class values

The last two lessons framed vectorization as something the compiler might do to your loop if you keep it clean. Unity.Mathematics lets you stop hoping. It provides types — float2, float3, float4, int4, float4x4, quaternion — that are SIMD vectors, and arithmetic on them maps to vector instructions directly:

using Unity.Mathematics;

float4 a = new float4(1f, 2f, 3f, 4f);
float4 b = new float4(10f, 20f, 30f, 40f);
float4 c = a + b;   // one vector add — four lanes, one instruction

That a + b is not a loop the compiler has to analyze and prove vectorizable. It’s four-wide arithmetic by construction; under Burst it compiles to a single vaddps-class instruction. When you express your math in these types, you are handing the compiler data already in vector shape — the most reliable path to vector code, because there’s nothing to infer. The types make the SIMD explicit rather than emergent.

This reframes a lot of numeric code. A position update on a float3 position and float3 velocity is one vector multiply-add, not three scalar ones. A dot product, a normalize, a matrix transform — each is a handful of vector ops on these types instead of a hand-written scalar loop the compiler has to rescue. You write the math at the width the hardware runs it.

Why it looks like shader code

If you’ve written HLSL, Unity.Mathematics will feel familiar on purpose. The types (float4, float4x4), the swizzles (v.xyz, v.xxzz), the function names (math.dot, math.normalize, math.lerp, math.saturate), and the column-vector math conventions all mirror shading languages. This isn’t cosmetic. Gameplay and graphics math are the same math, and a team that expresses both in the same vocabulary carries fewer translation errors between CPU simulation and GPU rendering, and reasons about both with one mental model. The math.* free functions and the lower-case type names are a deliberate break from typical C# naming precisely to keep that shader-shaped consistency intact.

The swizzles are worth calling out as more than convenience:

float3 p = transform.Position;
float2 groundPlane = p.xz;      // pull two lanes, no scalar shuffling
float3 flipped = p.zyx;        // reorder lanes in one operation

A swizzle is a lane operation the hardware does directly, not a set of scalar copies you’re writing by hand. Reaching into .x, .y, .z individually and recombining them is often slower and less clear than the swizzle that expresses the same rearrangement in vector terms.

The choices that are actually about speed

Two everyday decisions with these types are performance decisions wearing the costume of style:

float3 vs float4. A float3 is three lanes; the hardware vector is four (or eight). Sometimes a float3 is padded to float4 width anyway, so the fourth lane is “free” — and sometimes an algorithm is cleaner and faster expressed as a float4 that uses all four lanes than as a float3 that wastes one. This isn’t a rule to memorize; it’s an awareness that lane count and hardware width interact, and that the “natural” 3-component choice isn’t automatically the fastest one. Measure when it’s hot.

math.* intrinsics over hand-rolled scalar math. math.length(v), math.distance(a, b), math.normalize(v), math.mad(a, b, c) (a fused multiply-add) exist because each maps to an efficient vector sequence, often a single hardware instruction. Writing out sqrt(x*x + y*y + z*z) by hand gives the compiler a scalar expression to rescue; calling math.length hands it the vector idiom directly. The intrinsics are the vocabulary the optimizer recognizes.

cache line · 64 B fetched 64 B useful · 100% of the bandwidth you paid for a float4 occupies one 128-bit lane group; operating on it is one vector instruction — the math type is the SIMD register made into a value you can name

What this buys you

You can now write numeric code that is vectorized on purpose — in the same vocabulary as your shaders — and you know the two quiet performance choices (lane count, intrinsic vs hand-rolled) that hide inside these types. The final lesson of the module closes the loop on verification: how to read the assembly Burst actually generated and how to benchmark it without fooling yourself, so “I think it vectorized” becomes “I can see the vmulps and I measured the speedup.”

m03.l05