プログラミング

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++で実行。
#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ビット符号無し整数のすべてに関して有効であることは、数学的には、以下のように証明できる。
2014-07-22-math1.png
2014-07-22-math2.png

コメント

コメントはありません

コメント送信