web-dev-qa-db-ja.com

Cはどのようにsin()およびその他の数学関数を計算しますか?

私は.NETの分解とGCCソースコードを熟読してきましたが、sin()や他の数学関数の実際の実装をどこにも見つけられないようです...彼らは常に何か他のものを参照しているようです。

誰も私がそれらを見つけるのを手伝ってくれますか? Cが実行するすべてのハードウェアがハードウェアのトリガー機能をサポートすることはまずないと思われるので、ソフトウェアアルゴリズムが必要ですsomewhere、そうですか?


私は関数canを計算する方法をいくつか知っており、楽しみのためにtaylorシリーズを使用して関数を計算する独自のルーチンを作成しました。私のアルゴリズムはかなり賢い(明らかにそうではない)と思っていても、すべての実装は常に数桁遅いため、本物の生産言語がどのようにそれを行うのか興味があります。

223
Hank

GNU libmでは、sinname__の実装はシステムに依存します。したがって、各プラットフォームの実装は、 sysdeps の適切なサブディレクトリのどこかにあります。

1つのディレクトリには、IBMから提供されたCの実装が含まれています。 2011年10月以降、これは典型的なx86-64 Linuxシステムでsin()を呼び出したときに実際に実行されるコードです。明らかにfsinname__アセンブリ命令よりも高速です。ソースコード: sysdeps/ieee754/dbl-64/s_sin.c__sin (double x)を探します。

このコードは非常に複雑です。 1つのソフトウェアアルゴリズムが可能な限り高速で、x値の範囲全体にわたって正確でもないため、ライブラリは多くの異なるアルゴリズムを実装し、その最初の仕事はxを見て、使用するアルゴリズムを決定します。一部の地域では、おなじみのテイラー級数のように見えるものを使用します。いくつかのアルゴリズムは最初に迅速な結果を計算し、それが十分に正確でない場合、それを破棄してより遅いアルゴリズムにフォールバックします。

GCC/glibcの古い32ビットバージョンはfsinname__命令を使用していましたが、これは入力によっては驚くほど不正確です。 2行のコードでこれを説明する魅力的なブログ記事 があります。

純粋なCでのfdlibmのsinname__の実装は、glibcの実装よりもはるかに単純であり、コメントも充実しています。ソースコード: fdlibm/s_sin.c および fdlibm/k_sin.c

191
Jason Orendorff

子供たち、プロの時間....これは、経験の浅いソフトウェアエンジニアに対する私の最大の不満の1つです。彼らは超越関数をゼロから計算することになります(テイラーのシリーズを使用)。違います。これは明確に定義された問題であり、非常に賢いソフトウェアおよびハードウェアエンジニアによって何千回もアプローチされており、明確に定義されたソリューションを持っています。基本的に、超越関数のほとんどは、チェビシェフ多項式を使用して計算します。どの多項式が使用されるかは、状況によって異なります。最初に、この問題に関する聖書は、ハートとチェイニーによる「コンピューター近似」と呼ばれる本です。その本では、ハードウェア加算器、乗算器、除算器などがあるかどうかを判断し、どの操作が最速かを判断できます。例えば非常に高速な除算器がある場合、正弦を計算する最速の方法はP1(x)/ P2(x)で、P1、P2はチェビシェフ多項式です。高速分周器がないと、P(x)になりますが、PにはP1またはP2よりも多くの項があるので、...遅くなります。したがって、最初のステップは、ハードウェアとそれができることを決定することです。次に、チェビシェフ多項式の適切な組み合わせを選択します(たとえば、コサインの場合は通常cos(ax)= aP(x)の形式で、Pはチェビシェフ多項式です)。次に、必要な10進精度を決定します。例えば7桁の精度が必要な場合は、私が言及した本の適切な表を参照すると、(精度= 7.33の場合)数N = 4と多項式数3502が得られます。Nは多項式の次数です(つまり、p4.x ^ 4 + p3.x ^ 3 + p2.x ^ 2 + p1.x + p0)、N = 4であるためです。次に、3502の下の本の裏にあるp4、p3、p2、p1、p0値の実際の値を調べます(これらは浮動小数点になります)。次に、ソフトウェアでアルゴリズムを次の形式で実装します:(((p4.x + p3).x + p2).x + p1).x + p0 ....これは、コサインを10進数7に計算する方法です。そのハードウェアに配置します。

