わらばんし仄聞記

南の国で引きこもってるWeb屋さん

原書で学ぶ64bitアセンブラ入門(6)

11章、浮動小数点数について。

原書で学ぶ64bitアセンブラ入門(5) - connpassでの内容です。

chapter11 Floating point instructions

浮動小数点数の演算は、8088世代のCPUでは8087というコプロセッサ(補助プロセッサ)を使っていました。486世代になるとコプロセッサを内包するようになり、現在のCPUでもそれらの為の命令は使えるようです。
ですが、現在では基本的に、完全に分離された浮動小数点数用の機構があり、そちらを使います。その機構では16個の128bitレジスタ(Core iシリーズでは256bit)を使い、これらのレジスタはSSEレジスタ(Streaming SIMD ExtensionsレジスタSIMD=Single Instruction - Multiple Data)と呼ばれます。

この章では主にこのレジスタを使用した命令を見ていきます

Flaoting point registers

先ほど述べたSSEレジスタxmm0というように、xmmという接頭辞に数字が付いたものになります。16個存在するので、それぞれ

xmm0, xmm1, ... , xmm15

となります。core iシリーズでは256bit長のものになるとも述べましたが、それらはymmという接頭辞になり

ymm0, ymm1, ... , ymm15

となります。また、以前に64bitレジスタ、たとえばraxは対応する32bitレジスタeaxと下位32bitを共有しているという話がありましたが、それと同様、ymmレジスタは対応するxmmレジスタと下位128bitを共有しています。
加えてcore iシリーズではAVX命令(Advanced Vector Extension命令)が追加されています。

以下、基本的にxmmレジスタについて述べていきます。内容はbit長を2倍すればそのままymmレジスタにも当てはまります。

さて、このxmmレジスタは、一つの値を持つか、もしくは4つのfloatか2つのdoubleという複数の値を持つ事ができます。また、複数の整数値を持つようにもできますが、それについてはこの本では触れないそうです。

Moving data to/from floating point registers

これまた先述した通り、SSEレジスタでは1つの値(scalar値)を扱うか、複数の値を持つデータ(= packed data)として扱う事ができます。これらについてのmov命令がどのようになるか見てみます。

Moving scalars

mov命令のSSE版ということで、movss,movsdという命令があります。それぞれfloat用とdouble用です。これらの命令では

  • メモリ -> SSE
  • SSE -> メモリ
  • SSE -> SSE

というパターンで値を移すことができ、SSEレジスタはそれぞれで必要なbit長分の下位bitを使って当該値のやりとりをする事になります。   たとえばmovssした場合はこんな感じ

f:id:warabanshi21:20140520230747p:plain

Moving packed data

さて、今度はpack値でのmov命令です。
今回は命令が4つあり、float用、double用それぞれにalignedとunaligned用の命令があります。ここで言うalignについては後述するとして、表にするとこんな感じに。

aligned unaligned
float movaps movups
double movapd movupd

命令の命名規則movにalignedの場合はa、unalingedの場合はu。続けてpackedのp、floatならsingleのsでdoubleならdoubleのdを続けたパターンになっています。

alignについてですが、これはメモリ上の位置が16バイト境界で整列されていることを意味します。つまり、mov対象の値があるメモリ上のアドレスは、16進数表記の場合に末尾が0となっているということですね。そのような配置になっていないメモリ上の値をmovapsなどすると、segmentation faultを起こします。
適切にalignするためにはalign命令を使ってalign 16なんてのを対象となる配列定義直前に入れるか、もしくはmalloc関数で取得したメモリは16バイト境界になっています。

実際に書いてみるとこんな感じに。

align 16によって配列cの始まる位置が16バイト境界に整えられます。もしこれがalign 16を入れないなら、cの位置がたまたま16バイト境界に位置しない限りはsegmentation faultを引き起こします。
こんな感じでcのアドレスが16の倍数となるよう、間を適切に空けてくれます。

f:id:warabanshi21:20140520233552p:plain

こんな手間をしなくていいのがunaligned用の命令なんですが、やはりお手軽な分、処理は重いようです。しかし、この本曰くcore i世代のCPUではunalignedでもalignedと同じくらい早くなっているそうです・・・。
まぁ、極限の早さを求める人向きって事ですかね。

加減乗除

本ではそれぞれ節が分かれてますが、内容はほとんど同じなのでまとめて。

命令の命名規則add sub mul divそれぞれに、ss sd ps pdのいずれかが接尾辞として付いた形になります。接尾辞は1文字目がscalarかpackかを表し、二文字目がsingle(=float)かdoubleを表します。addを例に挙げて表にするとこんな感じ

scalar packed
single addss addps
double addsd addpd

他のsub mul div についても同様です。

レジスタとメモリについて、オペランドに使える組み合わせは以下のようになります

  • SSE -> SSE
  • メモリ -> SSE

デスティネーションは常にSSEレジスタである必要があります。
整数値の計算を行うとフラグレジスタに値がセットされることがありますが、浮動小数点数での計算では一切のフラグはセットされません。なので、テストをするには後で比較命令を使う必要があります。

packedでの演算は、2つのSSEレジスタ、またはSSEレジスタと同じだけのバイト長のメモリ領域にある、それぞれ対応する位置のpackされた値と演算をします。
コードで表すとこんな感じに。

movapd   xmm0, [a]      ; load 2 doubles from a
addpd    xmm0, [b]      ; add a[0] + b[0] and a[1] + b[1]

図示するとこんな感じのイメージ(コードと違ってfloat値4つの場合になってます)

f:id:warabanshi21:20140521000640p:plain

後半に続きます

11章は長いので、一旦ここまで。