帶正負號的整數的除法與餘數

來源:互聯網
上載者:User
帶正負號的整數的除法與餘數Division of Signed Integers

陳碩 giantchen_AT_gmail_DOT_com

 

最近研究整數到字串的轉換,讀到了 Matthew Wilson 的《Efficient Integer to String Conversions》系列文章。(http://synesis.com.au/publications.html 搜 conversions)。他的巧妙之處在於,用一個對稱的 digits 數組搞定了負數轉換的邊界條件(二進位補碼的正負整數表示範圍不對稱)。代碼大致如下,經過改寫:

 

const char* convert(char buf[], int value)<br />{<br /> static char digits[19] =<br /> { '9', '8', '7', '6', '5', '4', '3', '2', '1',<br /> '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };<br /> static const char* zero = digits + 9; // zero 指向 '0'<br /> // works for -2147483648 .. 2147483647<br /> int i = value;<br /> char* p = buf;<br /> do {<br /> int lsd = i % 10; // lsd 可能小於 0<br /> i /= 10; // 是向下取整還是向零取整?<br /> *p++ = zero[lsd]; // 下標可能為負<br /> } while (i != 0);<br /> if (value < 0) {<br /> *p++ = '-';<br /> }<br /> *p = '/0';<br /> std::reverse(buf, p);<br /> return p; // p - buf 即為整數長度<br />}

 

 

這段簡短的代碼對 32-bit int 的全部取值都是正確的(從 -2147483648 到 2147483647)。可以視為 itoa() 的參考實現,面試的標準答案。

讀到這份代碼,我心中頓時升起一個疑慮:《C Traps and Pitfalls》第7.7節講到,C 語言中的整數除法(/)和模數(%)運算在運算元為負的時候,結果是 implementation-defined。(網上能下載到的一份簡略版也有相同的內容,http://www.literateprogramming.com/ctraps.pdf 第7.5節。)

也就是說,如果 m、d 都是整數,
int q = m / d;
int r = m % d;
那麼C語言只保證 m == q*d + r。如果 m、d 當中有負數,那麼 q 和 r 的加號或減號是由實現決定的。比如 (-13)/4 == (-3)或 (-13)/4 == (-4) 都是合法的。如果採用後一種實現,那麼這段轉碼就錯了(因為將有 (-1) % 10 == 9)。只有商向 0 取整,代碼才能正常工作。

為了弄清這個問題,我研究了一番。

 

語言標準怎麼說
  • C89

我手頭沒有 ANSI C89 的文稿,只好求助於 K&R88,此書第 41 頁第 2.5 節講到 The direction of truncation for / and the sign of the result for % are machine-dependent for negative operands, ...。確實是實現相關的。為此,C89 專門提供了 div() 函數,這個函數算出的商是向 0 取整的,便於編寫可移植的程式。我得再去查 C++ 標準。

  • C++98

第 5.6.4 節寫到 If the second operand of / or % is zero the behavior is undefined; otherwise (a/b)*b + a%b is equal to a. If both operands are nonnegative then the remainder is nonnegative; if not, the sign of the remainder is implementation-defined. C++也沒有規定餘數的加號或減號(C++03 的敘述一模一樣)。

不過這裡有一個註腳,提到 According to work underway toward the revision of ISO C, the preferred algorithm for integer division follows the rules defined in the ISO Fortran standard, ISO/IEC 1539:1991, in which the quotient is always rounded toward zero. 即 C 語言的修訂標準會採用和 Fortran 一樣的取整演算法。我又去查了 C99。

  • C99

第 6.5.5.6 節說 When integers are divided, the result of the / operator is the algebraic quotient with any fractional part discarded. (腳註:This is often called "truncation toward zero".) C99 明確規定了商是向0取整,也就意味著餘數的符號與被除數相同,前面的轉換演算法能正常工作。C99 Rationale (http://www.open-std.org/jtc1/sc22/wg14/www/C99RationaleV5.10.pdf) 提到了這個規定的原因,In Fortran, however, the result will always truncate toward zero, and the overhead seems to be acceptable to the numeric programming community. Therefore, C99 now requires similar behavior, which should facilitate porting of code from Fortran to C. 既然 Fortran 在數值計算領域都做了如此規定,說明開銷(如果有的話)是可以接受的。

  • C++0x (x已經確定無疑是個十六進位數了)

最近的 n2800 草案第 5.6.4 節採用了與 C99 類似的表述:For integeral operands the / operator yields the algebraic quotient with any fractional part discarded; (This is often called truncation towards zero.) 可見 C++ 還是儘力保持與 C 的相容性。

小結:C89 和 C++98 都留給實現去決定,而 C99 和 C++0x 都規定商向0取整,這算是語言的進步吧。

C/C++編譯器的表現

我主要關心 G++ 和 VC++ 這兩個編譯器。需要說明的是,用代碼案例來探查編譯器的行為是靠不住的,儘管前面的代碼在兩個編譯器下都能正常工作。除非在文檔裡有明確表述,否則編譯器可能會隨時更改實現--畢竟我們關心的就是 implementation-defined 行為。

  • G++ 4.4

http://gcc.gnu.org/onlinedocs/gcc/Integers-implementation.html
GCC always follows the C99 requirement that the result of division is truncated towards zero.
G++ 一直遵循 C99 規範,商向0取整,演算法能正常工作。

  • Visual C++ 2008

http://msdn.microsoft.com/en-us/library/eayc4fzk.aspx
The sign of the remainder is the same as the sign of the dividend.
這個說法與商向0取整是等價的,演算法也能正常工作。

其他語言的規定

既然 C89/C++98/C99/C++0x 已經很有多樣性了,索性弄清楚其他語言是怎麼定義整數除法的。這裡只列出我(陳碩)接觸過的幾種常用語言。

  • Java

http://java.sun.com/docs/books/jls/third_edition/html/expressions.html#15.17.2
Java 語言規範明確說 Integer division rounds toward 0. 另外對於 int 整數除法溢出,特別規定不拋異常,且 -2147483648 / -1 = -2147483648 (以及相應的long版本)。

  • C#

http://msdn.microsoft.com/en-us/vcsharp/aa336809.aspx
C# 3.0 語言規定 The division rounds the result towards zero. 對於溢出的情況,規定在 checked 上下文中拋 ArithmeticException 異常;在 unchecked 上下文裡沒有明確規定,可拋可不拋。(據瞭解,C# 1.0/2.0 可能有所不同。)

  • Python

Python 在語言參考手冊的顯著位置標明,商是向負無窮取整。Plain or long integer division yields an integer of the same type; the result is that of mathematical division with the `floor' function applied to the result.
http://docs.python.org/reference/expressions.html#binary-arithmetic-operations

  • Ruby

Ruby 的語言手冊沒有明說,不過庫的手冊說到也是向負無窮取整。The quotient is rounded toward -infinity.
http://www.ruby-doc.org/docs/ProgrammingRuby/html/ref_c_numeric.html#Numeric.divmod

  • Perl

Perl 語言預設按浮點數來計算除法,所以沒有這個問題。Perl 的整數模數運算規則與Python/Ruby一致。

http://perldoc.perl.org/perlop.html#Multiplicative-Operators
不過要注意,use integer; 有可能會改變運算結果,例如。

print -10 % 3; // => 2</p><p>use integers;<br />print -10 % 3; // => -1

  • Lua

Lua 預設沒有整數類型,除法一律按浮點數來算,因此不涉及商的取整問題。

 

可以看出,在整數除法的取整問題上,語言分為兩個陣營,指令碼語言彼此是相似的,C99/C++0x/Java/C# 則屬於另一個陣營。既然 Python 和 Ruby 都是用 C 實現的,但是運算規則又自成一體,那麼必定能從代碼中找到證據。

 

Python 的代碼很好讀,我很快就找到了 2.6.4 版實現整數除法和模數運算的函數 i_divmod()
http://svn.python.org/view/python/tags/r264/Objects/intobject.c?revision=75707&view=markup
注意到這段代碼甚至考慮了 -2147483648 / -1 在32-bit下會溢出這個特殊情況,讓我大吃一驚。宏定義UNARY_NEG_WOULD_OVERFLOW 和函數 int_mul() 前面的注釋也值得一讀。

 

Ruby 的代碼要混亂一些,花點時間還是能找到,這是 1.8.7-p248 的實現,注意 fixdivmod() 函數。
http://svn.ruby-lang.org/cgi-bin/viewvc.cgi/tags/v1_8_7_248/numeric.c?view=markup
注意到 Ruby 的 Fixnum 整數的表示範圍比機器字長小1bit,直接避免了溢出的可能。

硬體實現

既然 C/C++ 以效率著稱,那麼應該是貼近硬體實現的。我考察了幾種熟悉的硬體平台,它們基本都支援 C99/C++0x 的語意,也就是說新規定沒有額外開銷。列舉如下。(其實我們只關係帶符號除法,不過為了完整性,這裡一併列出 unsigned/signed 整數除法指令。)

  • Intel x86/x64

Intel x86 系列的 DIV/IDIV 指令明確提到是向0取整,與 C99/C++0x/Java/C# 一致。

  • MIPS

很奇怪,我在 MIPS 的參考手冊裡沒有查到 DIV/DIVU 指令的取整方向,不過根據 Patternson&Hennessy 的講解,似乎向0取整硬體上實現起來比較容易。或許我沒找對地方?

  • ARM/Cortex-M3

ARM 沒有硬體除法指令,所以不存在這個問題。Cortex-M3 有硬體除法,SDIV/UDIV 指令都是向0取整。Cortex-M3 的除法指令不能同時算出餘數,這很特殊。

  • MMIX

MMIX 是 Knuth 設計的 64-bit CPU,替換原來的 MIX 機器。DIV 和 DIVU 都是向負無窮取整(依據 TAOCP 第1.2.4節的定義,在第一卷 40 頁頭幾行),這是我知道的惟一支援 Python/Ruby 語義的"硬體"平台。

 

總結:想不到小小的整數除法都有這麼大名堂。一段只涉及整數運算的代碼,即便能在各種文法相似的語言裡運行,結果也可能完全不同。

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.