2014年6月3日火曜日

点の回転から複素数_Complexの計算速度を検証してみる

C言語の話題です。
はじめに
C言語では、複素数(_Complex)が仕様に加わりました。C99の拡張です。
それによって複素数が計算に使えるようになり、プログラミングの幅が広がった…
はずなんですが…

「…それ何に使うんだ?」

というのが正直な感想です。
複素数を日常生活で使うことはまずありません。
プログラムの世界でも、本来複素数を使う式は複素数を使わない方法で書き直されています。コンパイラではdouble型(実数)の高速化や、SIMDを使った並列処理が自動で行われるようになり、floatよりもdoubleの方が早いと言われる時代です。

もし複素数の計算速度が遅ければ、それを利用する必要はあまりありません。

具体的な方法
複素数を利用するもっとも身近な式として、2次元の点の回転を考えます。
点Aが与えられ、それをangleだけ回転させます。
angleの回転のx成分はMx(Mx = cos(angle);)
angleの回転のy成分はMy(My = sin(angle);)
Rx,Ryが求めたい座標だとします。
点を回転させる3つの方法を比較します。

1、複素数の乗算

ガウス平面において、乗算は点の回転を意味しますので、
p = Ax + Ay * i;
q = Mx + My * i;
result = p * q;
Rx = resultの実部;
Ry = resultの虚部;

2、加法定理

cos(a + b) = cos(a) * cos(b) - sin(a) * sin(b);
sin(a + b) = sin(a) * cos(b) + cos(a) * sin(b);
角度同士であればこの公式の通りですが、cosはx座標、sinはy座標だと考えると、次のように書き直すことができます。
Rx = Ax * Mx - Ay * My;
Ry = Ay * Mx + Ax * My;

3、アフィン変換

加法定理を行列の形に整理すると一次変換になりますが、アフィン変換では拡大縮小などのほか、平行移動を加えて3×3の行列に拡張されます。加法定理であげた式よりも冗長な計算が必要になりますが、プログラムでは良く利用されるため、随所で高速化されています。
今回はCoreGraphicsライブラリを利用しますので、
CGAffineTransform transform = CGAffineTransformRotation(CGAffineTransformIdentity, angle);
R = CGPointApplyAffineTransform(A, transform);
で計算します。

予想

仮説1:コンパイラの高速化が全ての方法で、同じようにかかる

だとすれば、加法定理が最も早く、次に複素数、最も低速なのがアフィンだと考えられます。
複素数の内部的な計算が加法定理と同じであれば、簡潔な加法定理が早く済みます。
アフィンは計算回数が多いため、最も低速です。

仮説2:GPUがアフィン変換を計算する

だとすれば、アフィン変換が最も早く、次に加法定理、最も低速なのが複素数だと考えられます。
アフィン変換はアニメーションの中核になっています。
高速なGPUが計算を行い、徹底的に最適化されている可能性は十分にあります。

仮説3:複素数が専用の方法で計算されている

だとすれば、複素数が最も早く、次に加法定理、最も低速なのがアフィンだと考えられます。

やってみた
読み出しと代入、三角関数の実行は計測時間に含めない。
「必要なデータが与えられてから、回転後の数値が確定するまで」を1回とし、
10000回の計算にかかる時間を計測する。
検証のためのコードは次の通りです。
int main(int argc, const char * argv[])
{
    @autoreleasepool {
        
        CGPoint point;
        point.x = 3.2f;
        point.y = 4.f;
        
        const int count = 10000;
        
        CGPoint xy_result;
        
        NSTimeInterval transform_end, mult_end, xy_end, simd_end;
        
        for (int ang = 0; ang < 36; ang++)
        {
            CGFloat angle = 10.f * ang - 180.f;
            angle = (angle / 180.f) * M_PI;
            
            {
                //アフィン変換
                
                CGAffineTransform transform;
                
                transform = CGAffineTransformRotate(CGAffineTransformIdentity, angle);
                
                NSDate *begin = [NSDate date];
                
                for (int i = 0; i < count; i++) {
                    
                    xy_result = CGPointApplyAffineTransform(point, transform);
                }
                
                transform_end = [[NSDate date] timeIntervalSinceDate:begin];
            }
            
            {
                //複素数のかけ算
                
                _Complex double value, mul_value, comp_result;
                comp_result = 0;
                
                value = point.x + (I * point.y);
                mul_value = cos(angle) + (I * sin(angle));
                
                NSDate *begin = [NSDate date];
                
                for (int i = 0; i < count; i++) {
                    
                    comp_result = value * mul_value;
                }
                
                xy_result.x = creal(comp_result);
                xy_result.y = cimag(comp_result);
                
                mult_end = [[NSDate date] timeIntervalSinceDate:begin];
            }
            
            {
                //加法定理
                
                CGPoint transform_xy;
                transform_xy.x = cos(angle);
                transform_xy.y = sin(angle);
                
                NSDate *begin = [NSDate date];
                
                for (int i = 0; i < count; i++) {
                    
                    xy_result.x = point.x * transform_xy.x - point.y * transform_xy.y;
                    xy_result.y = point.y * transform_xy.x + point.x * transform_xy.y;
                }
                
                xy_end = [[NSDate date] timeIntervalSinceDate:begin];
            }
            
            {
                //加法定理をSIMDにする
                //floatのため、計算精度は落ちる
                
                CGPoint transform_xy;
                transform_xy.x = cosf(angle);
                transform_xy.y = sinf(angle);
                
                __m128 simd_point, simd_transform;
                float temp[4];
                
                simd_point = _mm_set_ps((float)point.x, (float)point.y, (float)point.y, (float)point.x);
                simd_transform = _mm_set_ps((float)transform_xy.x, (float)transform_xy.y, (float)transform_xy.x, (float)transform_xy.y);
                
                NSDate *begin = [NSDate date];
                
                for (int i = 0; i < count; i++) {
                    
                    _mm_stream_ps(temp, _mm_mul_ps(simd_point, simd_transform));

                    xy_result.x = (CGFloat)(temp[3] - temp[2]);
                    xy_result.y = (CGFloat)(temp[1] + temp[0]);
                }
                
                simd_end = [[NSDate date] timeIntervalSinceDate:begin];
            }
            
            printf("%.0f,%f,%f,%f,%f\n", angle, transform_end, mult_end, xy_end, simd_end);
        }
    }
    return 0;
}