FPUでの超越的操作のほとんどのハードウェア実装には、通常、このようなマイクロコードと操作が含まれることに注意してください(ハードウェアに依存)。チェビシェフ多項式は、すべてではありませんが、ほとんどの超越関数に使用されます。例えば平方根は、最初にルックアップテーブルを使用してニュートンラプソン法の二重反復を使用する方が高速です。繰り返しますが、その本「Computer Approximations」はそれを教えてくれます。

これらの機能を実装する予定がある場合は、その本のコピーを入手することをお勧めします。それは本当にこの種のアルゴリズムの聖書です。コーディックなど、これらの値を計算するための代替手段がたくさんありますが、これらは低精度のみが必要な特定のアルゴリズムに最適である傾向があることに注意してください。毎回精度を保証するには、チェビシェフ多項式を使用します。私が言ったように、明確に定義された問題。 50年にわたって解決されてきた.....そしてそれがどのように行われたか。

さて、言われているように、チェビシェフ多項式を使用して、低次多項式で単精度の結果を得ることができる手法があります(上記のコサインの例のように)。次に、「Gal's Accurate Tables Method」など、より大きな多項式に移動せずに値を補間して精度を高める他の手法があります。この後者の手法は、ACMの文献を参照している投稿が参照しているものです。しかし、最終的に、チェビシェフ多項式はそこにある方法の90%を得るために使用されるものです。

楽しい。

71
Donald Murray

サインやコサインなどの機能は、マイクロプロセッサ内のマイクロコードに実装されています。たとえば、Intelチップには、これらのアセンブリ命令があります。 Cコンパイラは、これらのアセンブリ命令を呼び出すコードを生成します。 (対照的に、Javaコンパイラはそうではありません。Javaは、ハードウェアではなくソフトウェアでトリガー関数を評価するため、実行速度ははるかに遅くなります。)

チップはしないテイラー級数を使用して、少なくとも完全にではなく、トリガー関数を計算します。まず最初に CORDIC を使用しますが、CORDICの結果を洗練するために短いテイラー級数を使用したり、非常に小さな角度に対して高い相対精度で正弦を計算するなどの特殊なケースにも使用できます。詳細については、こちらをご覧ください StackOverflow answer

63
John D. Cook

特にsinの場合、Taylor展開を使用すると次の結果が得られます。

sin(x):= x-x ^ 3/3! + x ^ 5/5! -x ^ 7/7! + ...(1)

それらの差が許容される許容レベルよりも小さくなるか、または有限量のステップ(より高速ですが、精度は低くなる)になるまで、用語を追加し続けます。例は次のようになります。

float sin(float x)
{
  float res=0, pow=x, fact=1;
  for(int i=0; i<5; ++i)
  {
    res+=pow/fact;
    pow*=-1*x*x;
    fact*=(2*(i+1))*(2*(i+1)+1);
  }

  return res;
}

注:(1)は、小さな角度では近似sin(x)= xのために機能します。より大きな角度の場合、許容できる結果を得るには、より多くの項を計算する必要があります。 while引数を使用して、一定の精度で続行できます。

double sin (double x){
    int i = 1;
    double cur = x;
    double acc = 1;
    double fact= 1;
    double pow = x;
    while (fabs(acc) > .00000001 &&   i < 100){
        fact *= ((2*i)*(2*i+1));
        pow *= -1 * x*x; 
        acc =  pow / fact;
        cur += acc;
        i++;
    }
    return cur;

}
13
Blindy

はい、sinを計算するためのソフトウェアアルゴリズムもあります。基本的に、これらの種類のデジタルコンピューターでの計算は、通常、関数を表す Taylorシリーズ を近似するような 数値的手法 を使用して行われます。

数値的手法では、関数を任意の精度で近似できます。また、浮動小数点数の精度は有限であるため、これらのタスクに非常に適しています。

12
Mehrdad Afshari

複雑な質問です。 Intelのようなx86ファミリーのCPUにはsin()関数のハードウェア実装がありますが、x87 FPUの一部であり、64ビットモードでは使用されなくなりました(代わりにSSE2レジスタが使用されます)。そのモードでは、ソフトウェア実装が使用されます。

そのような実装がいくつかあります。 1つは fdlibm にあり、Javaで使用されます。私の知る限り、glibcの実装にはfdlibmの一部と、IBMから提供された他の部分が含まれています。

sin()などの超越関数のソフトウェア実装では、通常、テイラー級数から取得されることが多い多項式による近似を使用します。

11
Thomas Pornin

Taylor series を使用して、シリーズの用語間の関係を見つけて、何度も何度も計算しないようにします

Cosinusの例を次に示します。

