山上企画タイトル

Top天文ソフト三角関数の高速化

三角関数の高速化

〜テーブルを使って〜

浮動小数点演算高速化の必要性

古のコンピュータ、とくにPCでは、浮動小数点計算は非常に時間の必要な処理でした。
もちろん、今のPCには、浮動小数点計算用のハードウェアが搭載されているのが普通で、浮動小数の計算もずいぶんと速くなりました。

けれども、浮動小数点演算ハードウェアが搭載されていないコンピュータもまだまだあるのです。 私にとって身近な例が、『PocketPC』です。 このコンピュータのCPUは、ARMという規格を基準にしてIntelが作ったXScaleというものですが、 これには、浮動小数点はおろか、整数の割り算命令もないようなのです。

モバイル用CPUで、省電力という大儀があるので省かれていると思うのですが、 私のやりたい天文計算では、浮動小数点が遅いのはかなり致命的です。 XScaleの動作クロックは400MHzくらい、いくら昔のデスクトップPCよりもずっと早いとはいえ、 プログラムは早いに越したことはありません。

そこで、天文計算の中で特に使用頻度の高い三角関数の高速化について、調べたことを記録しておきます。 使用言語はC++、最近はお手軽Web関係言語が流行で、すっかり時代遅れ言語ですが、私がこれしか喋れないので。

浮動小数点フォーマット

これは基本中の基本ですが、そんなに精度がいらない場合は、doubleではなくfloatを使います。 というのも、floatは4byte、32bitで表せて、データ転送にdoubleの半分の時間でいいということと、 いわゆるintと同じ大きさなのでちょっとしたトリックが使えるためです。

図1 IEEEの32bit浮動小数点フォーマット
31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
s e e e e e e e e m m m m m m m m m m m m m m m m m m m m m m m
符号 指数部 仮数部

詳細に入る前に、浮動小数がどのようにコンピュータの中で実現されているか確認です。
上の図は、IEEEの32bit浮動小数点フォーマットです。1bitの符号、8bitの指数、23bitの仮数で構成されています。 CやC++でいうfloat型は、この浮動小数点フォーマットを使っています。
このフォーマットは、どのコンピュータ、OSでも同じになっているはずです。そうでないと規格としての意味がありませんね。

表現範囲などはあちこちに資料がありますので、詳しくは述べませんが、2の±127乗の範囲で小数を表すことができます。 仮数部は正規化されていて、22bitの前に暗黙の1があるのも有名ですね。

このフォーマットで表される数nは、n=s×1.m×2^(e−127)となります。(^は、べき乗を表す)

float → int トリック

ここで、浮動小数点の整数部分だけを取りたいと考えます。というのは、三角関数、特にsin、cosを、テーブルを用いて求めたいからです。

三角関数の高速化にはいくつかの方法があり、精度、速度にそれぞれ利点、欠点があります。 一番単純で高速なのが、0〜360度までのsin、cosの値をあらかじめ求めてテーブルにしておき、 必要に応じてそれを参照する方法です。 プログラムとしては、sin、cosの値を配列に持っておき、必要な角度をその配列の範囲にして参照することになります。 配列範囲としてよく使われるのは、256や1024など、コンピュータにとって都合のいい数字ですね。

この方法では、与えられた角度を配列の参照値とするために、値を正規化し、小数を整数にする必要があります。 30度の場合、30×(256÷360)=21.33333・・という値を求め、この整数部分、21を使って配列を参照するわけです。

32bit浮動小数フォーマットは、float型として使われるわけですが、bit並びとして考えると、整数としても見ることができます。 ここに、高速なfloat→int変換が可能となるトリックがあるのです。

戯言:こういう、あるデータを別の形で見る、というのは、プログラムリテラシーのまさにリテラシーだと思うのですが、 あまりプログラミングの本には出てきませんね。bit列眺めてニヤニヤしているのはやっぱり変ですか?(笑)

 
浮動小数点フォーマットをbit並びで見てみる
値 = 41.26 float = 41.259998bit並び =0x42250a3d

ここで使うトリックが、

  1. 与えられた角度の値に、浮動小数で1.5×2^23を足す
  2. 求まった値から、整数で0x4b400000を引く

というものです。

