2002年に、JavaScript上で任意精度整数演算を行うライブラリ bigint.js v0.2 - v0.4 を作ってみたときのメモです。 2004年に、はるかに効率化された bigint.js v0.5 に更新されましたが、記録のため、 この古い記事も残しておきます。 ちなみに、円周率計算のデモ(手元では2秒台で596桁)。
「JavaScriptでPGPもどき」では24ビットの鍵の長さのRSA暗号を実装した。暗号強度のこの制限は、JavaScriptで「ふつう」に扱える整数が 253 までなので生じた。JavaScriptでもっと大きな数の計算を —— 浮動小数点でなく正確な整数計算として —— 実行するには、足し算や掛け算を自分で実装する必要がある。このメモは、実際に試してみた第一夜の模様です。
253 は十進16桁にすぎないので100桁の素数をかけたりする現代暗号の世界をJavaScript上で楽しむには、多少の準備が要る。
例えば、7771234567890 という整数を、67890, 12345, 777 という3要素からなる配列で表現することにしよう。40桁の整数
12345 67890 22222 00000 33333 12345 44444 56789
は8要素の配列になる。各要素は絶対値において BIG_UNIT = 100000 より小さい。要素は昇順、すなわち整数の下5桁が配列の最初の要素、6桁め~10桁めが2番めの要素……となる。すべて10進数で考えている。負の数では全要素が負になる。
次の関数は、文字列として与えられた整数を上記の形式で格納した配列を返す。これはスケルトンである。より詳細な実装は、bigint.js参照。
function bset( canon ) { var Work = new Array(); var size = Math.ceil( canon.length / 5 ); // 最大要素の桁数 var len0 = canon.length % 5; if(len0==0) len0=5; // とりあえず降順に格納。先頭には0がないので必ず十進法で評価される Work[0] = eval( canon.substr( 0, len0 ) ); var pos = len0; var tmp; for( var i=1; i<size; i++ ) { tmp = canon.substr( pos , 5 ); pos += 5; // 数値に変換。必ず十進法で評価されるよう0を消す Work[i] = eval( "1" + tmp ) - BIG_UNIT; } // 昇順にする return my_reverse( Work ); }
逆にこの形式の配列を文字列としての自然な整数表現に変換する bshow() を作っておく。
badd( A, B ) は上記形式の2整数を足し算する。要素ごとに足し算して、BIG_UNITを越えた場合は、「1くりあがる」ようにする。つまり、その要素から BIG_UNIT を引いて、ひとつ上の要素に1を足す。負数を含む足し算まで考えると、少し工夫が要る。
以下のサンプルで、bsign は正負の判定、babscmpは絶対値の大小の判定。
function badd( B1, B2 ) { var sign1 = bsign(B1), sign2 = bsign(B2); var ret_sign; // 結果の正負の判定 if( sign1 * sign2 <0 ) { var abscmp = babscmp( B1, B2 ); if( abscmp > 0 ) ret_sign = sign1; else if( abscmp < 0 ) ret_sign = sign2; else ret_sign = 1; // 符号だけ異なって絶対値が同じ、和はゼロ } else if( sign1 >= 0 ) { ret_sign = 1; // 正の数同士の足し算 } else { ret_sign = -1; // 負の数同士の足し算 } var sum = new Array(); var max = Math.max( B1.length, B2.length ); for( var i=0; i < max; i++ ) { sum[i] = 0; if( B1[i] ) sum[i] += B1[i]; if( B2[i] ) sum[i] += B2[i]; } // くりあがり等 for( var i=0; i<sum.length; i++ ) { if( ret_sign >= 0 ) { // 結果は非負 while( sum[i] >= BIG_UNIT ) { sum[i]-=BIG_UNIT; if(typeof sum[i+1] == "number" ) sum[i+1]++; else sum[i+1] = 1; } while( sum[i] < 0 ) { sum[i]+=BIG_UNIT; sum[i+1]--; } } else { // 結果は負 while( sum[i] > 0 ) { sum[i]-=BIG_UNIT; sum[i+1]++; } while( sum[i] <= - BIG_UNIT ) { sum[i]+=BIG_UNIT; if(typeof sum[i+1] == "number" ) sum[i+1]--; else sum[i+1] = -1; } } } return bset( bshow( sum ) ); }
いったん bshow してから bset し直すことで格納形式を正規化する。数字の頭の0を消したり、近い数の引き算の結果、配列の長さが縮んだときに縮んだ部分を(0の入った配列でなく)未定義に戻して大きさと配列の長さが対応するようにするため。
引き算は全要素の符号を反転させる bneg を使って簡単に定義できる。
function bsub( B1, B2 ) { return ( badd( B1, bneg(B2) ) ); }
例えば3要素の整数と4要素の整数をかけるには、要素を総当たり(12組み)全部かけて、積を足せば良い。JavaScript自身の加法や乗法を使うとオーバーフローで精度が失われてしまうので、上の badd を使って、bmul を構成する。
function bmul( B1, B2 ) { var zeros, _mul; var Work = bset("0"); for( var idx1=0; idx1 < B1.length; idx1++ ) { for( var idx2=0; idx2 <B2.length; idx2++ ) { zeros = getZeros( ( idx1 + idx2 )*5 ); _mul = ( B1[idx1] * B2[idx2] ).toString(10) + zeros; Work = badd( Work, bset(_mul) ); } } return Work; } function getZeros( how_many ) { var _z = how_many % 10; var _z10 = (how_many - _z) / 10; var ret = ""; for( var i = 0; i < _z10; i++ ) ret += "0000000000"; for( var i = 0; i < _z; i++ ) ret += "0"; return ret; }
デモページのフォームにはA, B, C3つの入力欄がある。AとBの欄のそれぞれに大きな整数を書き込んでから実行ボタンをクリックしてみよう。次にA,B,C3つの欄すべてに巨大整数を入力してみよう。しょっぱなからあまりに大きなものを入れると計算時間が長くなりすぎるかもしれないので、10~20桁程度からいろいろ試してほしい。
AとBのみに入力した場合、それらの和と積を求める。Cも入れた場合、結合法則や分配法則のデモンストレーションを行い、計算間違いが生じていないか2通りに計算した結果を自動的に比較する。Windows 2000 上のIE6.0とNetscape4.79で動作確認した。以下は100桁程度の整数3つを入れた場合の出力例。これは53秒かかった。
A = 12345 67890 22345 67890 32345 67890 42345 67890 52345 67890 62345 67890 72345 67890 82345 67890 92345 67890
B = 333 33333 33335 55555 55555 00000 00000 00000 00777 77777 77777 77777 77777 77788 88888 88888 44444 44444 44222
X = A+B =333 45679 01225 77901 23445 32345 67890 42345 68668 30123 45668 40123 45668 50134 56779 71234 12335 36790 12112
A = X-B =12345 67890 22345 67890 32345 67890 42345 67890 52345 67890 62345 67890 72345 67890 82345 67890 92345 67890
OK!
B = X-A =333 33333 33335 55555 55555 00000 00000 00000 00777 77777 77777 77777 77777 77788 88888 88888 44444 44444 44222
OK!
X = (A+B)+C =11 11444 56779 01225 77901 26778 65678 00214 00330 58069 40007 55516 55110 64586 91876 41197 20775 97220 65281 18218
Y = A+(B+C) =11 11444 56779 01225 77901 26778 65678 00214 00330 58069 40007 55516 55110 64586 91876 41197 20775 97220 65281 18218
OK!
X = (A*B)*C =457 24736 67040 08535 28521 59731 75098 57851 98732 72216 07382 63656 75462 12291 46674 46059 98307 52619 87597 43724 32377 94439 41806 17489 01610 69323 68417 76537 81637 16605 73585 89499 96060 73430 88742 68657 66261 31020 27937 30357 75150 21930 72184 91569 07868 59150 40102 41205 24915 15855 15963 72042 85290 50770 39540 27480
Y = A*(B*C) =457 24736 67040 08535 28521 59731 75098 57851 98732 72216 07382 63656 75462 12291 46674 46059 98307 52619 87597 43724 32377 94439 41806 17489 01610 69323 68417 76537 81637 16605 73585 89499 96060 73430 88742 68657 66261 31020 27937 30357 75150 21930 72184 91569 07868 59150 40102 41205 24915 15855 15963 72042 85290 50770 39540 27480
OK!
X = (A+B)*C =3705 07544 54359 13580 24672 72016 45760 70208 62626 73290 32358 67811 66471 18914 27745 80607 63768 77462 02779 69691 66999 61942 58478 34043 20661 44378 45529 00907 68299 65553 11252 97441 26123 94130 72547 34217 23471 55872
Y = A*B+A*C =3705 07544 54359 13580 24672 72016 45760 70208 62626 73290 32358 67811 66471 18914 27745 80607 63768 77462 02779 69691 66999 61942 58478 34043 20661 44378 45529 00907 68299 65553 11252 97441 26123 94130 72547 34217 23471 55872
OK!
Cost: 53.316 sec.
この方法をさらに発展させれば、強度の高い「非自明な」RSAをJavaScript上で構築できるであろう。巨大剰余演算 bmod、巨大累乗計算 bpow などを実装する必要がある。詳しくは次回の予定。