CORDICアルゴリズムを導き出す
(この記事は、2016年1月に筆者の別ブログで公開したものです)
CORDICアルゴリズムは組み込み系の数値計算に携わっていると随所でその名を見かけます。
むろん、ワークステーション系の恵まれた環境であれば全てはライブラリ化されており、こんなものにはお目に掛かりません。CORDICの名が浮上するのは手厚い支援を受けられない組み込みならではの事と言えます。
CORDICアルゴリズムは「実装が簡単でプログラムも単純である」というのがうたい文句ではあるのですが、一方でCORDICアルゴリズムが正しく動作する理由は、プログラムを子細に読んでもわかりません。ある意味、FFTと同じくらい元の数学から乖離した手順を踏んでいると言ってよいでしょう。
以下では数学的背景から順を追って説明し、三角関数のCORDICアルゴリズムを導き出します。是非、このアルゴリズムが導き出される巧妙な過程を楽しんでください。
CORDICアルゴリズムの導出
複素平面と回転子
最初に複素平面の事を考えてみましょう。CORDICアルゴリズムは平面上の線形な変形操作(アフィン変換)を利用したアルゴリズムです。ですので平面から話を始める方がすっきりするというものです。
よく使う複素平面は図1のように、横軸に実部、縦軸に虚部を配置したものです。図1はさらにこの平面の上に、点
図1: 複素平面と点
次に少し視点をひねってみましょう。この図をながめるとき、我々は上に書いたようにコンパスと定規による作図を想定しました。しかし上の図は、実軸上の点
さて、上の座標は複素平面ですので点
ここで、
このように、複素数一つを掛けることで、複素平面上の任意の点を好きな角度だけ原点を中心に回転させてその座標を計算することができます。
回転子群からsin(x)とcos(x)を求める
与えられた
一つには級数展開した
表1: 回転子
インデックス | 回転子 | |||
---|---|---|---|---|
表のように、角が45度、22.5度、11.25度…と次々に1/2になっていく回転子の表をあらかじめ持っているとしましょう(
例えば、
を計算すれば求めることができます。すなわち
図2: 回転子を用いたsin(x)とcos(x)の計算
図2で分かるように、この場合の回転子は二つの回転子の合成ですが、考え方を変えると、「まず45度動かして、つぎに22度動かしている」ともとれます。その場合、点の座標を表す複素数に対して次々に回転子である複素数を掛けていくことになります。
これらをまとめて一般化してみましょう。点
ここで
右辺を見てわかるように、1回の複素乗算あたり4回の実数乗算が必要になります。これを、
スケーリングして乗算を減らす
複素平面上で既知の角度の回転を繰り返していくことで、与えられた角
削るための方法はスケーリングです。先に挙げた
表2: スケーリングを導入する
インデックス | 回転子 | 修正回転子 | ||
---|---|---|---|---|
修正回転子は、スケーリングによって実部が1に固定されています。その結果、上の式3は修正回転子によって次のように変形されます。
この変形を座標上で考えてみましょう。式4からあえて
図3: スケーリングによる三角形の生成
そして、図3を見れば一目瞭然ですが、原点から
三角形の高さを1/(2^n)にして乗算を減らす
スケーリング・ファクタの導入で乗算を減らすことができました。これをさらに減らしてみましょう。
表2で、修正回転子の虚部は1,0.414,0.198となっています。これを思い切って、1, 0.5, 0.25 … 1/2^nに変更するとどうなるでしょうか。
表3: 回転子の虚部を1/(2^n)にする
インデックス | 修正回転子2 | ||
---|---|---|---|
こうすると、上の回転の式4は次のようになります。
なにやら却って複雑になったように思えますが、ここで思い直してみましょう。
つまり、ここまでの変形によって、繰り返し1回あたり乗算4つの演算だったのが、加減算とシフト演算(およびスケーリング処理)にまで演算負荷を下げることができました。
折りたたんでスケーリングを解決する
「与えられた角
ここまでの手順の変形により、点
スケーリング・ファクタの取り除きに必要な演算はどのくらいになるでしょうか。これを見積もることは簡単です。角
-
個の回転子それぞれのスケーリング・ファクタを取り出して、それら全ての積を求めるk - 求めた積の逆数を求める
つまり、乗算
これまでの方法では、表の中から必要な回転子だけを選び出して与えられた角xを合成していました。これをやめて表にある回転子全部を使用することにします。
例えば、表3のインデックスが0から14までの計15個であれば、15個の回転子全部を使うことにします。一見無駄に見えるこの方法のメリットはSによる補正値の計算を事前に済ませる事ができる点です。なにしろ回転子は全部使います。ですから、コーディング時に全ての
そして、もう一度頭をひねることで、そもそも
一方、全ての回転子をつかって反時計回りに回転を行えば、求める角
- インデックス0から始める
- 合成した角がxより小さければ、次の回転子をつかって反時計回りに回転させる。
- 合成した角がxより大きければ、次の回転子をつかって時計回りに回転させる。
- これを全てのインデックスに昇順に適用する
この方法を使うと、回転による点の動きは図3のように折り返しを含むものになります。
図4: 折り返しを含む回転
今回は時計回りの回転が入ってきます。その場合は(式6)を書き換えて次の式で時計回りの回転を求めることができます。
sin(x)、cos(x)を求めるCORDICアルゴリズム
以上の考察を元に導き出されたCORDICアルゴリズムはおおむね以下のようになります。
function sincos( x )
p = 1/S_ALL
q = 0
angle = 0
for ( i = 0; i< MAXINDEX; i++ )
if x > angle
// 反時計回り
p_next = p – q >> i
q_next = q + p >> i
angle += theta[i]
else
// 時計回り
p_next = p + q >> i
q_next = q – p >> i
angle -= theta[i]
end if
p = p_next
q = q_next
end for
cos = p
sin = q
return( sin, cos )
end function
よく言われているとおり、乗除算を用いずに加減算とシフト演算のみで
収束するのか?
さて、こうしてCORDICアルゴリズムを求めることができました。求めたアルゴリズムは乗算を含まず、加減算とシフト演算だけで実装できます。また事前に必要な定数も1/S_ALLと、theta[n]だけで、非常にコンパクトです。
しかし、そもそもこのアルゴリズムは収束するのでしょうか。
ポイントは与えられた
表1に基づく回転だけを用いるのであれば、直感的にこのアルゴリズムは収束すると言えます。コンピュータ技術者であれば、2の巾乗だけを足し合わせて任意の整数を作ることができると知っています。二進数の基本です。
しかし、乗算を削るために導き出した表3は、
詳しい考察はめんどくさいのでしていませんが、直感的に「任意のtheta[k]を選んだときに、theta[k+1]からtheta[n]の総和がtheta[k]より大きければ、xに十分近いangleを合成できる」と言えるようです。
なんにせよ、世間では広くCORDICアルゴリズムが使われています。精度に関して言えば、N回繰り返しを行うCORDICアルゴリズムは、Nが大きな領域ではNを1増やす毎にアルゴリズム固有の雑音が1/2になっていきます。
atan2(x,y)
ここまで、与えられた角
では、与えられた座標
結論からいえば、
sin,cosを求めるときには「合成したangleがxより大きいか否か」を判別して回転方向を決めています。atan2(X,Y)アルゴリズムでは、ここを「qの値が正か負か」で判別するようにします。回転を重ねてqを0に近づくように追い込んでいけば、ループから出たときには-atan2(Y,X)の値がangleに格納されています。
function atan2( x, y )
p = x
q = y
angle = 0
for ( i = 0; i< MAXINDEX; i++ )
if p < 0 // pが負ならば
// 反時計回り
p_next = p – q >> i
q_next = q + p >> i
angle += theta[i]
else
// 時計回り
p_next = p + q >> i
q_next = q – p >> i
angle -= theta[i]
end if
p = p_next
q = q_next
end for
// angleには-atan2(y,x)が格納されている
return( -angle )
end function
終わりに
CORDICアルゴリズムについて解説しました。
このアルゴリズムは実装が容易で必要な資源も軽いことで有名です。しかし、それを導き出す過程は最適化という観点から見たときに非常に興味深いものです。信号処理の最適化では時たまアルゴリズムから数学まで引き返して最適化を行う必要がありますが、CORDICはその好例と言えるでしょう。
Discussion