SSE/AVX
「SIMD (Single Instruction/Multiple Data)」は、一つの命令で、複数のデータを同時に処理する方法です。
MMX 命令でも、64bit レジスタを使って複数の整数値を処理することができましたが、SSE や AVX 命令を使うと、128bit、256bit のレジスタを使うことで、さらに多くのデータを同時に処理することができます。
SSE、SSE2、SSE3、SSSE3、SSE4.1、SSE4.2 は、128bit レジスタを使います。
これらは総称して「レガシー SSE」と呼ばれ、x86 でも使用できます。
AVX、AVX2、FMA などは、128/256bit レジスタを使います。
x64 で追加されたサブセット命令のため、「拡張 SSE」と呼ばれます。
※SSE4A、XOP、FMA4 は、AMD が独自に追加したもので、Intel プロセッサでは使用できません。
MMX 命令でも、64bit レジスタを使って複数の整数値を処理することができましたが、SSE や AVX 命令を使うと、128bit、256bit のレジスタを使うことで、さらに多くのデータを同時に処理することができます。
SSE、SSE2、SSE3、SSSE3、SSE4.1、SSE4.2 は、128bit レジスタを使います。
これらは総称して「レガシー SSE」と呼ばれ、x86 でも使用できます。
AVX、AVX2、FMA などは、128/256bit レジスタを使います。
x64 で追加されたサブセット命令のため、「拡張 SSE」と呼ばれます。
※SSE4A、XOP、FMA4 は、AMD が独自に追加したもので、Intel プロセッサでは使用できません。
CPUID
x64 に明確に含まれているのは、SSE と SSE2 のみです。
それ以外の命令のサポートはプロセッサによって異なるので、SSE3 以降の命令を使いたい場合は、CPUID 命令でフラグをチェックする必要があります。
それ以外の命令のサポートはプロセッサによって異なるので、SSE3 以降の命令を使いたい場合は、CPUID 命令でフラグをチェックする必要があります。
SSE | EAX = 0000_0001h > EDX (bit 25) |
---|---|
SSE2 | EAX = 0000_0001h > EDX (bit 26) |
SSE3 | EAX = 0000_0001h > ECX (bit 0) |
SSSE3 | EAX = 0000_0001h > ECX (bit 9) |
SSE4.1 | EAX = 0000_0001h > ECX (bit 19) |
SSE4.2 | EAX = 0000_0001h > ECX (bit 20) |
AVX | EAX = 0000_0001h > ECX (bit 28) |
AVX2 | EAX = 0000_0007h, ECX = 0 > EBX (bit 5) |
FMA | EAX = 0000_0001h > ECX (bit 12) |
レジスタと命令
レガシー SSE では、128bit の XMM0〜XMM15 レジスタが使用できます。
拡張 SSE では、XMM0〜XMM15 に加えて、256bit の YMM0〜YMM15 レジスタが使用できます。
ただし、XMM レジスタは、YMM レジスタの下位 128bit として使用されます。
拡張 SSE では、XMM0〜XMM15 に加えて、256bit の YMM0〜YMM15 レジスタが使用できます。
ただし、XMM レジスタは、YMM レジスタの下位 128bit として使用されます。
AVX/AVX2 命令
AVX/AVX2 のほとんどの命令は、レガシー SSE と同じ動作を行います。
レガシー SSE 命令のニーモニックの先頭に V を付けると、同じ動作の AVX/AVX2 命令になります。
例えば、パックされた 8bit 整数の加算命令は、SSE2/AVX/AVX2 で、以下の3通りあります。
レガシー SSE 命令のニーモニックの先頭に V を付けると、同じ動作の AVX/AVX2 命令になります。
例えば、パックされた 8bit 整数の加算命令は、SSE2/AVX/AVX2 で、以下の3通りあります。
PADDB xmm1, xmm2/mem128 | SSE2 VPADDB xmm1, xmm2, xmm3/mem128 | AVX (128bit) VPADDB ymm1, ymm2, ymm3/mem256 | AVX2 (256bit)
オペランドの数
レガシー SSE では、ソースと宛先は基本的に2つのオペランドで指定するため、宛先オペランドと1つ目のソースオペランドは同じになります。
一方、拡張 SSE では、宛先と1つ目のソースを別々に指定することができるため、多くの場合で、オペランドを3つ指定することになります。
上記の PADDB を例にすると、SSE2 命令では、operand1 + operand2 = operand1 という形になりますが、
AVX/AVX2 命令では、operand2 + operand3 = operand1 という形になります。
これにより、結果を別のレジスタに格納することができます。
一方、拡張 SSE では、宛先と1つ目のソースを別々に指定することができるため、多くの場合で、オペランドを3つ指定することになります。
上記の PADDB を例にすると、SSE2 命令では、operand1 + operand2 = operand1 という形になりますが、
AVX/AVX2 命令では、operand2 + operand3 = operand1 という形になります。
これにより、結果を別のレジスタに格納することができます。
YMM レジスタの扱い
XMM レジスタは、YMM レジスタの下位 128bit として使用されます。
そのため、XMM レジスタが宛先のオペランドとして指定されている場合、それに対応する YMM レジスタの下位 128bit に値が格納された時に、YMM レジスタの上位 128bit がどうなるかを認識しておく必要があります。
PADDB の例にあるように、SSE/AVX 命令は、基本的に以下の3通りのオペランド形式がある場合が多いです。
そのため、XMM レジスタが宛先のオペランドとして指定されている場合、それに対応する YMM レジスタの下位 128bit に値が格納された時に、YMM レジスタの上位 128bit がどうなるかを認識しておく必要があります。
PADDB の例にあるように、SSE/AVX 命令は、基本的に以下の3通りのオペランド形式がある場合が多いです。
- レガシー SSE 命令で、XMM レジスタを指定する。
この場合、対応する YMM レジスタの上位 128bit は変更されません。 - 拡張 SSE 命令で、XMM レジスタを指定する。
この場合、対応する YMM レジスタの上位 128bit は、0 にクリアされます。 - 拡張 SSE 命令で、YMM レジスタを指定する。
この場合、YMM レジスタの全体が使用されます。
rFLAGS
ほとんどの SSE/AVX 命令は、rFLAGS レジスタのフラグを変更しません。
ただし、一部の比較命令などでは、比較の結果として、rFLAGS のビットを変更します。
ただし、一部の比較命令などでは、比較の結果として、rFLAGS のビットを変更します。
エンコーディング
レガシー SSE 命令の場合は、汎用命令と同じようなエンコーディングが使用されますが、拡張 SSE 命令では、VEX プレフィックス (全体で 2〜3 byte) を使って追加情報を指定します。
なお、REX プレフィックスで指定するのと同じ情報が、ここには含まれているので、REX プレフィックスは使用されません。
詳細は、前回紹介した エンコーディング の方で確認してください。
上記のように、エンコーディングの表記に「is4」という指定がある場合は、8bit 即値で、4つ目のオペランドのレジスタを指定します。
1つ目のオペランドは ModRM.reg で指定され、2つ目のオペランドは VEX.vvvv で、3つ目のオペランドは ModRM.r/m で指定されますが、この場合、4つ目のレジスタオペランドを指定する方法がないため、最後に即値バイトを追加して、レジスタを指定することになります。
なお、REX プレフィックスで指定するのと同じ情報が、ここには含まれているので、REX プレフィックスは使用されません。
詳細は、前回紹介した エンコーディング の方で確認してください。
* VEX 3byte 形式 |- byte0 -|--- byte1 --------------|--- byte2 ----------| | | 7 | 6 | 5 | 4 0 | 7 | 6 3 | 2 | 1 0 | | C4 | R | X | B | map_select | W | vvvv | L | pp | * VEX 2byte 形式 |- byte0 -|-- byte1 -----------| | | 7 | 6 3 | 2 | 1 0 | | C5 | R | vvvv | L | pp | X = 1, B = 1, W = 0, map_select = 00001b * vvvv の値 0000: XMM15/YMM15 | 1000: XMM7/YMM7 0001: XMM14/YMM14 | 1001: XMM6/YMM6 0010: XMM13/YMM13 | 1010: XMM5/YMM5 0011: XMM12/YMM12 | 1011: XMM4/YMM4 0100: XMM11/YMM11 | 1100: XMM3/YMM3 0101: XMM10/YMM10 | 1101: XMM2/YMM2 0110: XMM9/YMM9 | 1110: XMM1/YMM1 0111: XMM8/YMM8 | 1111: XMM0/YMM0
例
例えば、PADDB と VPADDB 命令の場合、以下のようなエンコーディングが指定されています。
最後の「FC /r」(オペコードと ModRM バイト) は、3つの命令で同じです。
VPADDB 命令の場合は、先頭のバイトが C4 なので、VEX プレフィックスが付いていることになります。
空白で区切った、次の2つのフィールドは、それぞれ、VEX プレフィックスの2番目と3番目のバイトの値を示しています。
各バイトは、'.' (カンマ) で値のフィールドが区切られています。
RXB は、VEX の R, X, B の各ビットが、1 かどうかを示します (記述されていれば 1)。
次の 01 は、map_select の値 (ビット値) です。
3番目のバイトも、同じように [ W, vvvv, L, pp ] の4つの値を示しています。
W のフラグは、命令によって、使用される場合と使用されない場合があるので、使用されない (どちらでもよい) 場合は、X が指定されます。よって、通常は 0 になります。
vvvv は、3つ目または4つ目のオペランド (XMM or YMM) を指定する値です。
ここでは src1 になっているので、第1ソースのオペランドを指定することになります (VPADDB 命令では、2つ目のオペランド)。
PADDB xmm1, xmm2/mem128 | 66 0F FC /r | SSE2 VPADDB xmm1, xmm2, xmm3/mem128 | C4 RXB.01 X.src1.0.01 FC /r | AVX VPADDB ymm1, ymm2, ymm3/mem256 | C4 RXB.01 X.src1.1.01 FC /r | AVX2
最後の「FC /r」(オペコードと ModRM バイト) は、3つの命令で同じです。
VPADDB 命令の場合は、先頭のバイトが C4 なので、VEX プレフィックスが付いていることになります。
空白で区切った、次の2つのフィールドは、それぞれ、VEX プレフィックスの2番目と3番目のバイトの値を示しています。
各バイトは、'.' (カンマ) で値のフィールドが区切られています。
RXB は、VEX の R, X, B の各ビットが、1 かどうかを示します (記述されていれば 1)。
次の 01 は、map_select の値 (ビット値) です。
3番目のバイトも、同じように [ W, vvvv, L, pp ] の4つの値を示しています。
W のフラグは、命令によって、使用される場合と使用されない場合があるので、使用されない (どちらでもよい) 場合は、X が指定されます。よって、通常は 0 になります。
vvvv は、3つ目または4つ目のオペランド (XMM or YMM) を指定する値です。
ここでは src1 になっているので、第1ソースのオペランドを指定することになります (VPADDB 命令では、2つ目のオペランド)。
実際のエンコーディング
エンコーディングは、VEX プレフィックスの 3 byte 形式を使った場合の値が示されています。
しかし、特定のフィールドが、ある値の組み合わせになっている場合は、コンパクトな 2 byte の形式でエンコードされる場合があります。
例えば、アセンブラで以下のように記述した場合、3 byte 形式の各バイト値は、「C4 | 111_00001b | 0_1110_0_01b」となります。
この場合、2 byte 形式の条件である、X = 1, B = 1, W = 0, map_select = 00001b の値に一致しているため、2 byte 形式に圧縮することができます。
結果、「C5 | 1_1110_0_01b」となり、VEX プレフィックスは「C5 F1」の 2 byte になります。
しかし、特定のフィールドが、ある値の組み合わせになっている場合は、コンパクトな 2 byte の形式でエンコードされる場合があります。
例えば、アセンブラで以下のように記述した場合、3 byte 形式の各バイト値は、「C4 | 111_00001b | 0_1110_0_01b」となります。
vpaddb xmm0, xmm1, xmm2 [byte2] R,X,B = 1, map_select = 01b [byte3] W = 0, vvvv = 1110b (xmm1), L = 0 (128bit), pp = 01 (66h prefix)
この場合、2 byte 形式の条件である、X = 1, B = 1, W = 0, map_select = 00001b の値に一致しているため、2 byte 形式に圧縮することができます。
結果、「C5 | 1_1110_0_01b」となり、VEX プレフィックスは「C5 F1」の 2 byte になります。
補足
VPBLENDVB xmm1, xmm2, xmm3/mem128, xmm4 | C4 RXB.03 0.src1.0.01 4C /r is4
上記のように、エンコーディングの表記に「is4」という指定がある場合は、8bit 即値で、4つ目のオペランドのレジスタを指定します。
1つ目のオペランドは ModRM.reg で指定され、2つ目のオペランドは VEX.vvvv で、3つ目のオペランドは ModRM.r/m で指定されますが、この場合、4つ目のレジスタオペランドを指定する方法がないため、最後に即値バイトを追加して、レジスタを指定することになります。
数値
SSE/AVX 命令での浮動小数点数の演算では、単精度 (32bit) と倍精度 (64bit) がサポートされています。
半精度 (16bit) は、CPUID で F16C がサポートされている場合のみ、単精度との相互変換が行えます。
パック整数は、BYTE, WORD, DWORD, QWORD (64bit), Double QWORD (128bit) がサポートされています。
DQ (double quadword) は、演算は行えませんが、ビットシフトなどのみ行えます。
半精度 (16bit) は、CPUID で F16C がサポートされている場合のみ、単精度との相互変換が行えます。
パック整数は、BYTE, WORD, DWORD, QWORD (64bit), Double QWORD (128bit) がサポートされています。
DQ (double quadword) は、演算は行えませんが、ビットシフトなどのみ行えます。
不定値
浮動小数点データ型と整数データ型にはそれぞれ、不定値を示す値があります。
例えば、浮動小数点数を整数に変換する場合、ソース値が NaN (非数値) や無限大の場合や、変換後の結果が、指定された整数値の範囲で表現できない場合、結果として、整数の不定値が返されます (MXCSR の例外マスク IM が 1 の場合)。
データ型による各不定値は、以下の値になります。
整数の場合は、符号付きとして見た場合、負の最大値となります。数値としては正しい値となるので、注意してください。
例えば、浮動小数点数を整数に変換する場合、ソース値が NaN (非数値) や無限大の場合や、変換後の結果が、指定された整数値の範囲で表現できない場合、結果として、整数の不定値が返されます (MXCSR の例外マスク IM が 1 の場合)。
データ型による各不定値は、以下の値になります。
単精度 | FFC0_0000h |
---|---|
倍精度 | FFF8_0000_0000_0000h |
16bit 整数 | 8000h |
32bit 整数 | 8000_0000h |
64bit 整数 | 8000_0000_0000_0000h |
整数の場合は、符号付きとして見た場合、負の最大値となります。数値としては正しい値となるので、注意してください。
MXCSR レジスタ
SSE/AVX 命令に関する、例外フラグや例外マスク、丸めの設定などは、
「Media eXtension Control and Status Register (MXCSR)」によって設定されます。
リセット時は、例外マスク (bit 12:7) がすべて 1 となり、それ以外は 0 になります。
STMXCSR 命令で、メモリに MXCSR の値を保存でき、LDMXCSR 命令で、メモリから MXCSR に値をロードできます。
MXCSR の値を変更したい場合は、メモリに 32bit 値を保存してから、それをロードする形になります。
「Media eXtension Control and Status Register (MXCSR)」によって設定されます。
|31 - 18|17|16|15|14,13|12|11|10| 9| 8| 7| 6| 5| 4| 3| 2| 1| 0| bit |reversed|MM|--|FZ| RC |PM|UM|OM|ZM|DM|IM|DAZ|PE|UE|OE|ZE|DE|IE|
リセット時は、例外マスク (bit 12:7) がすべて 1 となり、それ以外は 0 になります。
STMXCSR 命令で、メモリに MXCSR の値を保存でき、LDMXCSR 命令で、メモリから MXCSR に値をロードできます。
MXCSR の値を変更したい場合は、メモリに 32bit 値を保存してから、それをロードする形になります。
詳細
- 無効な操作例外 (IE)
無効な操作の例外が発生すると、1 になります。 - 非正規化オペランド例外 (DE)
命令のソースオペランドの 1 つの値が、非正規化形式である場合、1 になります。
ただし、DAZ ビットが 1 の場合、DE ビットを設定しません。 - ゼロ除算例外 (ZE)
ゼロ以外の数値を、ゼロで除算するときに、1 になります。 - オーバーフロー例外 (OE)
丸められた結果の絶対値が、宛先フォーマットで表現可能な、最大の正規化浮動小数点数より大きい場合、1 になります。 - アンダーフロー例外 (UE)
丸められた非ゼロの結果の絶対値が小さすぎて、宛先フォーマットの正規化浮動小数点数として表現できない場合、1 になります。
UM ビットが 1 の場合、UE が精度例外 (PE) とともに発生した場合にのみ、UE 例外を報告します。 - 精度例外 (PE)
丸め後の浮動小数点の結果が、無限に正確な結果と異なるため、指定された宛先フォーマットで正確に表現できない場合、1 になります。 - 非正規化はゼロ (DAZ)
ハードウェア実装がこのモードをサポートしている場合、ソフトウェアはこのビットを 1 に設定して、DAZ モードを有効にすることができます。
DAZ モードでは、非正規化のソースオペランドの場合、演算前に、ソース値を符号付き 0 (元のソースと同じ符号) に変換します。
DAZ モードは、ANSI/IEEE Std 754 に準拠していません。 - 例外マスク (PM、UM、OM、ZM、DM、IM)
対応する6つの SIMD 浮動小数点例外 (PE、UE、OE、ZE、DE、IE) を、マスク、またはマスク解除できます。
ビットを 1 に設定するとマスクし、0 にクリアするとマスクを解除します (初期値は 1)。
マスクを解除すると、例外が発生したときに、SIMD 浮動小数点例外サービスルーチンに分岐します。 - 浮動小数点丸め制御 (RC) - bit 13,14
浮動小数点演算の丸め方法を指定します。
00 = 近い方向に丸め [default]
01 = 負の無限大方向に丸め
10 = 正の無限大方向に丸め
11 = 0 方向に丸め - フラッシュゼロ (FZ)
丸められた結果が小さく、UM マスクが設定されている場合、このビットにより、結果がゼロにフラッシュされます。
その結果、値は不正確になり、PE と UE の両方が設定されることになります。 - 不整列例外マスク (MM) [AMD]
AMD プロセッサのみで使用され、1 の場合、アライメントされていないメモリをサポートします。
CPUID: EAX = 8000_0001h > ECX(bit7) で、この機能がサポートされているかを確認できます。
データ型の違い
例えば、MOVAPS 命令は、パックされた単精度浮動小数点として、128bit 値 (32bit x 4) をコピーし、
MOVAPD 命令は、パックされた倍精度浮動小数点として、128bit 値 (64bit x 2) をコピーします。
この2つの命令は、全体で 128bit 値をコピーするという意味では同じなので、レジスタに格納される値は、どちらも変わりません。
また、値をコピーする時に、命令に一致するデータ型かどうかをチェックするようなことはないため、仮に不正確な値だったとしても、例外が発生するようなことはありません。
ただし、プロセッサ内部の実装では、各命令のデータ型を明確に記憶し、現在レジスタに格納されているデータ型と、命令で実行されるデータ型が一致しているかどうかを判断する場合があります。
この場合、例えば、パック単精度として格納したデータを使って、パック倍精度の演算を実行しようとした場合、型の不一致により、ペナルティとして遅延が起こる場合があります。
そのため、たとえ命令の実行結果が同じになるとしても、常に使用するデータ型に合わせた命令を使う必要があります。
MOVAPD 命令は、パックされた倍精度浮動小数点として、128bit 値 (64bit x 2) をコピーします。
MOVAPS xmm1, xmm2/mem128 | 0F 28 /r | パック単精度 MOVAPD xmm1, xmm2/mem128 | 66 0F 28 /r | パック倍精度
この2つの命令は、全体で 128bit 値をコピーするという意味では同じなので、レジスタに格納される値は、どちらも変わりません。
また、値をコピーする時に、命令に一致するデータ型かどうかをチェックするようなことはないため、仮に不正確な値だったとしても、例外が発生するようなことはありません。
ただし、プロセッサ内部の実装では、各命令のデータ型を明確に記憶し、現在レジスタに格納されているデータ型と、命令で実行されるデータ型が一致しているかどうかを判断する場合があります。
この場合、例えば、パック単精度として格納したデータを使って、パック倍精度の演算を実行しようとした場合、型の不一致により、ペナルティとして遅延が起こる場合があります。
そのため、たとえ命令の実行結果が同じになるとしても、常に使用するデータ型に合わせた命令を使う必要があります。
丸め
浮動小数点数の丸め方法は4通りあり、MXCSR レジスタの RC で、丸め方法を指定することができます。
実際にどのような値に丸められるかを、SSE4.1 の ROUNDPS 命令を使って確認してみます。
> SSE/AVX 変換
パックされた単精度浮動小数点数を、指定方法で整数に丸めて、パック単精度として格納します。
即値の下位 2bit で、丸めモードを指定できます。
デフォルトの RC = 0 は、要するに四捨五入です。
RC = 1 は、値が小さい方の整数に切り捨て、RC = 2 は、値が大きい方の整数に切り上げです。
RC = 3 は、正なら値が小さい方、負なら値が大きい方に丸められます。
0 | 近い方向に丸め (デフォルト) |
---|---|
1 | 負の無限大に近い方向に丸め |
2 | 正の無限大に近い方向に丸め |
3 | ゼロに近い方向に丸め |
実際にどのような値に丸められるかを、SSE4.1 の ROUNDPS 命令を使って確認してみます。
> SSE/AVX 変換
ROUNDPS xmm1, xmm2/mem128, imm8 | 66 0F 3A 08 /r ib | SSE4.1
パックされた単精度浮動小数点数を、指定方法で整数に丸めて、パック単精度として格納します。
即値の下位 2bit で、丸めモードを指定できます。
サンプルコード
<28_round.c>
<28_round.asm>
#include <stdio.h> void testfunc(void); int main(void) { testfunc(); return 0; } void putval(float *p) { int i; for(i = 0; i < 4; i++) printf("%.1f, ", p[i]); printf("\n"); }
<28_round.asm>
default rel global testfunc extern putval section .text testfunc: push rbp mov rbp, rsp sub rsp, 24 ; 結果の値の格納 ; push rbx 後の位置を 16byte 境界にする push rbx lea rbx, [rbp - 16] ; 値の格納先のアドレス roundps xmm0, [data], 0 ; 近い方に丸め movaps [rbx], xmm0 ; スタック領域に格納 mov rdi, rbx ; arg1 (ポインタ) call putval roundps xmm0, [data], 1 ; 負の無限大方向 movaps [rbx], xmm0 mov rdi, rbx call putval roundps xmm0, [data], 2 ; 正の無限大方向 movaps [rbx], xmm0 mov rdi, rbx call putval roundps xmm0, [data], 3 ; 0 に近い方に丸め movaps [rbx], xmm0 mov rdi, rbx call putval pop rbx mov rsp, rbp pop rbp ret align 16 data: dd 2.2, 2.8, -2.2, -2.8
$ nasm -f elf64 28_round.asm $ cc -o test 28_round.c 28_round.o $ ./test 2.0, 3.0, -2.0, -3.0, 2.0, 2.0, -3.0, -3.0, 3.0, 3.0, -2.0, -2.0, 2.0, 2.0, -2.0, -2.0,
解説
CALL 命令で C の関数を呼び出す場合、現在の RSP が 16 byte 境界である必要があります。
PUSH RBP の後、ローカル変数領域を確保して、さらに PUSH RBX を行うため、それぞれのアライメントを合わせる必要があります。
PUSH RBX の後 (CALL 命令を実行する時の RSP) が 16 byte 境界になるようにするためには、8 byte の余白が必要になります。
そのため、ローカル変数を確保する時に、余分に 8 byte を追加して、減算しています。
RBX には、結果の値を格納するアドレス (スタック内位置) を入れておきます。
RBX レジスタの値は、呼び出し元に属しているので、CALL 命令で関数が呼び出された後でも、元の値が常に維持されています。
※そのため、RBX の値は、関数が戻る時に、呼び出された直後の状態に戻す必要があります。
第1引数 (RDI) に、結果の値が格納されているポインタを入れて、C で定義されている putval 関数を呼び出します。
RDI は、呼び出した関数内で値が変更される可能性があるため、使いまわすことはできず、常に CALL 命令を実行する前に、値を設定する必要があります。
putval 関数内では、パックされた4つの単精度値を printf で表示しています。
PUSH RBP の後、ローカル変数領域を確保して、さらに PUSH RBX を行うため、それぞれのアライメントを合わせる必要があります。
-------- (-48) <-- RBX -------- (-40) 余白 [8byte] 結果を書き込む位置 [16byte] <-- -------- (-16) RBP -------- (-8) RIP -------- testfunc が呼び出される前 <-- の位置は、16 byte 境界であること
PUSH RBX の後 (CALL 命令を実行する時の RSP) が 16 byte 境界になるようにするためには、8 byte の余白が必要になります。
そのため、ローカル変数を確保する時に、余分に 8 byte を追加して、減算しています。
RBX には、結果の値を格納するアドレス (スタック内位置) を入れておきます。
RBX レジスタの値は、呼び出し元に属しているので、CALL 命令で関数が呼び出された後でも、元の値が常に維持されています。
※そのため、RBX の値は、関数が戻る時に、呼び出された直後の状態に戻す必要があります。
第1引数 (RDI) に、結果の値が格納されているポインタを入れて、C で定義されている putval 関数を呼び出します。
RDI は、呼び出した関数内で値が変更される可能性があるため、使いまわすことはできず、常に CALL 命令を実行する前に、値を設定する必要があります。
putval 関数内では、パックされた4つの単精度値を printf で表示しています。
結果
//2.2, 2.8, -2.2, -2.8 2.0, 3.0, -2.0, -3.0, //近い方向 2.0, 2.0, -3.0, -3.0, //負の無限大に近い方向 3.0, 3.0, -2.0, -2.0, //正の無限大に近い方向 2.0, 2.0, -2.0, -2.0, //ゼロに近い方向
デフォルトの RC = 0 は、要するに四捨五入です。
RC = 1 は、値が小さい方の整数に切り捨て、RC = 2 は、値が大きい方の整数に切り上げです。
RC = 3 は、正なら値が小さい方、負なら値が大きい方に丸められます。