SIMD in the 90s: Programming Intel's Pentium MMX

If you programmed PCs in the late 90s, you probably remember the 'Intel Inside Pentium with MMX' stickers. MMX was, of course, a big marketing buzzword, but it was also Intel's major attempt at bringing SIMD (Single Instruction, Multiple Data) instructions to mainstream desktop CPUs.

verlet physics

It's a rite of passage of every student that completes our software rendering module to start asking questions and exploring the topic of SIMD (Single Instruction, Multiple Data).

Today we have AVX-512, NEON, SVE, and GPUs with thousands of cores. These modern CPUs & extensions are the result of years of exploration, learning, market pressure, and they all bring with them a lot of history/retro-compatibility noise. As always, I like to look back and discuss different technologies by analyzing what was happening when they were just starting out. Back in the late 90s, many programmers were learning how to squeeze a few extra frames per second out of a Pentium MMX using hand-writing assembly for the first time.

Pentium MMX
Intel Pentium with MMX tech

Let's travel back to 1997 and see what programming MMX looked like!

What was MMX?

MMX (MultiMedia eXtensions) was introduced with the Pentium MMX processor in 1997 for improved multimedia experience.

It was a set of 57 additional instructions built into the Pentium chip for enhanced performance. The CPU had to be switched into MMX mode, which turned the first 64 bits of the x86 eight 80-bit floating point registers into MMX registers.

MMX registers
Eight MMX registers (64 bits long)

The idea motivating the creation and the use of MMX was simple: instead of processing one integer at a time, the CPU processes several integers packed inside a single 64-bit register simultaneously.

For multimedia software (image processing, audio mixing, video playback, and even games) this could provide significant speedups.

For example, instead of adding eight bytes individually:

10 + 20
30 + 40
50 + 60
70 + 80
...

MMX lets us perform all eight additions with a single instruction.

That's parallelism! And that's the main idea behind SIMD.

What is SIMD?

"Single Instruction, Multiple Data" is a type of parallel processing technique. It describes computers with multiple processing elements that perform the same operation on multiple data points simultaneously.

SIMD single instruction multiple data

SIMD can be internal (part of the hardware design) and it can be directly accessible through an instruction set architecture (ISA). In the case of the MMX, Intel extended their x86 instruction set to include the new SIMD instructions.

Was MMX the First SIMD?

No, MMX was definitely not the first SIMD, although it was hugely important in making SIMD mainstream on x86 PCs.

We must remember that many advances and innovations in computer technology predates the personal computer era. SIMD is no different, and it predates Intel's MMX by decades. The general idea of using one instruction operating on multiple data elements goes back much further than the Pentium.

One famous early example is the ILLIAC IV supercomputer project from the 60s. The ILLIAC IV was the first massively parallel computer. The system was originally designed to have 256 64-bit floating-point units (FPUs) and four central processing units (CPUs) able to process 1 billion operations per second.

ILLIAC IV computer
ILLIAC IV photo by Sascha Pohlflepp CC-BY-2.0

In summary, the ILLIAC IV used a large number of processing elements operating under a single instruction stream, making it an important ancestor of modern SIMD/vector architectures.

ILLIAC IV computer
ILLIAC IV array overview

There were also array-processing machines from companies such as CDC, Cray, and others throughout the 60s and 80s.

SIMD vs Array Processors

It's important to point out that Vector Processing and SIMD are not quite the same thing. The distinction is mostly about how the multiple data elements are presented to the processor and how the hardware executes them.

SIMD operations are fundamentally tied to the width of the registers.

SIMD fixed-sized elements

The Cray-1 from 1976 was characterized by being a vector processor. It had eight 64-element vector registers, each holding 64-bit values.

Cray-1 supercomputer
Cray-1 supercomputer

Similar to what we saw with SIMD, the Cray-1 did not require 64 separate ADD instructions. So, given that this is obviously SIMD-like, why don't we simply call it SIMD? Because of an architectural distinction. SIMD operates on fixed-width registers, while the vector register in the Cray-1 was more like a container for a sequence.

Vector processor operating on the entire array

Vector processors are usually characterized by the presence of a SET VECTOR SIZE instruction. This vector functional unit can process the elements over multiple cycles.

The Cray-1 had a VL (vector length) register and instructions that could set that length. The vector functional units then processed that many elements over multiple cycles. All the remaining extra elements of the array were simply not used/cycled.

