90年代のSIMD:Intel Pentium MMXのプログラミング (pikuma.com)
原題: SIMD in the 90s: Programming Intel's Pentium MMX (pikuma.com)
日本語訳
# タイトル
90年代のSIMD:Intel Pentium MMXのプログラミング
# 本文
当社のソフトウェアレンダリング・モジュールを修了する学生は、必ずと言っていいほどSIMD(Single Instruction, Multiple Data)というトピックについて疑問を持ち、探求し始めます。これは一種の通過儀礼のようなものです。
今日では、AVX-512、NEON、SVE、そして数千のコアを持つGPUが存在します。これらの現代的なCPUや拡張命令セットは、長年の探求、学習、市場の圧力の結果であり、そのすべてに膨大な歴史や後方互換性のノイズが伴っています。いつものように、私は技術が始まったばかりの頃に何が起きていたのかを分析することで、異なる技術を振り返り、議論するのが好きです。90年代後半、多くのプログラマーは、初めて手書きのアセンブリ言語を用いることで、Pentium MMXから数フレームの追加のFPS(フレームレート)を絞り出す方法を学んでいました。
1997年にタイムスリップして、MMXのプログラミングがどのようなものだったかを見てみましょう!
### MMXとは何だったのか?
MMX(MultiMedia eXtensions)は、マルチメディア体験を向上させるために、1997年にPentium MMXプロセッサと共に導入されました。
これは、パフォーマンス向上のためにPentiumチップに組み込まれた57個の追加命令セットでした。CPUをMMXモードに切り替える必要があり、それによってx86の80ビット浮動小数点レジスタの最初の64ビットがMMXレジスタへと変換されました。
MMXの作成と使用を動機付けた考え方は単純なものでした。一度に1つの整数を処理する代わりに、CPUが単一の64ビットレジスタ内にパックされた複数の整数を同時に処理するというものです。
マルチメディア・ソフトウェア(画像処理、オーディオミキシング、ビデオ再生、さらにはゲーム)にとって、これは大幅なスピードアップをもたらす可能性がありました。
例えば、8つのバイトを個別に加算する代わりに:
10 + 20
30 + 40
50 + 60
70 + 80
...
MMXを使えば、これら8つの加算すべてを単一の命令で実行できます。
これが並列処理です!そして、これこそがSIMDの背後にある主要な概念です。
### SIMDとは何か?
「Single Instruction, Multiple Data(単一命令、複数データ)」は、並列処理技術の一種です。これは、複数のプロセッシング・エレメントが、複数のデータポイントに対して同じ操作を同時に実行するコンピュータを指します。
SIMDには、内部的なもの(ハードウェア設計の一部)と、命令セットアーキテクチャ(ISA)を通じて直接アクセスできるものがあります。MMXの場合、Intelはx86命令セットを拡張して、新しいSIMD命令を含めました。
### MMXは最初のSIMDだったのか?
いいえ、MMXは間違いなく最初のSIMDではありません。しかし、x86 PCにおいてSIMDを主流にする上で非常に重要な役割を果たしました。
コンピュータ技術における多くの進歩や革新は、パーソナルコンピュータ時代よりも前から存在していたことを忘れてはなりません。SIMDも例外ではなく、IntelのMMXよりも数十年早くから存在していました。「1つの命令で複数のデータ要素を操作する」という一般的な概念は、Pentiumよりもずっと前から遡ることができます。
初期の有名な例としては、60年代のILLIAC IVスーパーコンピュータ・プロジェクトがあります。ILLIAC IVは、最初の超並列コンピュータでした。このシステムは、もともと256個の64ビット浮動小数点演算ユニット(FPU)と、毎秒10億回の演算が可能な4つの中央演算処理装置(CPU)を備えるように設計されていました。
要約すると、ILLIAC IVは単一の命令ストリームの下で動作する大量のプロセッシング・エレメントを使用しており、現代のSIMD/ベクトルアーキテクチャの重要な祖先となっています。また、60年代から80年代にかけて、CDCやCrayなどの企業から配列処理マシンも登場していました。
### SIMD vs 配列プロセッサ
ベクトル処理とSIMDは、全く同じものではないという点を指摘しておくことが重要です。その違いは、主に複数のデータ要素がプロセッサにどのように提示され、ハードウェアがそれらをどのように実行するかという点にあります。
SIMD演算は、根本的にレジスタの幅に紐付けられています。
1976年のCray-1は、ベクトルプロセッサとして特徴付けられていました。これには8つの64要素ベクトルレジスタがあり、それぞれが64ビットの値を保持していました。
SIMDで見られるものと同様に、Cray-1は64個の個別のADD命令を必要としませんでした。これほど明らかにSIMDに似ているのに、なぜ単にSIMDと呼ばないのでしょうか? それはアーキテクチャ上の違いによるものです。SIMDは固定幅のレジスタに対して動作しますが、Cray-1のベクトルレジスタは、シーケンス(連続データ)のためのコンテナのようなものでした。
ベクトルプロセッサは通常、「SET VECTOR SIZE」命令の存在によって特徴付けられます。このベクトル演算ユニットは、複数のサイクルにわたって要素を処理することができます。
### MMXの真の意義
ご覧の通り、SIMDはIntelによって発明されたわけではありません。その真の意義は、比較的アクセスしやすいSIMD命令セットを、主流のx86 PCに投入したことにあります。それによって、私たち(パーソナルコンピュータ愛好家)の多くが、初めてこの種の技術に実際に触れることができたのです。これこそが、レトロコンピューティングの議論においてMMXがこれほど注目される理由です。SIMDの発明ではありませんでしたが、SIMDが専門的なハイパフォーマンス・コンピューティングから、一般的なデスクトップ・ソフトウェア開発へと移行した瞬間の一つだったのです。
### MMXレジスタ
前述の通り、巧妙な設計上の決定の一つは、Intelが全く新しいレジスタファイルを追加しなかったことです。その代わりに、MMXは浮動小数点スタックレジスタを再利用しました。
MM0
MM1
MM2
MM3
MM4
MM5
MM6
MM7
各MMXレジスタの幅は64ビットです。内部的には、MMXレジスタはx87浮動小数点レジスタのエイリアス(別名)でした。これらの基盤となる浮動小数点レジスタは80ビットの長さでしたが、MMXは下位64ビットのみにアクセスしました。
この決定により回路面積を節約できましたが、同時に重要な制限も生み出しました。
また、重要なルールも作成されました。
ご安心ください……これらの制限については、すぐにまた詳しくお話しします。
### パックされた整数データ
単一のMMXレジスタは、異なるレイアウトを保持できます。
使用する命令に応じて、同じレジスタがこれらのレイアウトのいずれかを表現できます。
### あなたの最初のMMXプログラム
例えば、8つのピクセルを加算したいとします(コンパイラの最適化は考慮しないものとします)。
```c
for (int i = 0; i < 8; i++) {
dest[i] = src1[i] + src2[i];
}
```
MMX版は以下のようになります。
```assembly
movq mm0, [src1] ; 8バイトをロード
movq mm1, [src2] ; 8バイトをロード
paddb mm0, mm1 ; 8つのバイト加算を同時に実行
movq [dst], mm0 ; 結果を格納
```
1つの命令が8つの個別の加算に取って代わりました!
### MMXによる飽和演算
MMXのもう一つの非常に優れた機能は、「飽和(saturating)」演算を実行できる能力でした。
2つの値を加算するとき、通常の整数演算ではラップアラウンド(循環)が発生します。
250 + 20 // = 270、これは14へとラップアラウンドします
上記の加算は255を超えており、通常は14へとラップアラウンドします。
この整数演算の「ラップアラウンド」という性質は、特にグラフィックスを扱う際に厄介なことがあります。ピクセル値を加算する場合、最大輝度は最大輝度のまま維持したいものです!
MMXは、このようなケースに対して飽和を提供します。命令は単純に次の通りです。
`paddusb mm0, mm1`
`us` は「unsigned saturation(符号なし飽和)」を意味し、最大結果値を255にクランプ(固定)します。
250 + 20 // = 255
同様に:
10 - 40 // = 0
これにより、輝度調整やブレンディング操作が非常に簡単になりました。
### MMXによるピクセルの比較
MMXのパワーがグラフィックスやその他のマルチメディア・タスクに適用されることを考えるのは自然なことです。例えば、ある値より大きいすべての値が白くなるようなマスクを作成したいとしましょう。
MMXは比較命令を提供しています。
`pcmpgtb mm0, mm1`
各バイトは、`0xFF` または `0x00` のいずれかになります。
原文(英語)を表示
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.
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.
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 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.
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.
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.
The Cray-1 from 1976 was characterized by being a vector processor. It had eight 64-element vector registers, each holding 64-bit values.
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 processors are usually characterized by the presence of a SET VECTOR SIZE
instruction. This vector functional unit can process the elements over multiple cycles.
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.
This decision saved silicon, but it also created an important limitation:
It also created an important rule:
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:
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.
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!
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.
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.
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.
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.
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.
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!