計算機中整數加法滿足結合律嗎


 
 
今天看《程序設計語言概念》(Concepts of Programming Language),第七章“結合性”一節中有這么一段:
 
某些計算機中的整數加法不具有結合性。例如,假設一個程序要計算“A + B + C + D”,其中A、C是很大的正數,B、D是絕對值很大的負數。在這種情況下,將B加到A並不會導致溢出,但將C加到A就會溢出。B和D與此類似。
 
這段話很好理解,因為只要是程序員,整數計算可能會溢出是基本的常識。但這段話只談到計算的中間結果發生溢出的情況。如果不考慮中間結果而將重點放在最終結果上,計算順序是否依然會對結果產生影響呢?也就是說,計算機中的加法滿足結合律嗎?即:
 
(a + b) + c
 
是否一定等於
 
(a + c) + b
 
呢?
 
首先,如果每個中間結果以及最終結果都沒有溢出,可以肯定必然是滿足結合律的,否則就是計算機自身有錯誤。
 
如果中間結果發生了溢出會怎樣?我們不妨編寫一個簡單的程序驗證一下。這里我們選擇四個整數,a和c是兩個是很大的正數,b和d是兩個很小的負數(即絕對值很大的負數)。
 
int a =  2147483392; //0x7fffff00;
int b = -2147479553; //0x80000fff;
int c =  2146500592; //0x7ff0fff0;
int d = -2147421968; //0x8000f0f0;
int sum1 = ((a + b) + c) + d;
int sum2 = (a + b) + (c + d);
int sum3 = (a + c) + (b + d);
System.out.println("((a + b) + c) + d=" + sum1);
System.out.println("(a + b) + (c + d)=" + sum2);
System.out.println("(a + c) + (b + d)=" + sum3);

 

sum1為從左到右依次計算a+b+c+d的和;sum2先計算a+b和c+d,然后再計算二者的和;sum3則先計算a+c和b+d,然后再求和。通過代碼可以看出,sum1和sum2的中間結果沒有發生溢出,但sum3在計算a+c和b+d時都發生了溢出。下面來看看實際運行結果:
 
((a + b) + c) + d=-917537
(a + b) + (c + d)=-917537
(a + c) + (b + d)=-917537

 

可以看到無論順序如何結果都是正確的。所以我們可以得出結論:
 
計算機中的整數加法運算滿足結合律
 
這里還有一個問題,在上面的例子中,雖然中間結果發生了溢出,但最終結果是沒溢出的。那如果最終結果也溢出了會怎么樣?答案是不同計算順序得到的結果仍然一樣,只不過結果都是錯的(都溢出了)。這是由計算機本身有限的精度導致的,和結合律無關,所以這種情況仍然認為是符合結合律的。你可以自己寫個程序來驗證這一點。
 
不過要注意,這個結論是有限定條件的,對現代大多數計算機系統來說該結論都成立,因為這些系統通常都采用“二進制補碼”的方式來存儲整數,而二進制補碼的加法運算是符合結合律的。不滿足結合律的例子也是有的,比如BCD碼的加法運算。
 
------------------------------------------------------------------
寫到這里,我想到了一個老題目:如何在不引入臨時變量的情況下交換2個整數的值?一般來說,有2種方法可以做到,一種是使用加法,另一種是使用異或:
 
a = a + b;
b = a - b;
a = a - b;

 

a = a ^ b;
b = a ^ b;
a = a ^ b;

 

有人說第一種方法有問題,原因是將a和b相加時可能會溢出。如果你看了這篇文章,就會知道這種說法是錯誤的了——雖然a+b可能會溢出,但最后仍能得到正確的結果。要說缺點,只是它的效率比第二種要低一些。但話說回來,它的可讀性卻要優於第二種。
------------------------------------------------------------------
 
補碼簡介
 
下面簡單介紹一下補碼,如果對此不感興趣或已比較熟悉請略過。二進制補碼(Two's complement)采用“2^N的補”的方式存儲的整數編碼(其中N為整數的位長)。相比而言,另一種存儲方式“反碼”采用的是“1的補”,即逐位計算各個位的補(1的補為0,0的補為1,在二進制中這和取反是一樣的),因此反碼的英文名稱為“One's complement”。
 
補碼可以認為是對反碼的改進,這不但因為補碼中統一了“正零和負零”,還因為其計算也要比反碼容易。最主要的一點是補碼不用考慮進位(即溢出位),而反碼則必須考慮。補碼的另一個優點是其符號位同時也是計算位,因此計算時無需對正數和負數區別對待,這一點和反碼一樣。與補碼和反碼不同,原碼則必須同時考慮數的正負和進位,因此很少有系統采用原碼的方式來存儲整數。
 
下面分別用補碼和反碼的方式來計算“10 - 1”,以此加深理解。
 
由於大多數計算機只實現了加法而沒有減法,因此“10 - 1”實際上是轉換為“10 + (-1)”來計算的。為了簡單,這里假設整數只有8位。
 
補碼的計算過程如下(-1的補碼為“1111 1111”):
 
    0000 1010
+ 1111 1111
  ——————
  1 0000 1001
 
結果發生了溢出,產生了一個進位,對補碼來說簡單忽略即可,因此最后的結果為“9”。
 
注意這里的溢出屬於正常溢出。相比之下,如果正數+正數結果為負數,或負數+負數結果為正數時,則說明發生了不正常的溢出。正常的溢出結果仍然是正確的(這正是補碼的特性),而不正常的溢出得到的是錯誤的結果。
 
反碼的計算過程為(-1的反碼為“1111 1110”):
 
    0000 1010
+ 1111 1110
  ——————
 1 0000 1000
 
同樣發生了溢出,但此時不能忽略進位,否則將得到錯誤結果“8”,因此還需要把進位加到結果上:
 
    0000 1000
+                1
  ——————
    0000 1001
 
得到最終結果“9”。
 
總結
 
最后再來簡單總結一下。在大多數計算機系統中,整數的加法運算滿足結合律。具體來說,如果最終結果沒有溢出,即使計算過程的中間結果出現了溢出也不會影響最終結果。而如果最終結果本身就是溢出的,改變計算順序仍然會得到一致的結果,這時候仍然認為是滿足結合律的。
 
雖然這篇文章對實際編程可能用處不大,因為我們通常只需注意最終結果不要溢出即可,對中間過程無需在意。但這篇文章為這個結論提供了一定的理論支持,以幫助我們加深對計算機整數加法運算的理解。
 

 
參考資料:
 
 
 
 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM