2011年6月4日土曜日

1秒間に何回sqrtを出来るか?

前に書いた「アンチエイリアスがかかった円を高速描画するアルゴリズム」では、「平方根sqrtは重い処理だから前もってやっておきましょう」みたいなことを書きました。 でも、どれだけ重いかは確認してなかったんですよね。 ということで、厳密じゃないですが簡単なテストコードを作ってざっとsqrtの重さを確認してみました。

環境や開発ツールは、

  • Windows XP home edition SP3
  • CPU Pentium4 2.6c
  • Borland C++ 5.5.1

懐かしすぎるコンパイラbcc32再び登場。 Visual C#を立ち上げるのが面倒だったもので。 csc.exeでも良かったかな? まぁ、いいや。

テストコードはこんな感じです。

#include<stdio.h>
#include<stdlib.h>
#include<math.h>
#include<windows.h>
#include<mmsystem.h>

#define LOOP_NUM 100000000
#define TEST_NUM 10

long test()
{
    long start = timeGetTime();
    for(int i = 0; i < LOOP_NUM; i++)
    {
        sqrt( (double)rand() );
        //double t = (double)rand();
        //sin( (double)rand() );
    }
    
    return timeGetTime() - start;
}

double average(long *p, int num)
{
    double res = 0.0;
    
    for(int i = 0; i < num; i++)
    {
        res += (double)*p;
        p++;
    }
    
    return res / num;
}

void main()
{
    srand(timeGetTime() );
    long res[TEST_NUM];
    
    for(int i = 0; i < TEST_NUM; i++)
    {
        res[i] = test();
        printf("テスト%03d : %d回のsqrtに%dミリ秒かかった。\n", i, LOOP_NUM, res[i]);
    }
    
    double ave_time = average(res, TEST_NUM);
    printf("平均%.0fミリ秒かかった。\n", ave_time);
    printf("1秒に%.0f回計算できる。\n", LOOP_NUM * 1000.0 / ave_time);
    printf("0.1秒に%.0f回計算できる。\n", LOOP_NUM * 100.0 / ave_time);
    printf("0.016秒に%.0f回計算できる。\n", LOOP_NUM * 16.0 / ave_time);
}

C言語でsqrt関数を1億回呼び出して、何ミリ秒かかるかを測っています。 sqrtに渡す値は決まった数だとテストとして問題が出そうなので乱数にしています。 ってことで、正確にはsqrt+rand+キャストのテストですね。 多少ノイズは入るけど、気にしない方向で。

10セット繰り返して平均値を表示します。 ついでに、時間当たりに何回計算できるかも表示。 0.1秒って言うのは「UIで使うにはこれぐらいのレスポンスタイムかなぁ」という時間です。 0.016秒とかって言うのはゲームなどで60fpsの場合の時間。

とりあえず、最適化オプションなしで実行してみました。

最適化オプションなし sqrt
テスト000 : 100000000回のsqrtに3187ミリ秒かかった。
テスト001 : 100000000回のsqrtに3156ミリ秒かかった。
テスト002 : 100000000回のsqrtに3172ミリ秒かかった。
テスト003 : 100000000回のsqrtに3156ミリ秒かかった。
テスト004 : 100000000回のsqrtに3172ミリ秒かかった。
テスト005 : 100000000回のsqrtに3157ミリ秒かかった。
テスト006 : 100000000回のsqrtに3171ミリ秒かかった。
テスト007 : 100000000回のsqrtに3172ミリ秒かかった。
テスト008 : 100000000回のsqrtに3157ミリ秒かかった。
テスト009 : 100000000回のsqrtに3171ミリ秒かかった。
平均3167ミリ秒かかった。
1秒に31574627回計算できる。
0.1秒に3157463回計算できる。
0.016秒に505194回計算できる。

例のアルゴリズムでテーブルを使わなかった場合、半径100の円を描くには(100/1.4142=70回強)のsqrtをすることになります。 sqrt以外にいろんなコートが動くことを考慮しても、数百の円を描く程度ならたいした負荷ではなさそうですね。 テストに使ったPCは8年前のものなので、今のマシンでやればもっと余裕がありそうです。