double cosinus(double x, double prec)
{
    double t, s ;
    int p;
    p = 0;
    s = 1.0;
    t = 1.0;
    while(fabs(t/s) > prec)
    {
        p++;
        t = (-t * x * x) / ((2 * p - 1) * (2 * p));
        s += t;
    }
    return s;
}

これを使用して、すでに使用されているものを使用して合計の新しい項を取得できます(階乗とxを避けます2p

explanation

11
Hannoun Yassir

別の答えで述べたように、チェビシェフ多項式は、関数と多項式の最大の差が可能な限り小さい多項式です。それは素晴らしいスタートです。

場合によっては、最大誤差は関心のあるものではなく、最大相対誤差です。たとえば、正弦関数の場合、x = 0付近の誤差は、大きな値の場合よりもはるかに小さくなります。小さな相対エラーが必要です。したがって、sin x/xのチェビシェフ多項式を計算し、その多項式にxを掛けます。

次に、多項式の評価方法を理解する必要があります。中間値が小さく、したがって丸め誤差が小さくなるように評価する必要があります。そうしないと、丸め誤差が多項式の誤差よりもはるかに大きくなる可能性があります。また、サイン関数のような関数では、不注意な場合は、x <yの場合でも、sin xの計算結果がsin yの結果より大きくなる可能性があります。そのため、計算の順序を慎重に選択し、丸め誤差の上限を計算する必要があります。

たとえば、sin x = x-x ^ 3/6 + x ^ 5/120-x ^ 7/5040 ...単純にsin x = x *(1-x ^ 2/6 + x ^ 4/120-x ^ 6/5040 ...)、括弧内のその関数は減少しており、will yがxの次に大きな数である場合、sin yは時々より小さい罪x。代わりに、sin x = x-x ^ 3 *(1/6-x ^ 2/120 + x ^ 4/5040 ...)を計算してください。

チェビシェフ多項式を計算する場合、通常、係数を倍精度などに丸める必要があります。しかし、チェビシェフ多項式は最適ですが、倍精度に丸められた係数を持つチェビシェフ多項式は、倍精度係数を持つ最適な多項式ではありません!

たとえば、x、x ^ 3、x ^ 5、x ^ 7などの係数が必要なsin(x)の場合、以下を実行します。多項式(ax + bx ^ 3 + cx ^ 5 + dx ^ 7)を倍精度より高くしてから、aを倍精度に丸めてAを与えます。aとAの差は非常に大きくなります。次に、多項式(b x ^ 3 + cx ^ 5 + dx ^ 7)を使用して(sin x-Ax)の最適な近似を計算します。 aとAの差に適応するため、異なる係数が得られます。bを倍精度Bに丸めます。次に、多項式cx ^ 5 + dx ^ 7などで(sin x-Ax-Bx ^ 3)を近似します。元のチェビシェフ多項式とほぼ同等の多項式が得られますが、倍精度に丸められたチェビシェフよりもはるかに優れています。

次に、多項式の選択で丸め誤差を考慮する必要があります。丸め誤差を無視して、多項式の誤差が最小の多項式を見つけましたが、多項式と丸め誤差を最適化する必要があります。チェビシェフ多項式を取得したら、丸め誤差の範囲を計算できます。たとえば、f(x)は関数、P(x)は多項式、E(x)は丸め誤差です。最適化したくない| f(x)-P(x)|、最適化したい| f(x)-P(x)+/- E(x)|。丸め誤差が大きい場合は多項式誤差を抑え、丸め誤差が小さい場合は多項式誤差を少し緩和するわずかに異なる多項式が得られます。

これにより、最後のビットの最大0.55倍の丸め誤差が簡単に得られます。+、-、*、/には、最後のビットの最大0.50倍の丸め誤差があります。

10
gnasher729

ライブラリ関数の実際の実装は、特定のコンパイラおよび/またはライブラリプロバイダー次第です。ハードウェアで実行するかソフトウェアで実行するか、Taylor拡張であるかどうかなどは異なります。

私はそれが絶対に助けにならないことを理解しています。

6
John Bode

sin()cos()tan()のような三角関数については、5年後、高品質のトリガー関数の重要な側面について言及されていません:Range reduction.

これらの関数の初期段階では、角度をラジアン単位で2 *π間隔の範囲に縮小します。しかし、πは非合理的であるため、x = remainder(x, 2*M_PI)のような単純な簡約はM_PIまたはマシンpiがπの近似値であるため、エラーを導入します。だから、x = remainder(x, 2*π)を行う方法?

初期のライブラリでは、拡張精度または細工されたプログラミングを使用して高品質の結果が得られましたが、それでもdoubleの範囲は限定されていました。 sin(pow(2,30))のような大きな値が要求された場合、結果は無意味または0.0であり、おそらく エラーフラグTLOSS全体の精度の低下またはPLOSSの精度の部分的損失のようなものに設定されていました.

大きな値を-πからπのような間隔に適切に縮小することは、sin()のような基本的なトリガー関数の課題に匹敵する挑戦的な問題です。

良いレポートは、 巨大な引数の引数の削減:最後の部分まで (1992)です。問題を十分にカバーします:さまざまなプラットフォーム(SPARC、PC、HP、30 +その他)の必要性と状況について説明し、allの品質結果を提供するソリューションアルゴリズムを提供しますdoubleから-DBL_MAXからDBL_MAXまで。


元の引数が度単位で、それでも大きな値になる可能性がある場合は、精度を高めるために最初にfmod()を使用してください。適切なfmod()エラーなし を導入するため、優れた範囲の縮小を実現します。

// sin(degrees2radians(x))
sin(degrees2radians(fmod(x, 360.0))); // -360.0 <= fmod(x,360) <= +360.0

さまざまなトリガーIDとremquo()により、さらに改善されています。サンプル: sind()

6
chux

多くの人が指摘したように、それは実装に依存しています。しかし、私があなたの質問を理解している限り、あなたは数学関数の実際のソフトウェア実装に興味がありましたが、見つけられませんでした。これが事実である場合、ここにあります:

  • Glibcソースコードを http://ftp.gnu.org/gnu/glibc/ からダウンロードします
  • unpacked glibc root\ sysdeps\ieee754\dbl-64フォルダにあるファイルdosincos.cを見てください
  • 同様に、数学ライブラリの残りの実装を見つけることができます。適切な名前のファイルを探すだけです

また、.tbl拡張子を持つファイルを見ることができます。その内容は、バイナリ形式の異なる関数のprecomputed値の巨大なテーブルにすぎません。そのため、実装が非常に高速です。使用する系列のすべての係数を計算する代わりに、クイックルックアップを行うだけで、muchより高速です。ところで、彼らはサインとコサインを計算するためにテーラーシリーズを使用します。

これがお役に立てば幸いです。

5
Igor Korkhov

現在のx86プロセッサーでGCCのCコンパイラーでコンパイルされたCプログラムでsin()の場合に答えようとします(Intel Core 2 Duoを例に挙げます)。

C言語では、標準Cライブラリには、言語自体には含まれていない一般的な数学関数が含まれています(たとえば、パワー、サイン、コサインのそれぞれpowsin、およびcos)。ヘッダーは math.h に含まれています。

現在、GNU/Linuxシステムでは、これらのライブラリ関数はglibc(GNU libcまたはGNU C Library)によって提供されています。ただし、GCCコンパイラは、libm.soコンパイラフラグを使用して 数学ライブラリ-lm)にリンクして、これらの数学関数の使用を有効にすることを望んでいます。 なぜ標準Cライブラリの一部ではないのかわかりません。 これらは、浮動小数点関数のソフトウェアバージョン、つまり「ソフトフロート」になります。