VL register Cray-1

The VL register of the Cray-1 was 7 bits, even though the architecture only used values up to 64 (2⁶), given that Cray-1 vector registers contained up to 64 elements. Also, a small detail that is worth mentioning is the fact that the Cray-1 interpreted VL=0 as 64, processing all the 64 elements of the vector register.

MMX's Real Significance

So, as you can see, SIMD was not invented by Intel. Its real significance was putting a relatively accessible SIMD instruction set into mainstream x86 PCs. That was the first time most of us (personal computer enthusiasts) were able to get our hands dirty with this type of technology, and that is why MMX gets so much attention in retro-computing discussions. It wasn't the invention of SIMD, but it was one of the moments when SIMD crossed over from specialized/high-performance computing into ordinary desktop software development.

MMX Registers

As we saw before, one clever design decision was that Intel didn't add a brand-new register file. Instead, MMX re-used the floating-point stack registers.

MM0
MM1
MM2
MM3
MM4
MM5
MM6
MM7

Each MMX register is 64 bits wide. Internally, the MMX registers were aliases of the x87 floating-point registers. These underlying floating-point registers were 80 bits long, but MMX accessed only the lower 64 bits.

MMX registers
Eight MMX registers (64 bits long)

This decision saved silicon, but it also created an important limitation:

SIMD and floating-point operations were mutually exclusive. Since MMX is basically "taking over" the x87 floating-point registers, MMX multimedia operations and floating-point operations could not coexist.

It also created an important rule:

Don't mix MMX and x87 floating-point code without cleaning up first.

Don't worry... we'll come back and talk more about these limitations very soon.

Packed Integer Data

A single MMX register could hold different layouts:

Pentium Register Layout

The same register could represent any of these layouts depending on the instruction you used.

Your First MMX Program

Suppose we want to add 8 pixels together (assuming no compiler optimization):

for (int i = 0; i < 8; i++) {
  dest[i] = src1[i] + src2[i];
}

An MMX version would look like:

movq  mm0, [src1]  ; loads eight bytes
movq  mm1, [src2]  ; loads eight bytes
paddb mm0, mm1     ; performs eight byte additions simultaneously
movq  [dst], mm0   ; store the result

One instruction replaced eight separate additions!

Saturating Arithmetic with MMX

Another really great feature of MMX was its ability to perform "saturating" arithmetic.

When we add two values together, normal integer arithmetic wraps around.

250 + 20   // = 270, which wraps around to 14

The above addition exceeds 255 and will normally wrap around to 14.

This 'wrap around' nature of integer arithmetic can be cumbersome to deal with, especially when working with graphics. If we are adding pixel values together, we want maximum brightness to stay maximum brightness!

MMX provides saturation for cases like this. The instruction is simply:

paddusb mm0, mm1

The us stands for unsigned saturation, and clamps the maximum result value to 255.

250 + 20   // = 255

Likewise:

10 - 40    // = 0

This made brightness adjustments and blending operations much simpler.

Comparing Pixels with MMX

It's natural to think of MMX's power applied to graphics and other multimedia tasks. For example, let's suppose we want to create a mask where every value greater than another becomes white.

MMX offers comparison instructions:

pcmpgtb mm0, mm1

Each byte becomes either 0xFF or 0x00.

These masks are useful when implementing threshold filters, sprite transparency, or collision logic.

Shifting Packed Values with MMX

If you ever took a course with us, you know how often we use bitshifting to multiply & divide values by 2. Given how expensive multiplication and (especially) division instructions were in older CPUs, shifting bits left & right was a faster way of multiplying and dividing values by powers of 2.

MMX offers a way of shifting packed values simultaneously:

psllw mm0, 1  ; multiply every 16-bit value by two
psrlw mm0, 1  ; divide by two

Again, all packed values are shifted at the same time!

Multiplication with MMX

Need to multiply packed values by something other than 2? Not a problem. MMX also supports packed multiplication.

pmullw  mm0, mm1

Each pair of 16-bit integers is multiplied independently. This became useful for:

  • Audio volume scaling
  • Image convolution
  • Color transformations
  • Fixed-point math

Remember how one of the big aspects of MMX was that it "took over" the floating-point registers of the x87? That means there was no floating-point SIMD with MMX, so MMX programs relied heavily on fixed-point arithmetic. Real floating-point SIMD for x86 didn't arrive until SSE (Streaming SIMD Extensions) several years later with the Pentium III series.

