SIMD並列化ライブラリSmartVectorDotNet開発の知見まとめ(1) IEEE754浮動小数型の低レベル操作
現在、.Net用SIMD並列化ライブラリSmartVectorDotNetを開発しております。
本ライブラリの開発にあたっては数値型の低レベル操作、数学関数のアルゴリズムなど多くのノウハウを調査・実証し、パフォーマンスの追及を行ってきました。
ライブラリ自体はおおむね形になったので、後学のためその記録をここに残しておきます。
binary32(float), binary64(double)の基礎知識
現代的なコンピュータの多く、そして.Netの実行環境ではプリミティブな浮動小数型としてIEEE754のbinary32およびbinary64をサポートしています。
binary32の正規化数は次の数式で表すことができます。
(画像出典:IEEE754 - Wikipedia)
同様に、binary64の正規化数は次の数式で表すことができます。
(画像出典:IEEE754 - Wikipedia)
この変換を念頭に置くといくつかの数学関数を実装する際に効率化を行うことができます。例えばbinary32(
後述しますが、
剰余演算への応用
binary32/binary64の性質をうまく利用すると剰余演算の効率化を図ることができます。
IEEE754において正の正規化数として表現可能な2つの実数
-
(ただしx = a \cdot 2^n )1 \le a < 2, n\in \mathbb{Z} -
(ただしy = b \cdot 2^m )1 \le b < 2, m\in\mathbb{Z}
に対して
正の実数
の関係を満たすことを踏まえると、
-
ならば、n - m < 0 x\ \mathrm{mod}\ y = x -
ならば、n - m \ge 0 x\ \mathrm{mod}\ y = 2^m\cdot(a\cdot 2^{n - m}\ \mathrm{mod}\ b)
ここで、IEEE754において
-
はa\cdot 2^{n - m} の仮数部をマスクして整数型として再解釈し、さらにx だけ左シフトしたものn - m
(ただし、元のビット数に収まらない場合があるためオーバーフローを生じないよう十分にビット拡張されたものとする) -
はb の仮数部をマスクして整数型として再解釈したものy
つまり、オーバーフローが無視可能であるならば正規化浮動小数どうしの剰余演算も、整数の剰余演算とビットシフトに置換することができます。
また、仮数部をマスクしたということは、計算においては符号部・指数部のビットも利用することができます。このことを利用すればオーバーフロー対策として導入せざるを得なかったシフト→整数剰余計算の反復を最小限にとどめることができます。
以上により、IEEE754 binary32の剰余演算(正規化数限定)は以下のように実装できます。
このコードは入力に正規化数を要求する制限こそあるものの、C#標準の%
演算子すら凌駕するほど高速に動作できます。
binary32正規化数剰余演算
static float Modulo(float x, float y)
{
const int signBitOffset = 31;
const int expBitOffset = 23;
const int signPartMask = 1 << signBitOffset;
const int expPartMask = 0xFF << expBitOffset;
const int fracPartMask = (1 << expBitOffset) - 1;
const long economizedBit = 1L << expBitOffset;
const int exponentBits = signBitOffset - expBitOffset;
static void decompose(float x, out int sign, out int expo, out int frac)
{
var bin = Unsafe.As<float, int>(ref x);
sign = (bin & signPartMask) >> signBitOffset;
expo = (bin & expPartMask) >> expBitOffset;
frac = bin & fracPartMask;
}
static float scale(int sign, int expo, int frac)
{
var bin = (sign << signBitOffset) | (expo << expBitOffset) | frac;
return Unsafe.As<int, float>(ref bin);
}
decompose(x, out var s, out var n, out var a);
decompose(y, out var _, out var m, out var b);
var i = n - m;
if (i < 0)
{
return x;
}
var c = (uint)(a | economizedBit);
var d = (uint)(b | economizedBit);
while (i > exponentBits)
{
c <<= exponentBits;
c -= d * (c / d);
i -= exponentBits;
}
c <<= (int)i;
c -= d * (c / d);
i = 0;
if (c == 0)
{
return 0;
}
var eBitShift = BitOperations.LeadingZeroCount(c) - exponentBits;
i -= eBitShift;
c <<= eBitShift;
return scale(s, m + i, (int)c & fracPartMask);
}
本番においては正規化数以外の処理もサポートする必要がありますが、このアルゴリズム自体は非常に有用であると考えられます。
Discussion