強火で進め

このブログではプログラム関連の記事を中心に書いてます。

float型とdouble型を比較した場合、常にfloatが速いと思ってはダメらしい

※こちらに記載したプログラムには大ポカがあり、正しい検証プログラムになっていませんでした。こちらに修正エントリーを書きましたのでプログラムや速度比較についてはそちらを確認下さい。

以前、「Cプログラミング診断室」の作者の方がホームページを作れていることを知り、見ていたところ

こちらのページに

Cプログラミング診断室/キャストが好き/float型対double型
http://www.kojima-cci.or.jp/fuji/mybooks/cdiag/cdiag.4.4.html

ほとんどのCでは、float型よりdouble型で計算した方が数倍高速になります。

との記述がありました。自分もどっぷりとdoubleよりfloatの方が速いとの印象に浸かっていたのでかなりおどろきました。

もちろん、常にdoubleが速いという訳では無くこちらのサイトでも

なお、どっちが速いか、どのくらいの差が出るかは、マシンやコンパイラ、数値演算チップに依存するところが多いので、実際に使うマシンで確認してください。

との記載がありました。

ということ「早速、iPhoneで確認だー」と思ったのですがそのときはMacBook Airが入院中で検査できませんでしたorz

しかし、昨日やっと退院したので早速検証をすることにしました。

検証時のシステム構成

諸条件は以下の通りです。

  • 乱数で計算用のデータを生成。この値は、float用、double用とも同一とする。
  • コンパイラによる最適化や削除が入らない様なプログラムとなる様に気をつける
  • システム構成は以下
SDK 2.2.1
ビルド構成 Release
最適化レベル -Os
動作環境 iPhone(2.2.1)実機上で動作

計測用のプログラムはこちらです。このプログラムをボタンのアクションに設定し、アプリ起動後、少し間を空けてから実行しました。