A Practical Example

Let's say you are responsible for writing a small routine to brighten an image. In this image, every pixel is one byte long. Without MMX, our code might look something like this:

int intensity = 20;
for (int i = 0; i < WIDTH * HEIGHT; i++) {
  pixels[i] += intensity;
  if (pixels[i] > 255) {
    pixels[i] = 255;
  }
}

Using MMX, the iterations over chunks of pixels would look something like:

section .data
    ; we put 8 bytes in memory with the value 20 (or 0x14 in hex)
    intensity: times 8 db 20 ; 14 14 14 14 14 14 14 14

section .text
    ; esi = pointer to pixels
    ; ecx = number of 8-pixel chunks

loop:
    movq    mm0, [esi]          ; load 8 pixels
    movq    mm1, [intensity]    ; load eight 20s
    paddusb mm0, mm1            ; saturated addition
    movq    [esi], mm0          ; store 8 pixels

    add     esi, 8              ; next 8 pixels
    dec     ecx
    jnz     loop

    emms                        ; leave MMX state

With MMX, eight pixels are processed at once. This was exactly the sort of optimization that graphics libraries loved.

The EMMS Instruction

As we just learned, MMX "borrowed" the floating-point registers from the original x86 architecture.

That means when you're finished using MMX, you must tell the CPU that you're done.

emms

This clears the MMX state and allows x87 floating-point instructions to work correctly again.

If we forget to call EMMS, strange floating-point bugs may appear later in the program.

Trust me, every MMX programmer learned this lesson sooner or later.

Did Games use MMX?

During the late DOS and Windows 95 era, CPUs were often the bottleneck.

MMX found its way into routines such as:

  • Software texture mapping
  • Sprite composition
  • Alpha blending
  • Image scaling
  • MP3 decoding
  • MPEG video playback
  • Audio mixing

Kyle Freeman, the main programmer behind Novalogic's Comanche 3, often mentions how the game made heavy use of MMX instructions for both audio & graphics.

Kyle Freeman Comanche 3 MMX
Kyle Freeman's tweet on Comanche 3 use of MMX

Examples of games that either required or benefited from MMX enhancements in their software renderer were Eraser Turnabout, POD, and Extreme Assault. I have read rumors of a Tomb Raider patch that added MMX enhancements to the game for lighting and textures, but I personally never saw any reliable source to confirm this version existed. Let me know if you do!

Even though Quake and Quake II are often used as examples of games that benefited from MMX technology, it's important to point out that Quake's famous software renderer predates MMX. The original Quake was released in 1996, while Intel introduced the Pentium MMX in 1997. So, Quake's fast performance was achieved primarily through techniques like fixed-point arithmetic, register allocation, loop optimization, lookup tables, and hand-written x86; not by relying on MMX.

Many games and other multimedia applications shipped with optional MMX code paths that were selected if the CPU supported the new instructions. Of course, developers usually maintained a non-MMX version as well, since millions of users still owned older Pentium processors.

Detecting MMX Support

Software could not simply assume MMX was available. The common approach was to use the CPUID instruction.

CPUID is an x86 CPU instruction used to ask the processor what it supports and to retrieve information about the CPU.

mov eax, 1
cpuid               ; retrieve information about the CPU
test edx, 1 << 23   ; bit 23 of EDX indicates MMX support
jz NoMMX            ; if zero, jump to routine to handle absence of MMX

Bit 23 of the EDX register indicates MMX support. If the bit is set, the program can safely execute MMX instructions. Otherwise, it should fall back to a scalar (non-MMX) implementation.

Other bits in EDX will tell us about different supported technologies. For example:

  • Bit 23: MMX support
  • Bit 25: SSE support
  • Bit 26: SSE2 support

What About AMD's 3DNow!

Years later, Quake II added support for a technology called 3DNow!. 3DNow! was designed by AMD and it was conceptually similar to MMX; it adds SIMD instructions to the base x86 instruction set, enabling floating-point SIMD operations using vector registers.

Quake II
Quake II

3DNow! also used 64-bit MMX registers, but it added instructions oriented toward floating-point SIMD. The first processor to ship with 3DNow! technology was the AMD K6-2 from 1998.

AMD K6-2 3DNow!
AMD K6-2 with 3DNow!

