エラーフリー変換の紹介 および FastTwoSum アルゴリズム の紹介と証明 -- glibc のコードを読むための参考に --
小清水 (@curekoshimizu) です。
仕事が忙しくなかなか更新する余力がありませんでしたが一年半振りにブログを更新します!
エラーフリー変換とは?
浮動小数点数の計算というのは、このブログで何度も紹介していますが、 丸め誤差 を伴います。
簡単な という計算であっても を行ったあとに浮動小数点に変換されるために丸め誤差が発生する可能性があるのです。 その他にも
などなど各種計算も同様に計算過程で丸め誤差をともないます。
つまり
例えば
において を float32 で計算すると
となり、誤差 が生じます。
この 誤差項 も含めて結果を得たいというケースがあるのです。
これを行うのが エラーフリー変換 (Error Free Transformation) と呼ばれる計算です。
こんなこと本当にしたいのでしょうか?
一つの例としてみなさんがお世話になっている glibc でも使われているのだというのを見てみましょう。
glibc でエラーフリー変換が使われている例
たとえば glibc では ソフトウェアFMA を計算するために次のような処理をしています。
https://github.com/lattera/glibc/blob/master/sysdeps/ieee754/dbl-64/s_fma.c の LN188 - LN205 あたり。
/* Multiplication m1 + m2 = x * y using Dekker's algorithm. */ #define C ((1 << (DBL_MANT_DIG + 1) / 2) + 1) double x1 = x * C; double y1 = y * C; double m1 = x * y; x1 = (x - x1) + x1; y1 = (y - y1) + y1; double x2 = x - x1; double y2 = y - y1; double m2 = (((x1 * y1 - m1) + x1 * y2) + x2 * y1) + x2 * y2; /* Addition a1 + a2 = z + m1 using Knuth's algorithm. */ double a1 = z + m1; double t1 = a1 - z; double t2 = a1 - t1; t1 = m1 - t1; t2 = z - t2; double a2 = t1 + t2;
ここで登場するコメントにも書かれている Dekker の乗算アルゴリズムやKnuth の加算アルゴリズム
はエラーフリー変換の一つです。
/* Multiplication m1 + m2 = x * y using Dekker's algorithm. */ #define C ((1 << (DBL_MANT_DIG + 1) / 2) + 1) double x1 = x * C; double y1 = y * C; double m1 = x * y; x1 = (x - x1) + x1; y1 = (y - y1) + y1; double x2 = x - x1; double y2 = y - y1; double m2 = (((x1 * y1 - m1) + x1 * y2) + x2 * y1) + x2 * y2;
例えばこの箇所は x*y
を計算する m1 = x * y
という計算によってどの程度の誤差が生じたのかを知るために
m2
を厳密に計算しています。
これにより
m1 + m2 = x * y
となることを数学的に保証するというものです。
こちらの加算も同様で
/* Addition a1 + a2 = z + m1 using Knuth's algorithm. */ double a1 = z + m1; double t1 = a1 - z; double t2 = a1 - t1; t1 = m1 - t1; t2 = z - t2; double a2 = t1 + t2;
a1 = z1 + m1
で求めたい加算結果を得て、それによって生じた誤差を a2
で手に入れています。
乗算に関するエラーフリー変換と FMA 命令
上の Dekker の乗算アルゴリズムは FMA 命令がないアーキテクチャのためのアルゴリズムとして知られています。
もし FMA命令 (xy+zを計算する命令) を持っている場合には
/* Multiplication m1 + m2 = x * y using FMA instruction. */ double m1 = x * y; double m2 = x * y - m1;
これでエラーフリー変換できてしまいます。 そのため、現代のアーキテクチャで、 この Dekker の乗算アルゴリズムを実際に使う機会は少ないかもしれません。
FMA についてはこちらで詳しく記述しました:
加算に関するエラーフリー変換
一方で加算はそうはいきません。FMA命令では解決できないのです。
歴史 -- FastTwoSum(Fast2Sum)・TwoSum アルゴリズム
1971 年に Dekker に紹介された FastTwoSum アルゴリズム というアルゴリズムがあります。(T.J. Dekker: A floating-point technique for extending the available precision)
これが加算に関するエラーフリー変換として最も古いアルゴリズムです。 とはいえ、
1965 年に Kahan が総和演算に関する Compensated summation アルゴリズムに関する論文の中でこのアルゴリズムを暗に利用しているのも事実ですので、 最古といっていいか悩ましいところはあります…。(https://convexoptimization.com/TOOLS/Kahan.pdf)
この Dekker のアリゴリズムを FastTwoSum と名付けたのは Shewchuk の 1997年の論文です:
ただし、この FastTwoSum は上記通り、条件がついており、引数a, bについて大小比較をする必要 があります。
このため、現在のアーキテクチャの高速化技法等を考えると、条件分岐が発生するこのアルゴリズムよりも、
KunuthとMøllerによる条件分岐の発生しない加算エラーフリー変換である、 TwoSum アルゴリズム のほうが一般的に利用されています。
上の glibc で書かれていた
/* Addition a1 + a2 = z + m1 using Knuth's algorithm. */ double a1 = z + m1; double t1 = a1 - z; double t2 = a1 - t1; t1 = m1 - t1; t2 = z - t2; double a2 = t1 + t2;
この式はこのアルゴリズムです。
命名は先ほど登場した shewchuk の論文です。
論文中に FastTwoSum(a, b)
, Fast2Sum(a, b)
, TwoSum(a, b)
, 2Sum(a, b)
などと登場したらこれらのアルゴリズムのことなんだなと思うと良いと思います。
FastTwoSum アルゴリズムの紹介と証明
今回のブログではこの古典的な 加算エラーフリー変換アルゴリズムである Dekker の FastTwoSum アルゴリズムについて紹介と証明を与えてみようと思います。
FastTwoSum アルゴリズム
次の計算により の場合に なる が得られます。ただしここでは2進数浮動小数点環境とする:
ただし は最近接偶数方向丸めを表します。この最近接偶数方向丸めについては次の記事を参照ください:
FastTwoSum アルゴリズム証明
ここで
とおく、ただし かつ < , とする。
ここで、次のブログの Prop. (整数を用いた 進 桁の浮動小数点数の表示) を使用した。 この証明ではこの性質を頻繁につかっている。
また、
ここから、 と のとり方の自由度はあるものの、 と制限した上で を定めても問題ないことがわかる。
ここで次の3つにわけて考える
Case1. (すなわち )
Case2. かつ
Case3. かつ
このとき、
が浮動小数点として正確に表現できることを示し、 と表せることを示す。
Case1. のとき
このとき
となり、
と、 は で抑えられる実数として表現される。
最近接偶数方向丸めが であるから、先の項を整数化することになる。よって
と評価でき、
これより次の式が得られる:
ゆえに
から、この は整数であって、先の評価から 未満の場合、 は浮動小数点表示される。もちろん に一致する場合も、指数部を に修正しても、整数部は丸めによる誤差は生じないので、浮動小数点表示されていることがわかる。
そしてこの表示から より と表されることもわかる。
Case2. かつ のとき
ここで
最近接遇数方向の定義より
ここから、
が得られる。 と整数部を と表すと
となることから、 は浮動小数点表示されていることがわかる。また上評価より指数部は であることがわかっており、 であるから と表されることもわかる。
Case3. かつ のとき
ここで かつ であるから は整数となる。
また、 の丸めが であるから、
が整数であることや、 を比較して考えると、
丸めの定義を考えればそれによって値が変わることはなく、
となることがわかる。
ゆえに、 が示され、
であり、 は浮動小数点表示されていることがわかる。また、 であることもわかる。
ここまでわかったことまとめ
は浮動小数点表示でき、 となることがわかった。
ゆえに
であることがわかった。
最後に
であることを示せれば、 とエラーフリー変換であることが示される。
の証明
であるから
である。これは、 が に関して浮動小数点の中で最良近似であるからである。
ゆえに が示された。
ここで と表すことができたので その丸めである も と表される。これは、丸めが入るかもしれないが、整数部が変化するのみであることからわかる。
よって についても同様であり、 と整数 を定義すれば
であることから < であり、 は浮動小数点表示できる。
すなわち が示され、証明完。
まとめ
今回のブログでは エラーフリー変換 について紹介をしました。
そのエラーフリー変換として有名で glibc でも使われている 加算と乗算のエラーフリー変換を紹介しその歴史を紹介し、
さらに、そのエラーフリー変換の証明方法の例として古典的な Dekker のFastTwoSum を紹介しました。
この記事を書くにあたって、 kashi さんのブログを拝見し、TwoSum アルゴリズムで 気をつけるべき例 (途中でオーバーフローが発生する場合にエラーフリーとならないこと) が紹介されていて面白いなと思いました。
いつか私も TwoSum アルゴリズムの証明を読んでみたいと思います。
(追記)
調べると TwoSum アルゴリズムは、アンダーフローやオーバーフローが途中で起こらない限りという条件がついていることがわかりました。
そのため、Dekker の FastTwoSum に比べて条件が厳しいことがわかりました。
参考: