Using SIMD Vector Types in Swift

Using SIMD Vector Types in Swift

SIMD Vector Types is a feature added in Swift 5 that exposes Apple's <simd/simd.h> module to Swift, allowing you to calculate multiple results with a single instruction.

What's SIMD?

SIMD stands for Single Instruction, Multiple Data, a hardware instruction that can be used once to perform multiple operations at once. There's nothing you can do with SIMD that you can't do without it, but in the world of graphical applications where handling multiple values in the shape of vectors and matrices is important, usage of SIMD types can provide big performance boosts over calculating values normally.

Let's assume that we are handling the position of a character in a game that currently sits in the vector x: 2 | y: 4, and an explosion needs to apply some knockback to it.

If we assume that the knockback multiplies the character's position by two, we'll need to do two operations to end up with x: 4| y: 8:

(64bit integers)
0000000000000000000000000000000000000000000000000000000000000010
times
0000000000000000000000000000000000000000000000000000000000000010
-- and
0000000000000000000000000000000000000000000000000000000000000100
times
0000000000000000000000000000000000000000000000000000000000000010

While there's nothing particular wrong about doing two operations to achieve this, we can do better: a SIMD vector allows us to store multiple pieces of data in a single register by splitting our 64 bits of memory into smaller sections. In the case of our character, we can use a SIMD2 vector to store two different 32 bit values in a single 64 bit storage:

32 bits for (x: 2), and 32 bits for (4)
00000000000000000000000000000010 | 00000000000000000000000000000100

If we treat the knockback's power as another SIMD2, we can multiply the vectors together to get the character's new position in one go:

00000000000000000000000000000010 | 00000000000000000000000000000100
times
00000000000000000000000000000010 | 00000000000000000000000000000010

This allows us to perform a single hardware instruction that will end up containing the result of multiple operators -- in this case, a single instruction will allow us to get a new vector that contains the new values for both the x and y axes. Data-parallel problems like this have shown to gain between 2-10x better performance when using SIMD types, making them an invaluable tool when building high performance algorithms in the graphical applications world.

For example, a more common problem that benefits from SIMD is changing the brightness of an image -- instead of individually bumping the R, G and B channels of each pixel for each of the millions of pixels in a modern screen, we can treat the pixels' channels as a SIMD3 vector to reduce the number of instructions by around 3x.

SIMD in Swift

Starting from Swift 5, SIMD Vector types that range from 2 to 64 lanes are available for use. This is how the previous character example can be written in Swift using SIMD2<Int32>:

let character = SIMD2<Int32>(arrayLiteral: 2, 4)
let knockback = SIMD2<Int32>(arrayLiteral: 2, 2)
let result: SIMD2<Int32> = character &* knockback // 4,8

Vectors can be operated on through masked arithmetic operators, and individual values of the vectors can be accessed with the x,y,z,w properties, depending on how big the lanes are. If the SIMD has more than four lanes, you can use the lowHalf and highHalf to access smaller halves of the lanes until you can access individual values.

var character = SIMD2<Int32>()
character.x = 2
character.y = 4
var fourLanes = SIMD4<Int16>()
fourLanes.x = 2
fourLanes.y = 4
fourLanes.z = 8
fourLanes.w = 16
var eightLanes = SIMD8<Int8>()
eightLanes.lowHalf.x = 1
eightLanes.highHalf.w = 1

character &+ character // 4, 8
character &- character // 0, 0
character &* character // 4, 16
character / character // 1, 1

(Note that because only masked operators are supported, Swift will not protect you from overflowing values when using SIMD vectors.)

Besides arithmetic operators, SIMD types also supports comparison operators:

character == knockback // false

In addition to comparing the entire vector itself, SIMD types in Swift also support pointwise operators for comparisons. If instead of comparing everything we're looking to get the individual result for each comparison in the vector, we can prefix a dot . to the operator to get the pointwise version of it. For equality for example, we can use the .== pointwise operator to get a vector of booleans that indicates the result of each individual equality.

character .== knockback // SIMDMask<SIMD2<Int32>>(false, true)

Conclusion

Although the monstrous performance benefits can be only seen in the world of graphical applications, many problems can benefit from SIMD by being vectorized. Even when working with consumer applications, being careful about memory and CPU usage can bring big benefits to the user experience while teaching you more about software engineering.

Follow me on my Twitter (@rockbruno_), and let me know of any suggestions and corrections you want to share.

References and Good reads

The Swift Source Code
SIMD Proposal
SIMD Lecture from Princeton