The source code of Quake II 3.19 was released in 1999 by Id Software under a GPL licence. Just keep in mind that the 3DNow! code was not part of that original 3.19 source release. Instead, AMD/id Software distributed a separate 3DNow!-optimized Quake II v3.20 build. Of course, these were distributed as pre-compiled binaries (without source code).

Was the 3DNow! a Success?

No. Even though a few popular games added 3DNow! enhancements, 3DNow! was never more popular than MMX. In fact, MMX was substantially more widespread because it had a huge head start. Intel introduced MMX with the Pentium MMX in January 1997, while AMD's 3DNow! arrived with the K6-2 in 1998.

More importantly, Intel had an enormous share of the x86 market, so choosing MMX meant potentially supporting a larger fraction of PC gamers. At the end of the day, 3DNow! was an AMD-specific extension.

Developers faced the choice of supporting either Intel CPUs or AMD CPUs. If you wrote a 3DNow!-optimized renderer, it would only benefit AMD chips that supported it. Meanwhile, MMX was supported by Intel and subsequently by AMD, Cyrix, and others.

Finally, the ultimate blow to AMD's 3DNow! was Intel's introduction of SSE (Streaming SIMD Extensions) with the Pentium III in 1999.

Pentium III

SSE: Streaming SIMD Extensions

Intel's SSE was a major turning point for both 3DNow! and MMX, but it didn't immediately kill MMX.

SSE technology introduced both scalar and packed floating-point instructions, which basically removed the old MMX limitation of not allowing floating-point code and SIMD to coexist.

3DNow! vs SSE

SSE's threat to 3DNow! was bigger because it did the same fundamental job: floating-point SIMD, but with a more powerful and ultimately industry-standard architecture. SSE didn't immediately make MMX obsolete because MMX was still excellent at integer SIMD, particularly for things like pixels, image processing, audio samples, and packed 8/16-bit arithmetic.

SSE gave developers a more powerful and vendor-backed floating-point SIMD architecture. And unlike 3DNow!, SSE became the long-term direction of x86 SIMD.

Why Did MMX Die Out?

So, what ultimately made the old MMX registers unnecessary? If you ask retro tech enthusiasts, most answers would point to the introduction of SSE2 by Intel in 2000. SSE2 brought exactly the thing MMX was good at (integer SIMD) into the newer XMM register architecture, while also giving programmers much wider registers.

SSE2 gives you 128-bit XMM registers and, crucially, adds extensive integer SIMD operations to the x86 architecture.

MMX vs SSE2

Imagine you were a game programmer in 2001.

Using the old MMX registers, you could process 8 pixels at a time:

movq    mm0, [pixels]
paddusb mm0, mm1
movq    [pixels], mm0

With SSE2, you could now process 16 pixels at a time:

movdqu  xmm0, [pixels]
paddusb xmm0, xmm1
movdqu  [pixels], xmm0

Conclusion

Hopefully, by now, you understand that MMX was not a magic "turbo button" that made the entire CPU faster. Many of my friends assumed that buying a fancy Pentium "with MMX technology" would make the entire computer (and especially their multimedia applications) faster. As you just saw, MMX simply gave the CPU new tools, but it didn't automatically make the old tools faster.

Looking back, MMX may seem tiny by today's standards. MMX had only eight 64-bit registers, only supported integer arithmetic, and there was no floating-point SIMD or 128-bit vectors.

MMX introduced ideas that are still with us today:

  • Packed data
  • Vectorized arithmetic
  • Saturating operations
  • Data-parallel programming
  • Explicit SIMD optimizations

Later, SSE expanded the registers to 128 bits, and AVX doubled them again. Modern CPUs can process hundreds of bytes per instruction using vector extensions that all trace their lineage back to those original MMX instructions.

For many programmers in the late 1990s, MMX was the first exposure to thinking about algorithms in terms of vectors instead of individual values. Once you started seeing arrays as chunks of data that could move through the CPU together, it changed how you approached performance-critical code.

Unlike many authors, I would not call MMX a failure. What I would say is that MMX was a successful technology transition, but not a particularly successful long-lived instruction set.

Even though MMX has long since disappeared from modern software, it remains an important milestone in the evolution of PC programming. It is a fascinating snapshot of an era when every CPU cycle counted.

And that's it for our quick review of MMX in old Pentium CPUs. If you have any suggestions for this article, you can yell at me on Twitter. Also, remember to visit the courses page to access my lectures on retro programming.

See you inside!