なぜこれで整数部分が求まるのかというと、

  1. 1.5×2^23を足すことによって
    元の数の小数部分がアンダーフローして整数部分しか残らない
    符号部分と指数部分が0x4b4と決まった値になる
  2. 整数で0x4b400000を引くことによって
    浮動小数フォーマットの指数部分が0になる

ためなのです。

マジックナンバー0x4b400000は、1.5×2^23を、浮動小数点フォーマットで表したbit並びです。 これを整数として引き算するので、指数部分が0となるわけです。

 たとえば、このトリックで41.26の整数部分を取り出してみると、

float → intトリック
操作floatbit並び、int
代入値 = 41.2641.2599980x42250a3d
floatで1.5×2^23を足す12582953.0x4b400029
intで0x4b400000を引く5.745e−044#DEN0x00000029

という操作となり、0x29=41と、正しい値が求まります。

気をつけないといけないのは、このトリックでは±2^22の範囲にある浮動小数の整数部分しか、 得ることができない、ということです。それよりも絶対値が大きい場合は、このトリックは使えません。桁があふれちゃうからですね。

sinテーブル参照版

 では、sinをテーブルを使って求めるプログラムを書いてみます。

// sinテーブルの大きさ
const   int     SINTABLESIZE    = 256;
// バイアス
const float     FTOIBIAS        = 12582912.0f;
// 0〜2π正規化用
const float     PI2_F           = 6.2831853f;   // 3.14 * 2;
const float     TWOPISCALE      = (float)SINTABLESIZE / PI2_F;

// sine table.
float   SinTable[SINTABLESIZE];

// float <=> int 共用体
typedef union
{
    int     i;          // as integer
    float   f;          // as float
} INTORFLOAT;

使用する定数を定義して、float<=>intコンバータ共用体を定義します。

// sine table.初期化
void    s24fSinCosInit( void )
{
    for ( int i = 0; i < SINTABLESIZE; ++i) {
        double  theta = (double)i/TWOPISCALE;
        SinTable[i]   = (float)sin(theta);
    }
}

fsinを呼び出す前に、あらかじめテーブルを初期化します。ランタイムライブラリと互換をとりたいので、与える値は0〜360ではなく、 0〜2πとします。

float   fsin( float theta )
{
    INTORFLOAT      fi;

    // 与えられた値を0〜2πに正規化して1.5^23を足す
    fi.f    = theta * TWOPISCALE + bias.f;
    // 整数化&256の範囲に収めるため、255でandを取る
    unsigned int    i = fi.i & (SINTABLESIZE - 1);

    return  SinTable[i];
}

与えられた値に、TWOPISCALE = 256/6.2831853をかけて、2πが255となるよう正規化します。 その後、1.5×2^23を足し255でandを取って、整数化と適性範囲化を一度にしてしまいます。
上のfloat<=>int変換では、0x4b400000で引き算しましたが、今、求める数は正の数ですから、 指数部が0になればいいので、255=0xFFでandを取ることにより0にしています。
その値でテーブルを参照すれば、必要なsinの値が求まります。 cosは、−90度ずれたsinと同じですから、最後のテーブル参照をする時に、配列要素が256の場合には64を足して、 255でマスクしてあげれば求まります。

このsinの求め方については、 Game Programming Gems2に詳しく出ていますので、 詳細が知りたい方はGem2を参照ください。 この記事には他にも、浮動小数の高速な比較や符号判定、絶対値、平方根や任意の関数の高速化技法が出ています。
この本、すごくいい本なんですが、高くて厚くて重いのが難点です。Gem3も出ているようですね。

精度と速度

この方法の精度は、360度の範囲を256や1024で割るわけですから非常に悪く、 配列範囲を256とすると、約1.4度位になります。 それでも、半径50dotの円の中に模様を描く、PlanCEの惑星拡大画面のように、 1度の精度すら不要な場合も結構あり、使う場所に必要な精度さえ気にしていれば、使い道はたくさんあります。

PlanCEの惑星拡大画面の場合、doubleの組み込みsinでは、1枚描くのに2秒ほどかかっていましたが、 このテーブル法を使うことによって、0.2秒まで高速化されました。実に10倍!  1.4GHzのPentium4でも、3倍ほどの高速化がなされました。

もちろん、精度がガタガタなのを忘れてはいけませんが、ライブラリは適材適所で使うのが一番ですね。

戻ります