実行結果
角度アフィン変換複素数の乗算加法定理SIMDを使った加法定理
-1800.0001610.0000340.0000460.000066
-1700.0001550.0000340.0000490.000068
-1600.0001550.0000340.0000530.000067
-1500.0001580.0000340.0000470.000068
-1400.0001570.0000340.0000470.000067
-1300.0001570.0000340.0000460.000066
-1200.0001580.0000340.0000470.000068
-1100.0001590.0000340.0000460.000068
-1000.0001590.0000340.0000470.000069
-900.0001590.0000340.0000470.000068
-800.0001590.0000340.0000460.000067
-700.0001600.0000340.0000470.000069
-600.0001600.0000330.0000460.000069
-500.0001600.0000340.0000460.000068
-400.0001600.0000330.0000460.000069
-300.0001570.0000330.0000460.000070
-200.0001690.0000340.0000470.000069
-100.0001600.0000340.0000460.000069
00.0001590.0000340.0000460.000068
100.0001600.0000340.0000460.000068
200.0001590.0000340.0000470.000067
300.0001580.0000340.0000470.000068
400.0001590.0000610.0001250.000067
500.0001650.0000340.0000460.000068
600.0001630.0000340.0000470.000068
700.0001630.0000340.0000460.000068
800.0001640.0000330.0000460.000068
900.0001630.0000340.0000460.000092
1000.0001660.0000340.0000470.000068
1100.0002510.0000340.0000420.000066
1200.0001660.0000340.0000430.000068
1300.0001640.0000340.0000470.000068
1400.0001630.0000340.0000470.000069
1500.0001640.0000340.0000470.000068
1600.0001630.0000330.0000460.000068
1700.0001640.0000330.0000460.000068
やるじゃないか複素数
目に見えて早いです。
SIMDが遅いのは、キャストに時間がかかっていることと、floatであること、式がSIMDに向いていないことが原因だと思います。コンパイラの最適化が強力なため、SIMDの利点が活かされていないようです。
しかし、数値の代入や三角関数の呼び出しもforループの中に含めると、次のようになります。
角度アフィン変換複素数の乗算加法定理SIMDを使った加法定理
-1800.0008860.0006970.0005790.000546
-1700.0009560.0006890.0005850.000539
-1600.0008930.0007520.0005620.000539
-1500.0010620.0007040.0005740.000613
-1400.0009520.0007130.0005920.000534
-1300.0008720.0006930.0005790.000532
-1200.0008650.0006740.0005740.000567
-1100.0008780.0006870.0005700.000558
-1000.0008580.0008860.0005740.000546
-900.0009480.0007130.0006140.000534
-800.0008720.0006930.0005790.000532
-700.0008800.0007120.0005860.000532
-600.0009010.0006840.0005660.000538
-500.0008610.0006740.0005740.000538
-400.0008790.0007120.0005870.000532
-300.0008780.0006930.0006220.000535
-200.0009770.0007120.0005860.000534
-100.0008770.0006850.0005660.000538
00.0004900.0001750.0001130.000263
100.0012210.0007390.0005690.000653
200.0008810.0008280.0006050.000535
300.0008760.0007120.0006760.000535
400.0008670.0006930.0005790.000534
500.0008570.0006780.0005740.000538
600.0008920.0006870.0005620.000538
700.0008950.0006940.0005800.000535
800.0008850.0007130.0005870.000532
900.0008750.0006930.0008330.000678
1000.0008720.0007310.0010710.000539
1100.0008980.0006870.0005670.000552
1200.0009280.0006830.0005790.000544
1300.0010240.0007150.0005870.000535
1400.0008690.0006930.0005790.000532
1500.0008790.0007120.0005860.000531
1600.0008920.0006970.0005600.000539
1700.0008790.0006750.0005740.000539
複素数は明らかに遅くなります。先の表でも見た通り計算自体は遅くないのですが、creal、cimagなど、データを読み出す部分がインライン定義されていないため、ボトルネックになります。

考察
複素数には専用の計算方法が使われているらしい。
回転を繰り返す場面であれば複素数による高速化も可能性があるのではないかと思うのですが、そのほかの場面では、doubleを使う方が早いのだと考えるべきだと思います。