32ビット整数の、定数での除算
2014年7月22日
プログラミングをしていると、整数値を10で割るというコードは良く出てくる。例えば、整数を10進法文字列に変換する場合など。
コンパイラーを用いて、10で割るコードを記述すると、アセンブリーを見たときに0xcccccccdを掛けて右35ビットシフトするコードになっていることがある。割り算はCPUにとって非常に高負荷な演算なので、掛け算とシフトに置き換えることで、高速化を図っているようだ。32ビットのほとんどのCPUでは、32ビットどうしの掛け算の結果を64ビット値として得、上位32ビットと下位32ビットを2つのレジスターに格納するようになっているので、「0xcccccccdを掛けて右35ビットシフト」は非常に効率がよい。
では、10以外の数値での割り算はどうかと、ネットで検索してみたが、包括的に解説している記事は見つからなかった。そこで、ちょっと調べてみた。
0xcccccccdという値は何かというと、0x100000000(4294967296)を10で割り、左3ビットシフトした値である。正確には3435973836.8であるが、整数値しか扱えなので切り上げて、3435973837(0xcccccccd)にしてある。右35ビットシフトは、0x800000000(34359738368)での割り算に等しい。従って、「0xcccccccdを掛けて右35ビットシフト」は、3435973837を掛けた後に34359738368で割る演算である。ここで、「3435973837を掛ける」のではなく「3435973836.8を掛ける」のであれば、数学的に正しい演算をしていることになるが、実際には0.2の誤差がある。この誤差が結果に影響を及ぼさなければ、「0xcccccccdを掛けて右35ビットシフト」は問題なく使えることになる。
「3435973836.8を掛ける」ではなく「3435973837を掛ける」場合、値が少し大きくなる可能性がある。コンピューターにおける整数型の演算では、割り算の結果を切り捨てにするから、数学的な演算の結果の少数部分が0.9になる場合に、異なる結果を生み出す可能性がある。逆に言えば、数学的な演算の結果の少数部分が0.9の場合でも常に同じ結果を生み出すのであれば、すべての整数値に関して正確に演算できることになる。
32ビット整数では、4294967296より小さな値しか扱わないから、この範囲内での最大の値、4294967289で同じ値になるかどうかを判断すればよい。4294967289*3435973837/34359738368=429496728.925であるので、切り捨てると同じ429496728になることが分かる。
では、他の整数値ではどうだろうかと、調べてみた。次のコードを、C++で実行。
結果の一部を、下に示す。
"num"で割る場合は、"mul"の値を掛けて、右"shift"ビットシフトすればよい。"valid"で示されたビット幅の整数値に関して、エラー無く演算できる。65536までの結果は、こちらを参照されたい。
結果を見て分かるように、すべてのケースに於いて、31ビット長の符号無し整数に関して、有効である。符号付き整数で考えれば、数値はすべて31ビット長で表現されるから、32ビット符号付き整数値のすべてに対応できることになる。C++での確認では、少なくとも、65536までの割り算には対応できる事が確認できた。
この方法が、32ビット符号付き整数・31ビット符号無し整数のすべてに関して有効であることは、数学的には、以下のように証明できる。
コンパイラーを用いて、10で割るコードを記述すると、アセンブリーを見たときに0xcccccccdを掛けて右35ビットシフトするコードになっていることがある。割り算はCPUにとって非常に高負荷な演算なので、掛け算とシフトに置き換えることで、高速化を図っているようだ。32ビットのほとんどのCPUでは、32ビットどうしの掛け算の結果を64ビット値として得、上位32ビットと下位32ビットを2つのレジスターに格納するようになっているので、「0xcccccccdを掛けて右35ビットシフト」は非常に効率がよい。
では、10以外の数値での割り算はどうかと、ネットで検索してみたが、包括的に解説している記事は見つからなかった。そこで、ちょっと調べてみた。
0xcccccccdという値は何かというと、0x100000000(4294967296)を10で割り、左3ビットシフトした値である。正確には3435973836.8であるが、整数値しか扱えなので切り上げて、3435973837(0xcccccccd)にしてある。右35ビットシフトは、0x800000000(34359738368)での割り算に等しい。従って、「0xcccccccdを掛けて右35ビットシフト」は、3435973837を掛けた後に34359738368で割る演算である。ここで、「3435973837を掛ける」のではなく「3435973836.8を掛ける」のであれば、数学的に正しい演算をしていることになるが、実際には0.2の誤差がある。この誤差が結果に影響を及ぼさなければ、「0xcccccccdを掛けて右35ビットシフト」は問題なく使えることになる。
「3435973836.8を掛ける」ではなく「3435973837を掛ける」場合、値が少し大きくなる可能性がある。コンピューターにおける整数型の演算では、割り算の結果を切り捨てにするから、数学的な演算の結果の少数部分が0.9になる場合に、異なる結果を生み出す可能性がある。逆に言えば、数学的な演算の結果の少数部分が0.9の場合でも常に同じ結果を生み出すのであれば、すべての整数値に関して正確に演算できることになる。
32ビット整数では、4294967296より小さな値しか扱わないから、この範囲内での最大の値、4294967289で同じ値になるかどうかを判断すればよい。4294967289*3435973837/34359738368=429496728.925であるので、切り捨てると同じ429496728になることが分かる。
では、他の整数値ではどうだろうかと、調べてみた。次のコードを、C++で実行。
#define div32(x,y,z) ((((unsigned long long)x)*((unsigned long long)y))>>z) int _tmain(int argc, _TCHAR* argv[]) { unsigned int i,j,num,div,rem,mul,shift,valid,mul2,shift2; unsigned int res1,res2; FILE* fh; // Open a file to store the result. fh=fopen("result.txt","w"); fprintf(fh,"num,mul,shift,valid\n"); // Test several numbers used for div. for(num=1;num<65536;num++){ // Determine the parameters div=(unsigned long long)0x100000000/(unsigned long long)num; rem=(unsigned long long)0x100000000%(unsigned long long)num; if (rem==0) { // num==2^n // Just shifting is the equlvalent of div. div=num; for(shift=0;(div=div>>1);shift++); fprintf(fh,"%d,0x00000001,%d,32\n",num,shift); } else { // Determine values to use. mul=div; shift=32; while(mul<0x80000000){ shift++; mul=((unsigned long long)((unsigned long long)1<<shift))/(unsigned long long)num; } mul++; // Check validity for(valid=32;valid;valid--){ j=0xFFFFFFFF>>(32-valid); for(i=0;i<num;i++){ // Check values "num" times from the largest ones. if (div32(j,mul,shift)!=j/num) break; j--; } if (i==num) break; } if (valid!=32) { // Try the other ways mul2=div; shift2=31; while(mul2<0x80000000){ shift2++; mul2=((unsigned long long)((unsigned long long)1<<shift2))/(unsigned long long)num; mul2++; // Check validity for(i=0;i<num;i++){ // Check values "num" times from the largest ones. j=0xFFFFFFFF-i; if (div32(j,mul2,shift2)!=j/num) break; } if (i==num) { // Found an available combination mul=mul2; shift=shift2; valid=32; } } } fprintf(fh,"%d,0x%x,%d,%d\n",num,mul,shift,valid); } } fclose(fh); return 0; }
結果の一部を、下に示す。
num,mul,shift,valid 1,0x00000001,0,32 2,0x00000001,1,32 3,0xaaaaaaab,33,32 4,0x00000001,2,32 5,0xcccccccd,34,32 6,0xaaaaaaab,34,32 7,0x92492493,34,31 8,0x00000001,3,32 9,0xe38e38e4,35,32 10,0xcccccccd,35,32 11,0xba2e8ba3,35,32 12,0xaaaaaaab,35,32 13,0x9d89d89e,35,32 14,0x92492493,35,31 15,0x88888889,35,32 16,0x00000001,4,32 17,0xf0f0f0f1,36,32 18,0xe38e38e4,36,32 19,0xd79435e6,36,31 20,0xcccccccd,36,32
"num"で割る場合は、"mul"の値を掛けて、右"shift"ビットシフトすればよい。"valid"で示されたビット幅の整数値に関して、エラー無く演算できる。65536までの結果は、こちらを参照されたい。
結果を見て分かるように、すべてのケースに於いて、31ビット長の符号無し整数に関して、有効である。符号付き整数で考えれば、数値はすべて31ビット長で表現されるから、32ビット符号付き整数値のすべてに対応できることになる。C++での確認では、少なくとも、65536までの割り算には対応できる事が確認できた。
この方法が、32ビット符号付き整数・31ビット符号無し整数のすべてに関して有効であることは、数学的には、以下のように証明できる。