さておき:数学関数を分離する理由は歴史的であり、単にで実行可能プログラムのサイズを小さくすることを目的としていました。私の知る限り、おそらく共有ライブラリが利用可能になる前の、非常に古いUnixシステム。

これで、コンパイラは標準Cライブラリ関数sin()libm.soで提供)を最適化して、CPU/FPUの組み込みsin()関数へのネイティブ命令の呼び出しに置き換えることができます。これは、FPU命令(x86 /のFSIN x87)Core 2シリーズのような新しいプロセッサ(これはi486DXにまでさかのぼります)。これは、gccコンパイラーに渡される最適化フラグに依存します。コンパイラーがi386以降のプロセッサーで実行されるコードを記述するように指示された場合、そのような最適化は行われません。 -mcpu=486フラグは、そのような最適化を行うことが安全であることをコンパイラに通知します。

プログラムがsin()関数のソフトウェアバージョンを実行した場合、 CORDIC (COordinate Rotation DIgital Computer)または BKMアルゴリズム に基づいて実行されます。 moreこのような超越関数を計算するために現在一般的に使用されているテーブルまたはべき級数計算。 [ソース: http://en.wikipedia.org/wiki/Cordic#Application]

Gccの最近のバージョン(約2.9x以降)には、最適化として、Cライブラリバージョンへの標準呼び出しを置き換えるために使用される組み込みバージョンのsin __builtin_sin()も用意されています。

私はそれが泥と同じくらい明確であることを確信していますが、あなたが期待していたよりも多くの情報を与え、あなた自身をもっと学ぶために多くのポイントを飛び越えることを願っています。

5
mctylr

通常、これらはソフトウェアで実装され、ほとんどの場合、対応するハードウェア(つまり、アセンブリ)呼び出しを使用しません。ただし、Jasonが指摘したように、これらは実装固有のものです。

これらのソフトウェアルーチンはコンパイラソースの一部ではなく、clibなどの対応するライブラリ、またはGNUコンパイラのglibcにあることに注意してください。 http://www.gnu.org/software/libc/manual/html_mono/libc.html#Trig-Functions を参照してください

より優れた制御が必要な場合は、必要なものを正確に評価する必要があります。典型的な方法のいくつかは、ルックアップテーブルの補間、アセンブリ呼び出し(通常は遅い)、または平方根のニュートンラプソンなどの他の近似スキームです。

5
mnemosyn

ソースにアクセスして、一般的に使用されているライブラリで誰かが実際にどのようにそれを実行したかを確認すること以外に何もありません。特に1つのCライブラリの実装を見てみましょう。私はuLibCを選びました。

Sin関数は次のとおりです。

http://git.uclibc.org/uClibc/tree/libm/s_sin.c

これはいくつかの特殊なケースを処理し、引数の削減を実行して入力を範囲[-pi/4、pi/4]にマップします(引数を2つの部分(大きな部分と尾部)に分割します)呼び出す前に

http://git.uclibc.org/uClibc/tree/libm/k_sin.c

これらの2つの部分で動作します。テールがない場合、次数13の多項式を使用しておおよその答えが生成されます。テールがある場合、sin(x+y) = sin(x) + sin'(x')yという原理に基づいて小さな修正加算が得られます。

