なお、Visual C++の場合のみ実装が異なるのは、以下の理由による。
また、fesetroundや_controlfp_sでは、
-DKV_FASTROUNDを付けると、この2つを行なうことによって高速化する。ただし、 IntelのCPUで64bitモードで動作している場合しか使うことは出来ない。
実装の詳細はkv/hwround.hppを見て欲しい。
アルゴリズムの詳細は、 emu.pdf (2014/6/9版, 2024/4/27に微修正) を見て欲しい。
実装の詳細は
[注意] -DKV_NOHWROUNDによる方向付き丸めのエミュレートは、 doubleの演算がIEEE754に準拠していることを前提 に設計されている。多くの標準的な環境で動作するが、 例えばIntel CPUの32bit環境はIEEE754に従わないことがあるので、 本ライブラリはコンパイルできないようになっている (version 0.4.54から)。 具体的には、#include <cfloat>した後マクロFLT_EVAL_METHODを調べ、 それが0でないならばエラーでコンパイルできないようにした。 Intel CPUの32bit環境でどうしてもKV_NOHWROUNDを使いたい場合は、 コンパイルオプションとして -msse2 -mfpmath=sse を付けるなどして SSE2を使わせるようにすると、上記マクロが0になってコンパイルできるようになる。 このマクロが定義されないコンパイラ、あるいはSSE2を持たないCPUは、 古すぎるためサポートされない。
実装の詳細は
add_up: OK add_down: OK sub_up: OK sub_down: OK mul_up: OK mul_down: OK div_up: OK div_down: OK sqrt_up: OK sqrt_down: OKのように表示される。
#include <fenv.h> static double add_up(const double& x, const double& y) { volatile double r, x1 = x, y1 = y; fesetround(FE_UPWARD); r = x1 + y1; fesetround(FE_TONEAREST); return r; }
#include <emmintrin.h> unsigned long int reg = _mm_getcsr(); unsigned long int nearest = (reg & ~0x00006000) | 0x00000000; unsigned long int down = (reg & ~0x00006000) | 0x00002000; unsigned long int up = (reg & ~0x00006000) | 0x00004000; unsigned long int chop = (reg & ~0x00006000) | 0x00006000; static double add_up(const double& x, const double& y) { volatile double r, x1 = x, y1 = y; _mm_setcsr(up); r = x1 + y1; _mm_setcsr(nearest); return r; }
#include <cmath> #include <limits> static void twosum(const double& a, const double& b, double& x, double& y) { double tmp; x = a + b; if (std::fabs(a) > std::fabs(b)) { tmp = x - a; y = b - tmp; } else { tmp = x - b; y = a - tmp; } } static double succ(const double& x) { static const double th1 = std::ldexp(1., -969); static const double th2 = std::ldexp(1., -1021); static const double c1 = std::ldexp(1., -53) + std::ldexp(1., -105); static const double c2 = std::ldexp(1., -1074); static const double c3 = std::ldexp(1., 53); static const double c4 = std::ldexp(1., -53); double a, c, e; a = std::fabs(x); if (a >= th1) return x + a * c1; if (a < th2) return x + c2; c = c3 * x; e = c1 * std::fabs(c); return (c + e) * c4; } static double add_up(const double& x, const double& y) { double r, r2; twosum(x, y, r, r2); if (r == std::numeric_limits<double>::infinity()) { return r; } else if (r == -std::numeric_limits<double>::infinity()) { if (x == -std::numeric_limits≤double>::infinity() || y == -std::numeric_limits::infinity()) { return r; } else { return -(std::numeric_limits≤double>::max)(); } } if (r2 > 0.) { return succ(r); } return r; }
#include <immintrin.h> static double add_up(const double& x, const double& y) { volatile __m128d x1, y1, z1; double r; x1 = _mm_set_sd(x); y1 = _mm_set_sd(y); z1 = _mm_add_round_sd(x1, y1, _MM_FROUND_TO_POS_INF | _MM_FROUND_NO_EXC); _mm_store_sd(&r, z1); return r; }
static void split(const double& a, double& x, double& y) { static const double sigma = (double)((1L << 27) + 1); double tmp; tmp = a * sigma; x = tmp - (tmp - a); y = a - x; } static void twoproduct(const double& a, const double& b, double& x, double& y) { static const double th = std::ldexp(1., 996); static const double c1 = std::ldexp(1., -28); static const double c2 = std::ldexp(1., 28); static const double th2 = std::ldexp(1., 1023); double na, nb, a1, a2, b1, b2; x = a * b; if (std::fabs(a) > th) { na = a * c1; nb = b * c2; } else if (std::fabs(b) > th) { na = a * c2; nb = b * c1; } else { na = a; nb = b; } split(na, a1, a2); split(nb, b1, b2); if (std::fabs(x) > th2) { y = ((((a1 * 0.5) * b1 - (x * 0.5)) * 2 + a2 * b1) + a1 * b2) + a2 * b2; } else { y = (((a1 * b1 - x) + a2 * b1) + a1 * b2) + a2 * b2; } }
static void twoproduct(const double& a, const double& b, double& x, double& y) { x = a * b; y = fma(a, b, -x); }FMA命令の使用については、IntelのHaswell以降のCPUとあまり古くないOS、 コンパイラなら大丈夫と思うが、以下の手順でチェックして欲しい。 また不具合があれば報告して欲しい。
使用した計算環境は、
Intel Core i5 11400 2.6GHz/4.4GHz, Memory 32Gである。
Ubuntu 22.04 LTS
gcc 11.2.0
なお、前節で述べたように -DKV_USE_TPFMA オプションを使うと 計算速度が改善する場合があり、その場合は両者の計算時間を記した。
計算精度(端点の型) | コンパイルオプション (-O3 -DNDEBUGに加えて) |
計算時間 | |
---|---|---|---|
-DKV_USE_TPFMAなし | -DKV_USE_TPFMAあり | ||
double | なし | 6.65 sec | |
-DKV_FASTROUND | 3.74 sec | ||
-DKV_NOHWROUND | 7.88 sec | 6.05 sec | |
-DKV_USE_AVX512 -mavx512f | 2.74 sec | ||
dd | なし | 37.20 sec | 33.58 sec |
-DKV_FASTROUND | 25.08 sec | 21.52 sec | |
-DKV_NOHWROUND | 92.24 sec | 71.81 sec | |
-DKV_USE_AVX512 -mavx512f | 16.47 sec |
続いて、M1 MacBook Airにおけるベンチマーク結果を記載する。 現状ではARMのCPUではハードウェアによる丸めモード変更の実行時間に おけるペナルティが大きいようで、エミュレーションが一番速いという 残念な結果になっている。
使用した計算環境は、
Apple M1 MacBook Air 3.2GHz, Memory 16Gである。
macOS Monterey 12.4
gcc 12.2.0
計算精度(端点の型) | コンパイルオプション (-O3 -DNDEBUGに加えて) |
計算時間 | |
---|---|---|---|
-DKV_USE_TPFMAなし | -DKV_USE_TPFMAあり | ||
double | なし | 24.45 sec | |
-DKV_FASTROUND | 22.36 sec | ||
-DKV_NOHWROUND | 9.08 sec | 6.03 sec | |
dd | なし | 108.8 sec | 100.6 sec |
-DKV_FASTROUND | 102.4 sec | 94.64 sec | |
-DKV_NOHWROUND | 84.60 sec | 53.76 sec |