Welcome back to this series exploring the many extensions the x86 architecture has seen over the past decades. In this installment of the series, we will be looking at the successor to MMX: Streaming SIMD Extensions, or SSE for short. Most of these instructions are SIMD (as their name implies), which stands for Single Instruction Multiple Data. In brief, SIMD instructions are similar to the ones we’ve covered in the MMX article: an instruction can possibly work on multiple data groups.

SSE was introduced in 1999 with Intel’s Pentium III soon after Intel saw AMD’s “3DNow!” extension (we will cover this extension in a future installment, but right now I lack access to an AMD machine that I could use 🙂). A question arises naturally: SSE wasn’t the first SIMD set that Intel has introduced to the x86 family of processors, so why did Intel create a new extension set? Unfortunately, MMX had two major problems at the time. First, the registers it “introduced” were aliases of previously existing registers (amusingly, this was touted as an advantage for a while because of the easier context switching), this meant that floating points and MMX operations couldn’t coexist. Second, MMX only worked on integers, it had no support for floating points which was an increasingly important aspect of 3D computer graphics. SSE adds dozens of new instructions that operate on an independent register set and a few integer instructions that continue to operate on the old MMX registers.

(A slight note before we start: In this article “SSE” refers to the very first SSE extension introduced by Intel. In future installments of this series, we will explore SSE2, SSE3, SSSE3, SSE4 and SSE4.1, but here we focus on “SSE1”.)

Do you have SSE?

As with all instruction set extensions, there is a chance that your CPU does not have it. The chances are once again pretty slim with SSE, given its age, but it’s always interesting to see how one can feel sure about its CPU’s support for SSE.

On Linux:

$ cat /proc/cpuinfo | grep -wq sse && echo “SSE available”  || echo “SSE not available”

On OS X/macOS:

$ sysctl machdep.cpu.features | grep -wq SSE && echo “SSE available”  || echo “SSE not available”

Alternatively, CPUID offers a way to gather this information on bare-metal or in an OS-agnostic way. SSE is indicated by CPUID leaf 1, EDX bit 25:

.text
.globl _is_sse_available
_is_sse_available:
    pushq   %rbx

    movq    $1, %rax
    cpuid
    movq    %rdx, %rax
    shrq    $25, %rax
    andq    $1, %rax

    popq    %rbx
    ret

Once you are satisfied that your CPU allows for SSE instructions, it’s time to dive in to the specifics of SSE!

Registers

Since SSE introduces actual, new registers (in contrast with its predecessor), I think it’s useful to have a quick glance at them. SSE added eight, 128-bit registers named: %xmm0, %xmm1, ..., %xmm7. (Amusingly, xmm is the reverse of mmx which is the name of the MMX registers, I assume this is meant as a pun, but I couldn’t find a source confirming) In stark contrast with MMX, SSE does not allow for multiple data types. Each XMM register can hold four, 32-bit single-precision floating points, while MMX could hold different widths of integers.

%xmm0, %xmm1, ..., %xmm7:
*- - - - - - - - -*- - - - - - - - -*- - - - - - - - -*- - - - - - - - -*
| 32-bit SP float | 32-bit SP float | 32-bit SP float | 32-bit SP float |
*- - - - - - - - -*- - - - - - - - -*- - - - - - - - -*- - - - - - - - -*
|                            128-bit value                              |
*- - - - - - - - -*- - - - - - - - -*- - - - - - - - -*- - - - - - - - -*

In this figure, each line represents a data type that can be in the XMM register with SSE. I’ve put the “128-bit value” in the figure, since if you only load data into the register and not issue any floating point operation, then it can be potentially any unstructured data. However, when using floating points only the four, single-precision floating points are supported as data in the register. Unstructured data can potentially cause exceptions to happen.

To control the state of some operations, an additional control and status register is added, dubbed MXCSR. This register cannot be accessed using the mov family of instructions, rather SSE adds two new instructions that allow the register to be loaded and stored, LDMXSCR & STMXSCR. The figure shows its layout and then explains its usage within the SSE environment.

The MXCSR register

Bits 0-5 in MXCSR are flags that show that a certain type of floating-point exception occurred, they are also sticky meaning that the user (or the OS) has to reset them manually after an exception, otherwise they’ll stay set forever. Bits 7-12 are masking bits, they can be used to used to stop the CPU from issuing an exception when certain conditions pertaining to the specific exception are met, in which case the processor will return a value (qNaN, sNaN, definite integer or one of the source operands; see [1] for more details).

For more information on the specific meanings of the registers, look at [1], Chapter 10.2.3.

Instructions

Now that we have covered the registers introduced in the SSE extension, let’s have a look at what new instructions have Intel added and their implications. To utilize SSE to its fullest extent, the very first step to be taken is to move data into the new XMM registers, SSE offers a couple instructions, out of which the following (movaps & movups) are the most common:

# Create a memory location with four single-prec floats
vector0: .dq 3.14, 2.71, 1.23, 4.56
scalar0: .dd 1234
vector1: .dq 3.62, 6.73, 8.41, 9.55

movaps vector0, %xmm0
movups vector1, %xmm1

movaps stands for MOVe Aligned Packed Single Precision Float, and movups stands for the same, but Unaligned. The distinction between aligned and unaligned access is important, and generally developers should aim for aligned access whenever possible for better overall performance.

Now that we have managed to move data into an XMM register, let’s do something with it. A trivial example and one that we explored previously is some simple vector manipulation:

# assuming vector0 and vector1 from the previous snippet

movaps vector0, %xmm0
movups vector1, %xmm1

addps %xmm0, %xmm1 # ADD Packed Single precision float
subps %xmm0, %xmm1 # undo previous operation
maxps %xmm0, %xmm1 

maxps is a very handy instruction: it compares each of the four single-precision floats in the XMM registers and then moves the larger float into the destination operand (it can be either a register like %xmm1 or a 128-bit memory location). This instruction alone can save a large chunk of cycles by avoiding a loop and many cmp and branch instructions.

An other interesting aspect of the SSE extensions are cacheability controls. The application programmer can now tell the CPU that some memory is “non-temporal”, that is it won’t be needed in the near future so do not pollute the cache with it, like so:

movntps %xmm0, vector0

The reverse (i.e., if the programmer knows that a certain memory location will be needed in the near future) can also be signaled to the processor using the PREFETCH family of instructions:

Instruction Pentium III Pentium 4/Xeon Temporal?
prefetch0 L2 or L1 L2 Temporal
prefetch1 L2 L2 Temporal
prefetch2 L2 L2 Temporal
prefetchnta L1 L2 Non-temporal

Conclusion

The next extension we will be looking at will be the SSE2 extension set that builds on the foundations of SSE and MMX to deliver better performance. Starting with the new installment, we will introduce benchmarks, too. In the meantime, have a look at a cache of examples in the GitHub Repo for the series! Until next time!

References

1: Intel IA-32 Software Development Manual, Chapter 11.5.2: SIMD Floating-Point Exception Conditions