描くときにsqrtを使っても問題なかったか? でもまぁ、テーブル作るのもたいした手間じゃないし、「速いに越したことがない」っていうケースのことも考えれば枯れたアルゴリズムの説明を書いた意味もあったかも...

一応、最適化オプションを指定してコンパイル→実行もしてみました。

...昔のコンパイラの最適化オプション、ワカラン。 これでいいのかな? -O2っと。

-O2 sqrt
平均3305ミリ秒かかった。
0.016秒に484159回計算できる。

結果を全部書くと長くなるので2行だけ抜粋です。 アレ? 最適化したのに重くなってる? オプション間違えたかな? それっぽいの全部付けて、-O2 -Oc -Oi -Os -Ovっと

-O2 -Oc -Oi -Os -Ov sqrt
平均3320ミリ秒かかった。
0.016秒に481899回計算できる。

変わらないです。 何なんでしょう? 昔のコンパイラの最適化の機微なんか深く調べるつもりはないぞ?

と思いつつもチラッと調べるとそれっぽい情報がありました。 どうやらbcc32はデフォルトでいくつかの最適化がかかっており、-Odでそれを解除できる模様。 素人考えで適当な最適化オプション付けるよりデフォルトの方が良かったんですね。 っていうことで、-Odでコンパイル→実行すると。

-Od sqrt
テスト000 : 100000000回のsqrtに5828ミリ秒かかった。
テスト001 : 100000000回のsqrtに5828ミリ秒かかった。
テスト002 : 100000000回のsqrtに5812ミリ秒かかった。
テスト003 : 100000000回のsqrtに5813ミリ秒かかった。
テスト004 : 100000000回のsqrtに5812ミリ秒かかった。
テスト005 : 100000000回のsqrtに5828ミリ秒かかった。
テスト006 : 100000000回のsqrtに5813ミリ秒かかった。
テスト007 : 100000000回のsqrtに5812ミリ秒かかった。
テスト008 : 100000000回のsqrtに5829ミリ秒かかった。
テスト009 : 100000000回のsqrtに5812ミリ秒かかった。
平均5819ミリ秒かかった。
1秒に17185969回計算できる。
0.1秒に1718597回計算できる。
0.016秒に274976回計算できる。

よしよし、遅くなった。 オプション指定無しと最適化を切ったのを比較すればいいようです。 sqrtの速さって最適化無しのこっちを基準に考えた方が良いんでしょうかね? ホントは逆アセンブルしてコードを見て判断した方がいいんでしょうけど、そこまでやる気は無いなぁ。

最適化の有無で倍近く所要時間が変わりましたね。 でもまぁ、ホワホワホワッと自分が作りそうなプログラムを思い浮かべたら、どっちの所要時間を基準に考えてもCPU使用率に響くようなものはありません。

まぁ、いいや。 sqrtの速さについてちょっと調べてみたけど、円を描くアルゴリズムを使うアテ、無いですしね。 今はとりあえず、「円を描くの、多少遅くても大丈夫」ってことで終了です。

一応ついでに、randだけ1億回の所要時間と、sqrtの代わりにsinを使ったときの所要時間を測ってみました。

最適化オプションなし rand
平均1211ミリ秒かかった。
0.016秒に1321331回計算できる。
-Od rand
最適化オプションなしとほぼ同じで割愛。
最適化オプションなし sin
平均9714ミリ秒かかった。
0.016秒に164711回計算できる。
-Od sin
平均12578ミリ秒かかった。
0.016秒に127205回計算できる。

sqrtよりsinの方が重い処理なのか。 でもこれは、新しいCPUだと最近の3DCGとかの需要で変わるかも? この数字、何かの参考になるかな?

今のPCが不調なのでPCを買いかえるか考え中だったりします。 もし買いかえたら、そっちでも測ってみるかな?