正弦関数の例を使用した浮動小数点数の正確で高速な計算。はじめにとパート1

からの非常に良い記事を注意深く読んでください ArtemKaravaev浮動小数点数の追加について。このトピックは非常に興味深いものであり、これを続けて、実際に浮動小数点数を操作する方法の例を示したいと思います。 GNU glibcライブラリ(libm)を参照してください。そして、記事が退屈すぎないように、競争力のあるコンポーネントを追加します。繰り返すだけでなく、ライブラリコードを改善して、より速く/より正確にするようにします。



例として三角測量正弦関数を選択しました。これは広く普及している機能であり、その数学は学校や大学でよく知られています。同時に、それが実装されると、数字を使った「正しい」作業の多くの鮮やかな例があります。浮動小数点数としてdoubleを使用します。



このシリーズの記事では、数学からマシンコード、コンパイラオプションまで、多くのことが計画されています。記事を書くための言語はC ++ですが、「フリル」はありません。C言語とは対照的に、実際の例は、言語に精通していない人でも読みやすく、行数も少なくて済みます。



記事は浸漬法で書かれます。サブタスクについて説明し、問題の単一のソリューションにまとめます。



サインのテイラーシリーズへの分解。



正弦関数は無限のテイラーシリーズに拡張されます。



((バツ=バツ-バツ33+バツ-バツ77+バツナインナイン-



無限の合計の分析式がある場合を除いて、無限の系列を計算できないことは明らかです。しかし、これは私たちの場合ではありません)))間隔内の正弦を計算したいとします[0π2]..。パート3で、間隔を置いて作業について詳しく説明します。((π2=1 見積もり、条件に基づいて破棄できる最初の用語を見つける ((π/2nn<eどこ e数字の1と1より大きい最小の数字の差です。大まかに言えば、これはマンティッサの最後のビットです(wiki)。力ずくでこの方程式を解く方が簡単です。ためにe2.22××-16..。私はなんとかしたn=23すでにドロップすることができます。用語の数の正しい選択については、次のパートの1つで説明します。そのため、今日は「安全」で、用語を次のように取り上げます。n=25包括的。

最終項は約10,000分の1ですe..。



最も簡単な解決策



手はすでにかゆいです、私たちは書きます:



テスト用プログラムの全文
#include <iostream>
#include <iomanip>
#include <cmath>
#include <array>
#include <bitset>
#include <quadmath.h>
//      clang
//#include "/usr/lib/gcc/x86_64-linux-gnu/10/include/quadmath.h"
#include <numeric>
#include <limits>
#include <vector>

#include <boost/timer/timer.hpp>
#include <boost/math/special_functions/factorials.hpp>

namespace bm = boost::math;

using namespace std;

typedef union { uint32_t i[2]; double x; } mynumber;

array<double, 25> fc;

double sin_e1(double x) {
  double result = 0;
  int sign = 1;
  for(int i = 1; i < 25; i += 2) {
    result += sign * pow(x, i) / bm::unchecked_factorial<double>(i);
    sign = -sign;
  }
  return result;
}

double sin_e2(double x) {
  double result = 0;
  int sign = 1;
  double xx = x * x;
  double pw = x;
  double fti = 1.0;
  for(int i = 1; i < 25; i += 2) {
    fti /= i;
    result += sign * pw * fti;
    fti /= ( i + 1 );
    sign = -sign;
    pw  *= xx;
  }
  return result;
}

double sin_e3(double x) {
  double result = 0;
  for(int i = 25; i >= 1; i -= 2) {
    result += (((i - 1) % 4 == 0) ? 1 : -1 ) * pow(x, i) / bm::unchecked_factorial<double>(i);
  }
  return result;
}

double sin_e4(double x) {
  double xx = x * x;
  double res = fc[25];
  for(int i = 23; i >= 1; i -= 2) {
    res = fc[i] + xx * res;
  }
  return x * res;
}

double sin_e5(double x) {
  double xx = x * x;
  double res = fc[25];
  for(int i = 23; i >= 3; i -= 2) {
    res = fc[i] + xx * res;
  }
  return x + x * xx * res;
}

inline
double fsin(double x) {
  double result;
  asm ("fsin" :"=t" (result) : "0" (x));
  return result;
}

#define SIN(a) fsin(a)
//#define SIN(a) sin(a)
//#define SIN(a) sin_e5(a)
// ^^     . ^^

int main() {

  __uint128_t ft = 1;
  fc[1] = 1.0; //3 * 5;
  for(int i = 2; i < fc.size(); i++) {
    ft *= i;
    // factorial with sign for Taylor series
    fc[i] = (1.0 / ft) * (( (i - 2) % 4 < 2) ? -1 : 1);
  }
  vector<double> xv;
  xv.resize(8 * 2000000);
  //      0  M_PI/2
  for (int i = 0; i < xv.size(); i++) {
    //      .
    xv[i] = (M_PI / 2) * i / double(xv.size());
  }

  double res = 0;
  {
    boost::timer::auto_cpu_timer at;
    for(int i = 0; i < xv.size(); i++) {
      res += SIN(xv[i]);
    }
  }

  int co = 0, cn = 0;
  //      .
  __float128 avg = 0.0, div = 0.0;
  for(int i = 0; i < xv.size(); i++) {
    mynumber dold, dnew;
    dold.x = sin(xv[i]);
    dnew.x = SIN(xv[i]);
    __float128 q = sinq(xv[i]); // <= sinq  .
    __float128 dd = __float128(dnew.x) - q;
    //     .
    div += dd * dd;
    avg += dd;
    //  ,         .
    //   ,         .
    if( dold.i[0] != dnew.i[0] || dold.i[1] != dnew.i[1] ) {
      if( fabsq(q - dold.x) <= fabsq(q - dnew.x) )
        co++;
      else
        cn++;
    }
  }
  avg /= xv.size();
  div /= xv.size();

  cout << res << endl;

  //  ,           .
  cout << "Better libm: " <<  co << " / " << xv.size() << "(" << 100.0 * co / xv.size() << "%)" << endl;

  //  ,  ""         .
  cout << "Better new: " <<  cn << " / " << xv.size() << "(" << 100.0 * cn / xv.size() << "%)" << endl;

  //         .
  cout << "  Avg / std new: " << double(avg) << " / " << double(sqrtq( div - avg * avg )) << endl;
  return 0;
}








double sin_e1(double x) {
  double result = 0;
  int sign = 1;
  for(int i = 1; i < 25; i += 2) {
    result += sign * pow(x, i) / bm::factorial<double>(i);
    sign = -sign;
  }
  return result;
}


プログラムをスピードアップする方法は、多くの人がすぐに理解したと思います。あなたの変更がプログラムをスピードアップできると思いますか?最適化されたバージョンとスポイラーの下の質問への回答。



プログラムの最適化されたバージョン。
double sin_e2(double x) {
  double result = 0;
  int sign = 1;
  double xx = x * x;
  double pw = x;
  double fti = 1.0;
  for(int i = 1; i < 25; i += 2) {
    fti /= i;
    result += sign * pw * fti;
    fti /= ( i + 1 );
    sign = -sign;
    pw  *= xx;
  }
  return result;
}


10000 (GNU C++ v10; -O2)



精度の向上



方法論



関数の計算精度は、2つの標準パラメーターによって決定されます。



真の罪(float128)からの標準偏差とこの偏差の平均。最後のパラメーターは、関数の動作に関する重要な情報を提供できます。彼女は体系的に結果を過小評価または過大評価する可能性があります。



これらのパラメータに加えて、さらに2つ紹介します。関数と一緒に、ライブラリに組み込まれているsin(double)関数を呼び出します。2つの関数の結果(oursと組み込み関数)が(ビットごとに)一致しない場合、2つの関数のどちらが真の値から遠いのかを統計に追加します。



合計順序



元の例に戻りましょう。どうすればその精度を「すばやく」向上させることができますか?記事をよく読んだ方ダブルタイプのN個を最も正確に足すことができますか?おそらくすぐに答えを出すでしょう。サイクルを反対方向に回す必要があります。最小のモジュロから最大のモジュロに追加します。



double sin_e3(double x) {
  double result = 0;
  for(int i = 25; i >= 1; i -= 2) {
    result += (((i - 1) % 4 == 0) ? 1 : -1 ) * pow(x, i) / bm::unchecked_factorial<double>(i);
  }
  return result;
}


結果を表に示します。



関数 平均誤差 STD 私たちの方が良い より良いlibm
sin_e1 -1.28562e-18 8.25717e-17 0.0588438% 53.5466%
sin_e3 -3.4074e-21 3.39727e-17 0.0423% 10.8049%
sin_e4 8.79046e-18 4.77326e-17 0.0686% 27.6594%
sin_e5 8.78307e-18 3.69995e-17 0.0477062% 13.5105%


スマート加算アルゴリズムを使用すると、エラーがほぼ0に除去されるように見えるかもしれませんが、そうではありません。もちろん、これらのアルゴリズムは精度を向上させますが、エラーを完全に取り除くには、スマート乗算アルゴリズムも必要です。それらは存在しますが、非常に高価です。不要な操作がたくさんあります。それらの使用はここでは正当化されません。ただし、後で別のコンテキストでそれらに戻ります。



残りはほとんどありません。高速で正確なアルゴリズムを組み合わせます。これを行うには、テイラーシリーズに戻りましょう。たとえば、4つのメンバーに制限して、次の変換を行います。



((バツバツ((1+バツ2((-1/3+バツ2((1/+バツ2((-1/7+バツ21/ナイン





括弧を展開して、元の式が得られていることを確認できます。このビューは、ループに簡単に収まります。



double sin_e4(double x) {
  double xx = x * x;
  double res = fc[25];
  for(int i = 23; i >= 1; i -= 2) {
    res = fc[i] + xx * res;
  }
  return x * res;
}


高速に動作しますが、e3と比較して精度が低下します。繰り返しますが、丸めは問題です。ループの最後のステップを見て、元の式を少し変換してみましょう。

((バツバツ+バツバツ2((-1/3+





そして対応するコード。



double sin_e5(double x) {
  double xx = x * x;
  double res = fc[25];
  for(int i = 23; i >= 3; i -= 2) {
    res = fc[i] + xx * res;
  }
  return x + x * xx * res;
}


精度はlibmと比較して2倍になりました。精度が向上した理由が推測できる場合は、コメントに書き込んでください。さらに、sin_e5にはないsin_e4について、精度に関連して、もう1つはるかに不快なことがあります。問題が何であるかを推測してみてください。次のパートでは、間違いなくそれについて詳しく説明します。



この記事が気に入ったら、次の記事で、GNUlibcが最大ULP0.548のサインを計算する方法を説明します。



All Articles