- (IBAction)run:(id)sender
{
#define TEST_NUM    300
#define LOOP_NUM    100000
    
    int i, j;
    double startTime;
    double elapsedTime;
    float floatVal[LOOP_NUM];
    double doubleTmp;
    float floatTotal;
    double doubleVal[LOOP_NUM];
    float floatTmp;
    double doubleTotal;
    
    srand((unsigned)CFAbsoluteTimeGetCurrent());
    
    for (i=0; i<LOOP_NUM; i++) {
        doubleVal[i] = (double)(rand() % 100) / 100.0;
        floatVal[i] = (float)doubleVal[i];
    }
    
    // float型の場合の速度を計測
    startTime = CFAbsoluteTimeGetCurrent();
    floatTotal = 0.0f;
    for (i=0; i<TEST_NUM; i++) {
        for (j=0; j<LOOP_NUM; j++) {
            floatTotal += floatVal[j] - 0.8f*i/TEST_NUM;
        }
        floatTotal *= 0.1f;
    }
    elapsedTime =  CFAbsoluteTimeGetCurrent() - startTime;
    NSLog(@"float                            : time=%f\tval=%f",  elapsedTime, floatTotal);
    
    startTime = CFAbsoluteTimeGetCurrent();
    floatTotal = 0.0f;
    for (i=0; i<TEST_NUM; i++) {
        for (j=0; j<LOOP_NUM; j++) {
            floatTotal += floatVal[j] - 0.8f*(float)i/TEST_NUM;
        }
        floatTotal *= 0.1f;
    }
    elapsedTime =  CFAbsoluteTimeGetCurrent() - startTime;
    NSLog(@"float(変数をキャスト)              : time=%f\tval=%f",  elapsedTime, floatTotal);
    
    startTime = CFAbsoluteTimeGetCurrent();
    floatTotal = 0.0f;
    for (i=0; i<TEST_NUM; i++) {
        for (j=0; j<LOOP_NUM; j++) {
            floatTotal += floatVal[j] - 0.8f*i/(float)TEST_NUM;
        }
        floatTotal *= 0.1f;
    }
    elapsedTime =  CFAbsoluteTimeGetCurrent() - startTime;
    NSLog(@"float(定数をキャスト)              : time=%f\tval=%f",  elapsedTime, floatTotal);
    
    startTime = CFAbsoluteTimeGetCurrent();
    floatTotal = 0.0f;
    for (i=0; i<TEST_NUM; i++) {
        for (j=0; j<LOOP_NUM; j++) {
            floatTotal += floatVal[j] - 0.8f*(float)i/(float)TEST_NUM;
        }
        floatTotal *= 0.1f;
    }
    elapsedTime =  CFAbsoluteTimeGetCurrent() - startTime;
    NSLog(@"float(両方をキャスト)              : time=%f\tval=%f",  elapsedTime, floatTotal);
    
    startTime = CFAbsoluteTimeGetCurrent();
    floatTotal = 0.0f;
    for (i=0; i<TEST_NUM; i++) {
        for (j=0; j<LOOP_NUM; j++) {
            doubleTmp = floatVal[j] - 0.8*i/TEST_NUM;
            floatTotal = (float)doubleTmp;
        }
    }
    elapsedTime =  CFAbsoluteTimeGetCurrent() - startTime;
    NSLog(@"float(異なる型のデータを使ったとき)  : time=%f\tval=%f",  elapsedTime, floatTotal);
    
    // double型の場合の速度を計測
    startTime = CFAbsoluteTimeGetCurrent();
    doubleTotal = 0.0f;
    for (i=0; i<TEST_NUM; i++) {
        for (j=0; j<LOOP_NUM; j++) {
            doubleTotal += doubleVal[j] - 0.8*i/TEST_NUM;
        }
        doubleTotal *= 0.1;
    }
    elapsedTime =  CFAbsoluteTimeGetCurrent() - startTime;
    NSLog(@"double                           : time=%f\tval=%f",  elapsedTime, floatTotal);
    
    startTime = CFAbsoluteTimeGetCurrent();
    doubleTotal = 0.0f;
    for (i=0; i<TEST_NUM; i++) {
        for (j=0; j<LOOP_NUM; j++) {
            doubleTotal += doubleVal[j] - 0.8*(double)i/TEST_NUM;
        }
        doubleTotal *= 0.1;
    }
    elapsedTime =  CFAbsoluteTimeGetCurrent() - startTime;
    NSLog(@"double(変数をキャスト)             : time=%f\tval=%f",  elapsedTime, floatTotal);
    
    startTime = CFAbsoluteTimeGetCurrent();
    doubleTotal = 0.0f;
    for (i=0; i<TEST_NUM; i++) {
        for (j=0; j<LOOP_NUM; j++) {
            doubleTotal += doubleVal[j] - 0.8*i/(double)TEST_NUM;
        }
    }
    elapsedTime =  CFAbsoluteTimeGetCurrent() - startTime;
    NSLog(@"double(定数をキャスト)             : time=%f\tval=%f",  elapsedTime, floatTotal);
    
    startTime = CFAbsoluteTimeGetCurrent();
    doubleTotal = 0.0f;
    for (i=0; i<TEST_NUM; i++) {
        for (j=0; j<LOOP_NUM; j++) {
            doubleTotal += doubleVal[j] - 0.8*(double)i/(double)TEST_NUM;
        }
        doubleTotal *= 0.1;
    }
    elapsedTime =  CFAbsoluteTimeGetCurrent() - startTime;
    NSLog(@"double(両方をキャスト)             : time=%f\tval=%f",  elapsedTime, floatTotal);
    
    startTime = CFAbsoluteTimeGetCurrent();
    doubleTotal = 0.0f;
    for (i=0; i<TEST_NUM; i++) {
        for (j=0; j<LOOP_NUM; j++) {
            floatTmp = doubleVal[j] - 0.8f*i/TEST_NUM;
            doubleTotal += floatTmp;
        }
    }
    elapsedTime =  CFAbsoluteTimeGetCurrent() - startTime;
    NSLog(@"double(異なる型のデータを使ったとき) : time=%f\tval=%f",  elapsedTime, floatTotal);
}

計測結果

