まえがき
ビデオカードプロセッサのリアルタイムで精度を高めてアークを計算する必要がありました。
著者は、標準関数System.Math.Sin()(C#)を超える目標を設定せず、それに到達しませんでした。
仕事の結果と私の選択(読みたくない人のために):
Sin_3(ラッド)
using System; class Math_d { const double PI025 = Math.PI / 4; /// <summary> 2^17 = 131072 (1 ), 10000 ( ), 2^21 = 22097152 (16 ) +-1 ( ) ( ) </summary> const int length_mem = 22097152; const int length_mem_M1 = length_mem - 1; /// <summary> sin, . </summary> static double[] mem_sin; /// <summary> cos, . </summary> static double[] mem_cos; /// <summary> , sin, . </summary> public static void Initialise() { Ini_Mem_Sin(); Ini_Mem_Cos(); } /// <summary> Cos, . </summary> /// <param name="rad"></param> public static double Sin_3(double rad) { double rad_025; int i; // //if (rad < 0) { rad = -rad + Math.PI; } i = (int)(rad / PI025); // rad_025 = rad - PI025 * i; // ( ) i = i & 7; // 8 // switch (i) { case 0: return Sin_Lerp(rad_025); case 1: return Cos_Lerp(PI025 - rad_025); case 2: return Cos_Lerp(rad_025); case 3: return Sin_Lerp(PI025 - rad_025); case 4: return -Sin_Lerp(rad_025); case 5: return -Cos_Lerp(PI025 - rad_025); case 6: return -Cos_Lerp(rad_025); case 7: return -Sin_Lerp(PI025 - rad_025); } return 0; } /// <summary> sin </summary> static void Ini_Mem_Sin() { double rad; mem_sin = new double[length_mem]; for (int i = 0; i < length_mem; i++) { rad = (i * PI025) / length_mem_M1; mem_sin[i] = Math.Sin(rad); } } /// <summary> cos </summary> static void Ini_Mem_Cos() { double rad; mem_cos = new double[length_mem]; for (int i = 0; i < length_mem; i++) { rad = (i * PI025) / length_mem_M1; mem_cos[i] = Math.Cos(rad); } } /// <summary> sin 0 pi/4. </summary> /// <param name="rad"> 0 pi/4. </param> static double Sin_Lerp(double rad) { int i_0; int i_1; double i_0d; double percent; double a; double b; double s; percent = rad / PI025; i_0d = percent * length_mem_M1; i_0 = (int)i_0d; i_1 = i_0 + 1; a = mem_sin[i_0]; b = mem_sin[i_1]; s = i_0d - i_0; return Lerp(a, b, s); } /// <summary> cos 0 pi/4. </summary> /// <param name="rad"> 0 pi/4. </param> static double Cos_Lerp(double rad) { int i_0; int i_1; double i_0d; double percent; double a; double b; double s; percent = rad / PI025; i_0d = percent * length_mem_M1; i_0 = (int)i_0d; i_1 = i_0 + 1; a = mem_cos[i_0]; b = mem_cos[i_1]; s = i_0d - i_0; return Lerp(a, b, s); } /// <summary> . (return a + s * (b - a)) </summary> /// <param name="a"> . </param> /// <param name="b"> . </param> /// <param name="s"> . 0 = a, 1 = b, 0.5 = a b. </param> public static double Lerp(double a, double b, double s) { return a + s * (b - a); } }
出版の理由
- HLSL言語にはdoubleの標準的なSin関数はありません(ただし、これは正確ではありません)
- このトピックに関する情報は、インターネット上でほとんど入手できません。
考慮されたアプローチ
- テイラーシリーズ(ウィキペディア)
- 多項式( 関数作成者: "asvp" )
- Math.SinおよびMath.Cosの事前計算結果の線形補間(著者:)
分析されたパラメーター
- Math.Sinによる精度
- Math.Sinに関連する速度
分析に加えて、パフォーマンスを改善します。
テイラーランク
長所:
- 最高の精度Sin値の計算に使用されるこの関数は、 無限に正確な Sin 値の計算に使用できます 。 繰り返される回数が多いほど、出力でより正確に値が取得されます(仮説)。 プログラミングの実践では、使用するパラメーターのタイプ(double、float、decimalなど)に応じて計算の丸め誤差を考慮する価値があります。
- 任意の角度を計算します関数の引数として任意の値を入力できるため、入力パラメーターを監視する必要はありません。
- 独立(以下で説明する関数のように)予備計算を必要とせず、多くの場合、より高速な関数が組み立てられるベースになります。
短所:
- 非常に低速(4-10%)精度がMath.Sinの精度に近づくには、多くの反復が必要です。その結果、標準関数よりも25倍遅く動作します。
- 角度が大きいほど、精度が低くなります関数に入力された角度が大きいほど、Math.Sinと同じ精度を達成するためにより多くの反復が必要になります。
元の外観(速度:4%):
標準関数には、階乗の計算と、反復ごとの累乗が含まれます。
変更(速度:10%):
ある程度の計算はサイクル(a * = aa;)で削減でき、他の階乗は事前に計算して配列に入れることができますが、符号(+、-、+、...)を変更しても累乗にはならず、計算も削減できます以前の値を使用します。
結果は次のコードです。
罪(ラッド、ステップ)
// <summary> , Fact </summary> static double[] fact; /// <summary> . /// rad, . /// ( Math): 4% (fps) steps = 17 </summary> /// <param name="rad"> . pi/4. </param> /// <param name="steps"> : , . pi/4 E-15 8. </param> public static double Sin(double rad, int steps) { double ret; double a; //, double aa; // * int i_f; // int sign; // ( - +, = +) ret = 0; sign = -1; aa = rad * rad; a = rad; i_f = 1; // for (int n = 0; n < steps; n++) { sign *= -1; ret += sign * a / Fact(i_f); a *= aa; i_f += 2; } return ret; } /// <summary> (n!). n > fact.Length, -1. </summary> /// <param name="n"> , . </param> public static double Fact(int n) { if (n >= 0 && n < fact.Length) { return fact[n]; } else { Debug.Log(" . n = " + n + ", = " + fact.Length); return -1; } } /// <summary> . </summary> static void Init_Fact() { int steps; steps = 46; fact = new double[steps]; fact[0] = 1; for (int n = 1; n < steps; n++) { fact[n] = fact[n - 1] * n; } }
スーペリアビュー(速度:19%):
角度が小さいほど、反復が少なくて済むことがわかります。 必要な最小角度= 0.25 * PI、つまり 45度。 45度の領域のSinとCosを考慮すると、Sinの-1〜1のすべての値を取得できます(2 * PIの領域)。 これを行うために、円(2 * PI)を8つの部分に分割し、各部分に対してサインを計算する独自の方法を示します。 さらに、計算を高速化するために、余り(%)を取得する機能(45度のゾーン内の角度の位置を取得する)の使用を拒否します。
Sin_2(rad)
// <summary> , Fact </summary> static double[] fact; /// <summary> Sin </summary> /// <param name="rad"></param> public static double Sin_2(double rad) { double rad_025; int i; //rad = rad % PI2; //% - . , fps 90 150 ( 100 000 ) //rad_025 = rad % PI025; i = (int)(rad / PI025); rad_025 = rad - PI025 * i; i = i & 7; // 8 // switch (i) { case 0: return Sin(rad_025, 8); case 1: return Cos(PI025 - rad_025, 8); case 2: return Cos(rad_025, 8); case 3: return Sin(PI025 - rad_025, 8); case 4: return -Sin(rad_025, 8); case 5: return -Cos(PI025 - rad_025, 8); case 6: return -Cos(rad_025, 8); case 7: return -Sin(PI025 - rad_025, 8); } return 0; } /// <summary> . /// rad, . /// ( Math): 10% (fps) steps = 17 </summary> /// <param name="rad"> . pi/4. </param> /// <param name="steps"> : , . pi/4 E-15 8. </param> public static double Sin(double rad, int steps) { double ret; double a; //, double aa; // * int i_f; // int sign; // ( - +, = +) ret = 0; sign = -1; aa = rad * rad; a = rad; i_f = 1; // for (int n = 0; n < steps; n++) { sign *= -1; ret += sign * a / Fact(i_f); a *= aa; i_f += 2; } return ret; } /// <summary> . /// rad, . /// ( Math): 10% (fps), 26% (test) steps = 17 </summary> /// <param name="rad"> . pi/4. </param> /// <param name="steps"> : , . pi/4 E-15 8. </param> public static double Cos(double rad, int steps) { double ret; double a; double aa; // * int i_f; // int sign; // ( - +, = +) ret = 0; sign = -1; aa = rad * rad; a = 1; i_f = 0; // for (int n = 0; n < steps; n++) { sign *= -1; ret += sign * a / Fact(i_f); a *= aa; i_f += 2; } return ret; } /// <summary> (n!). n > fact.Length, -1. </summary> /// <param name="n"> , . </param> public static double Fact(int n) { if (n >= 0 && n < fact.Length) { return fact[n]; } else { Debug.Log(" . n = " + n + ", = " + fact.Length); return -1; } } /// <summary> . </summary> static void Init_Fact() { int steps; steps = 46; fact = new double[steps]; fact[0] = 1; for (int n = 1; n < steps; n++) { fact[n] = fact[n - 1] * n; } }
多項式
インターネットでこの方法に出くわしました。著者は、事前に計算された値のライブラリを使用せずに、低精度(エラー<0.000 001)の2倍の高速Sin検索機能を必要としていました。
長所:
- 高速(9-84%)最初に、変更なしでスローされた多項式は、元のMath.Sinの9%の速度を示しました。これは10倍遅いです。 小さな変更のおかげで、速度は84%まで急激に上昇します。これは、目を正確に閉じても悪くありません。
- 追加の予備計算とメモリは不要上と下で計算を高速化するために変数の配列を構成する必要がある場合、ここですべての主要な係数が親切に計算され、作成者自身が定数の形で式に入れました。
- Mathf.Sin(float)よりも高い精度比較のために:
0.84147 1 -Mathf.Sin(1)(Unityエンジン);
0.841470984807897-Math.Sin(1)(標準C#関数);
0.8414709 56802368 -sin(1)(GPU、hlsl言語);
0.84147 1184637935 - Sin_0(1) 。
短所:
- 普遍的ではない著者がこの多項式を計算するために使用したツールが不明であるため、手動で精度を調整することはできません。
- なんで?なぜ著者は、配列を必要とせず、そのような低い(2倍に比べて)精度の関数を必要としたのですか?
元のビュー:
Sin_1(x)
/// <summary> ( Math): 9% (fps)</summary> /// <param name="x"> -2*Pi 2*Pi </param> public static double Sin_1(double x) { return 0.9999997192673006 * x - 0.1666657564532464 * Math.Pow(x, 3) + 0.008332803647181511 * Math.Pow(x, 5) - 0.00019830197237204295 * Math.Pow(x, 7) + 2.7444305061093514e-6 * Math.Pow(x, 9) - 2.442176561869478e-8 * Math.Pow(x, 11) + 1.407555708887347e-10 * Math.Pow(x, 13) - 4.240664814288337e-13 * Math.Pow(x, 15); }
スーペリアビュー:
Sin_0(rad)
/// <summary> ( Math): 83% (fps)</summary> /// <param name="rad"> -2*Pi 2*Pi </param> public static double Sin_0(double rad) { double x; double xx; double ret; xx = rad * rad; x = rad; //1 ret = 0.9999997192673006 * x; x *= xx; //3 ret -= 0.1666657564532464 * x; x *= xx; //5 ret += 0.008332803647181511 * x; x *= xx; //7 ret -= 0.00019830197237204295 * x; x *= xx; //9 ret += 2.7444305061093514e-6 * x; x *= xx; //11 ret -= 2.442176561869478e-8 * x; x *= xx; //13 ret += 1.407555708887347e-10 * x; x *= xx; //15 ret -= 4.240664814288337e-13 * x; return ret; }
線形補間
この方法は、配列内の2つのレコードの結果間の線形補間に基づいています。
エントリはmem_sinとmem_cosに分割され、0〜0.25 * PIの入力パラメータのセグメントでの標準関数Math.SinとMath.Cosの事前計算結果が含まれています。
角度が0〜45度の操作の原則は、改良版のテイラー級数と違いはありませんが、同時に、2つのレコードの間に角度があり、それらの間の値を見つける関数が呼び出されます。
長所:
- 高速(65%)補間アルゴリズムの単純さにより、速度はMath.Sinの速度の65%に達します。 33%を超える速度で十分だと思います。
- 最高の精度拒否のまれなケースの例:
0.255835595715180-Math.Sin;
0.2558355957151 79 - Sin_3 。
- 速い足この機能は苦労して生まれたもので、私が書いたものであり、要件を超えています。速度> 33%、1e-14以上の精度です。 彼女に誇らしげな名前を付けます-VēlōxPes。
短所:
- メモリ内の場所が必要です動作するには、まず2つの配列を計算する必要があります。sinとcosの場合です。 各アレイの重量は約16mb(16 * 2 = 32mb)
元のビュー:
Sin_3(ラッド)
class Math_d { const double PI025 = Math.PI / 4; /// <summary> 2^17 = 131072 (1 ), 10000 ( ), 2^21 = 22097152 (16 ) +-1 ( ) ( ) </summary> const int length_mem = 22097152; const int length_mem_M1 = length_mem - 1; /// <summary> sin, . </summary> static double[] mem_sin; /// <summary> cos, . </summary> static double[] mem_cos; /// <summary> , sin, . </summary> public static void Initialise() { Ini_Mem_Sin(); Ini_Mem_Cos(); } /// <summary> Cos, . </summary> /// <param name="rad"></param> public static double Sin_3(double rad) { double rad_025; int i; // //if (rad < 0) { rad = -rad + Math.PI; } i = (int)(rad / PI025); // rad_025 = rad - PI025 * i; // ( ) i = i & 7; // 8 // switch (i) { case 0: return Sin_Lerp(rad_025); case 1: return Cos_Lerp(PI025 - rad_025); case 2: return Cos_Lerp(rad_025); case 3: return Sin_Lerp(PI025 - rad_025); case 4: return -Sin_Lerp(rad_025); case 5: return -Cos_Lerp(PI025 - rad_025); case 6: return -Cos_Lerp(rad_025); case 7: return -Sin_Lerp(PI025 - rad_025); } return 0; } /// <summary> sin </summary> static void Ini_Mem_Sin() { double rad; mem_sin = new double[length_mem]; for (int i = 0; i < length_mem; i++) { rad = (i * PI025) / length_mem_M1; mem_sin[i] = Math.Sin(rad); } } /// <summary> cos </summary> static void Ini_Mem_Cos() { double rad; mem_cos = new double[length_mem]; for (int i = 0; i < length_mem; i++) { rad = (i * PI025) / length_mem_M1; mem_cos[i] = Math.Cos(rad); } } /// <summary> sin 0 pi/4. </summary> /// <param name="rad"> 0 pi/4. </param> static double Sin_Lerp(double rad) { int i_0; int i_1; double i_0d; double percent; double a; double b; double s; percent = rad / PI025; i_0d = percent * length_mem_M1; i_0 = (int)i_0d; i_1 = i_0 + 1; a = mem_sin[i_0]; b = mem_sin[i_1]; s = i_0d - i_0; return Lerp(a, b, s); } /// <summary> cos 0 pi/4. </summary> /// <param name="rad"> 0 pi/4. </param> static double Cos_Lerp(double rad) { int i_0; int i_1; double i_0d; double percent; double a; double b; double s; percent = rad / PI025; i_0d = percent * length_mem_M1; i_0 = (int)i_0d; i_1 = i_0 + 1; a = mem_cos[i_0]; b = mem_cos[i_1]; s = i_0d - i_0; return Lerp(a, b, s); } /// <summary> . (return a + s * (b - a)) </summary> /// <param name="a"> . </param> /// <param name="b"> . </param> /// <param name="s"> . 0 = a, 1 = b, 0.5 = a b. </param> public static double Lerp(double a, double b, double s) { return a + s * (b - a); } }
UPD:Sin_Lerp()、Cos_Lerp()、Ini_Mem_Sin()、およびIni_Mem_Cos()でインデックスを決定する際のエラーを修正しました。