5
Moschops

ハードウェアではなくソフトウェアでの実装が必要な場合、この質問に対する明確な答えを探す場所は Numerical Recipes の第5章です。私のコピーは箱に入っているので、詳細を述べることはできませんが、短いバージョン(このことを覚えている場合)は、tan(theta/2)をプリミティブ操作として使用し、そこから他のものを計算することです。計算は級数近似で行われますが、テイラー級数よりも速く収束するmuchものです。

申し訳ありませんが、本を手に入れない限りこれ以上思い出せません。

5
Norman Ramsey

そのような関数が評価されるときはいつでも、あるレベルで次のいずれかの可能性があります。

  • 補間される値の表(高速で不正確なアプリケーション-コンピュータグラフィックスなど)
  • 所望の値に収束するシリーズの評価---おそらくnotテイラーシリーズ、より可能性が高いクレンショー-カーティスのような派手な求積法に基づいたもの。

ハードウェアのサポートがない場合、コンパイラはおそらく後者の方法を使用し、cライブラリを使用するのではなく、アセンブラコードのみ(デバッグシンボルなし)を出力します---デバッガで実際のコードを追跡するのが難しくなります。

4
James

Cでのこれらの関数の実際のGNU実装を見たい場合は、glibcの最新のトランクをチェックしてください。 GNU C Library を参照してください。

3
Chris Tonkinson

Taylorシリーズを使用しないでください。上記の数人が指摘したように、チェビシェフ多項式はより高速で正確です。実装は次のとおりです(元はZX Spectrum ROMから): https://albertveli.wordpress.com/2015/01/10/zx-sine/

2
Albert Veli

サイン/コサイン/タンジェントの計算は、テイラー級数を使用したコードを通じて実際に非常に簡単に実行できます。自分で1つ書くには5秒ほどかかります。

ここで、この方程式を使用してプロセス全体を要約できます。

sin and cost expansion

C用に作成したいくつかのルーチンを以下に示します。

double _pow(double a, double b) {
    double c = 1;
    for (int i=0; i<b; i++)
        c *= a;
    return c;
}

double _fact(double x) {
    double ret = 1;
    for (int i=1; i<=x; i++) 
        ret *= i;
    return ret;
}

double _sin(double x) {
    double y = x;
    double s = -1;
    for (int i=3; i<=100; i+=2) {
        y+=s*(_pow(x,i)/_fact(i));
        s *= -1;
    }  
    return y;
}
double _cos(double x) {
    double y = 1;
    double s = -1;
    for (int i=2; i<=100; i+=2) {
        y+=s*(_pow(x,i)/_fact(i));
        s *= -1;
    }  
    return y;
}
double _tan(double x) {
     return (_sin(x)/_cos(x));  
}
1
user1432532