計測結果は以下の様になりました。「Compile for Thumbにチェック有り(Thumb)」、「Compile for Thumbにチェック無し(ARM)」で速度が大きく異なるのでそれぞれの場合の結果を記載します。

Compile for Thumbにチェック有り(Thumb)
処理時間 計算結果
float 6.033257 -3380.365234
float(変数をキャスト) 6.049636 -3380.365234
float(定数をキャスト) 6.090470 -3380.365234
float(両方をキャスト) 6.475050 -3380.365234
float(異なる型のデータを使ったとき) 8.916007 -0.697333
double 0.226563 -0.697333
double(変数をキャスト) 0.226190 -0.697333
double(定数をキャスト) 0.224555 -0.697333
double(両方をキャスト) 0.252861 -0.697333
double(異なる型のデータを使ったとき) 0.226501 -0.697333
Compile for Thumbにチェック無し(ARM)
処理時間 計算結果
float 2.545253 -3343.713623
float(変数をキャスト) 2.702051 -3343.713623
float(定数をキャスト) 2.406180 -3343.713623
float(両方をキャスト) 1.846316 -3343.713623
float(異なる型のデータを使ったとき) 2.205353 0.132667
double 0.152696 0.132667
double(変数をキャスト) 0.149546 0.132667
double(定数をキャスト) 0.153359 0.132667
double(両方をキャスト) 0.162193 0.132667
double(異なる型のデータを使ったとき) 0.226825 0.132667

結果の検証

  • floatとdoubleではdoubleを使った方がかなり速い。floatとdoubleが混ざった計算が行われる場合もdoubleを使った方が良い
  • 「両方をキャスト」は遅い、不必要な場所にまでキャストしちゃダメ
  • 「変数をキャスト」「定数をキャスト」は「定数をキャスト」の方が速度が速いときが多いみたいですけど何回かに1回は逆になる事もあるみたい。ここはそんなに神経質にならなくても良いかも
  • doubleはfloatと組み合わせた場合もほとんど速度低下はみられない
  • doubleの場合(特に「異なる型のデータを使ったとき」)はThumbとARMで大きな差は発生していない
  • 精度を求めるならdouble、それが難しい場合も計算途中はdoubleで行う様にする
  • OpenGL ESは GLdouble が使えないので使い方に工夫が必要。 id:mswar さんからtwitterreplyを貰ってなるほど、思った物理エンジンなどOpenGL ESとは別の部分での使用を検討した方が良いかもしれません(もちろん途中の計算が長い様であればそれ以外のときでもdoubleの使用の検討の価値も有りです)。
計算結果について

floatとdoubleで計算結果が異なることに気がついた方も多いかと思われますがこれはfloatとdoubleで表現出来る数値の限界が異なるためです。この限界については以下のプログラムで簡単に確認できます。

    NSLog(@"float  %20.10f", 1.0f/0.3f);
    NSLog(@"double %20.10f", 1.0/0.3);

※1.0などと数値を記載したいコンパイラはdoubleとして処理しますが1.0fと数値の後に f を付けるとfloatとして処理されます。

このプログラムを実行するとこの様に表示されます。

float          3.3333332539
double         3.3333333333

本来であれば 3.3333333333 と表示されるdoubleのものが正しいのですがfloatでは途中から 2539 となり、正しい結果では無くなっています。少しの量の計算結果であれば多少の誤差で済み、許容できる場合も多いですが今回の計測プログラムの様にかなりの回数計算を行う場合は誤差が積み重なり、大きな誤差になる場合もあります。

必要に応じて計算途中はdoubleで行うなど必要な処理を行いましょう。

最後に

なお、これはあくまで速度を中心にした検証ですので実際に使用するときはアプリの性質により、アプリのファイルサイズ、バッテリーの消費速度など別の面も考慮した上で float にするか double にするかの検討が必要だと思います。

P.S.
あと、自分はそんなに効率的なプログラムやベンチプログラムについて詳しくないので「ここ、もっとこうした方が良い」とか「オレならこうする」などありましたらコメント欄またはトラックバックにてよろしくお願いします<(_ _)>

改訂新版 Cプログラミング診断室

改訂新版 Cプログラミング診断室