簡體   English   中英

整數除法算法

[英]Integer division algorithm

我在思考一個大數除法的算法:用bigint D除以余數bigint C,我們知道基數b中C的表示,D是b ^ k-1的形式。 在一個例子中展示它可能是最容易的。 讓我們嘗試將C = 21979182173除以D = 999。

  • 我們將數字寫成三位數的集合:21 979 182 173
  • 我們從左邊開始采用連續集合的總和(模999):21 001 183 356
  • 我們在“超過999”之前的那些集合中加1:22 001 183 356

實際上,21979182173/999 = 22001183,其余為356。

我已經計算了復雜度,如果我沒弄錯的話,算法應該在O(n)中工作,n是基本b表示中C的位數。 我在C ++中也做了一個非常粗略和未經優化的算法版本(僅適用於b = 10),根據GMP的一般整數除法算法進行測試,它確實看起來比GMP更好。 在我看的任何地方都找不到這樣的東西,所以我不得不求助於對抗一般師。

我發現有幾篇文章討論了看起來非常相似的問題,但沒有一篇專注於實際的實現,特別是在不同於2的基礎上。我想這是因為數字在內部存儲的方式,盡管所提到的算法似乎很有用,比方說,b = 10,即使考慮到這一點。 我也嘗試過聯系其他人,但是,再一次無濟於事。

因此,我的問題是:是否有文章或書籍或其他描述上述算法的東西,可能討論實施? 如果沒有,那么嘗試在C / C ++中嘗試實現和測試這樣的算法是否有意義,或者這種算法本質上是不是很糟糕?

另外,我不是程序員,雖然我在編程上相當不錯,但我還是對計算機“內部”知之甚少。 因此,請原諒我的無知 - 這篇文章很可能有一個或多個非常愚蠢的事情。 再次抱歉。

非常感謝!


進一步澄清評論/答案中提出的觀點:

謝謝,每個人 - 因為我不想用同樣的事情評論所有偉大的答案和建議,我只想談談你提到的很多觀點。

我完全清楚,一般來說,在基地2 ^ n工作顯然是最有效的做事方式。 幾乎所有bigint庫都使用2 ^ 32或其他。 但是,如果(並且,我強調,它僅對這個特定算法有用!)我們將bigint實現為基數b中的數​​字數組? 當然,我們要求b在這里是“合理的”:b = 10,最自然的情況,似乎足夠合理。 我知道考慮到內存和時間,考慮到內部存儲數字的方式或多或少效率不高,但我能夠,如果我的(基本的和可能有些缺陷的)測試是正確的,產生的結果比GMP的一般部門更快,這對於實現這樣的算法是有意義的。

Ninefingers通知我必須在這種情況下使用昂貴的模運算。 我希望不是:我只能通過查看old + new + 1的位數來看看是否舊的+新交叉,例如999。 如果它有4位數字,我們就完成了。 更重要的是,由於舊<999和新<= 999,我們知道如果舊+新+ 1有4位數(它不能有更多),那么,(舊+新)%999等於刪除最左邊的數字(老+新+ 1),我認為我們可以廉價地做。

當然,我並沒有質疑這個算法的明顯局限性,也沒有聲稱它無法改進 - 它只能分成一定數量的數字,我們必須事先了解基數b中股息的表示。 然而,例如,對於b = 10,后者看起來很自然。

現在,我們已經實施了如上所述的bignums。 假設基數b中的C =(a_1a_2 ... a_n)且D = b ^ k-1。 算法(可能更加優化)會像這樣。 我希望沒有很多錯別字。

  • 如果k> n,我們顯然已經完成了
  • 在C的開頭添加一個零(即a_0 = 0) (以防萬一我們試圖除以9999和99)
  • l = n%k (“常規”整數的mod - 不應該太貴)
  • old =(a_0 ... a_l) (第一組數字,可能小於k位)
  • for(i = l + 1; i <n; i = i + k) (我們將有樓層(n / k)左右迭代)
    • 新=(A_I ... A_第(i + K-1))
    • new = new + old (這是bigint加法,因此O(k))
    • aux = new + 1 (再次,bigint加法 - O(k) - 我不高興)
    • 如果aux有超過k位數
      • 刪除aux的第一個數字
      • old = old + 1 (再次添加bigint)
      • 在開頭用零填充舊,所以它有盡可能多的數字
      • (a_(ik)... a_(i-1))= old (如果i = l + 1,(a _ 0 ... a _ l)= old)
      • 新= AUX
    • 在開頭填充新的零,所以它有盡可能多的數字
    • (A_I ... A_第(i + K-1)=新
  • QUOT =(A_0 ... A_(N-K + 1))
  • REM =新

在那里,感謝您與我討論 - 正如我所說的,在我看來,這似乎是一個有趣的“特殊情況”算法,試圖實現,測試和討論,如果沒有人看到任何致命的缺陷。 如果它到目前為止還沒有被廣泛討論,那就更好了。 請讓我知道你的想法。 抱歉這篇長篇文章。

另外,還有一些個人評論:

@Ninefingers:我實際上有一些(非常基本的!)GMP如何工作,它做什么以及一般bigint除法算法的知識,所以我能夠理解你的大部分論點。 我也知道GMP是高度優化的,並且在某種程度上為不同的平台定制自己,所以我當然不會試圖“擊敗它” - 這似乎與用尖頭棒攻擊坦克一樣富有成效。 然而,這不是這個算法的想法 - 它適用於非常特殊的情況(GMP似乎沒有涵蓋)。 在一個不相關的說明中,你確定在O(n)中完成了一般划分嗎? 我見過的最多的是M(n)。 (如果我理解正確,那么在實踐中(Schönhage-Strassen等)就不會達到O(n).Fürer算法仍然沒有達到O(n),如果我是正確的,幾乎是純粹的理論。)

@Avi Berger:雖然這個想法很相似,但實際上它似乎並不完全與“淘汰9”相同。 但是,如果我沒有弄錯的話,上述算法應該始終有效。

您的算法是基礎10算法的變體,稱為“輸出9”。 你的例子是使用基數1000並“逐出”999(比基數少一個)。 這曾經在小學教過,作為快速檢查手工計算的方法。 我有一個高中數學老師,他很驚訝地發現它不再被教導並且讓我們充滿了它。

在基數1000中輸出999將不能作為一般除法算法。 它將生成與實際商和余數一致的模999的值 - 而不是實際值。 你的算法有點不同,我沒有檢查它是否有效,但它是基於有效地使用基數1000和除數比基數小1。 如果您想嘗試將其除以47,則必須先轉換為基數為48的數字系統。

谷歌“淘汰了9”以獲取更多信息。

編輯:我最初讀的帖子太快了,你知道這是一個有效的算法。 由於@Ninefingers和@Karl Bielefeldt在他們的評論中已經比我更清楚地說明了,你在績效評估中沒有包括的是轉換成適合當前特定除數的基數。

我認為有必要根據我的評論添加到此。 這不是答案,而是對背景的解釋。

bignum庫使用所謂的肢體 - 在gmp源中搜索mp_limb_t,它通常是固定大小的整數字段。

當你做類似添加的事情時,一種方法(雖然效率低)接近它是這樣做的:

doublelimb r = limb_a + limb_b + carryfrompreviousiteration

在總和大於肢體大小的情況下,這個雙倍大小的肢體捕獲limb_a + limb_b的溢出。 因此,如果我們使用uint32_t作為我們的肢體大小,如果總數大於2 ^ 32,則可以捕獲溢出。

我們為什么需要這個? 好吧,你通常做的是循環遍歷所有的肢體 - 你自己完成了將你的整數划分並通過每一個 - 但我們先做LSL(所以最小的肢體)就像你做算術一樣用手。

這可能看起來效率低下,但這只是C方式。 為了真正打破大槍,x86將adc作為指令 - 添加隨身攜帶。 這樣做的是算術和字段,如果算術溢出寄存器的大小,則設置進位。 下次進行addadc ,處理器也會在進位位中計算。 在減法中,它被稱為借用標志。

這也適用於換檔操作。 因此,處理器的這一特性對於使bignums快速變化至關重要。 事實上,芯片中有電子電路用於完成這些工作 - 在軟件中進行操作總是會變慢。

沒有太多的細節,操作是通過這種添加,移位,減去等功能建立起來的。它們至關重要。 哦,如果你做得對,你可以使用處理器每個肢體的整個寬度。

第二點 - 基地之間的轉換。 不能在數字的中間取值並更改它的基數,因為您無法考慮原始基數下方數字的溢出,並且該數字無法解釋下方數字的溢出。 .. 等等。 簡而言之,每次要更改基礎時,都需要將整個bignum從原始基礎轉換回新基礎。 所以你必須至少三次走到bignum(所有四肢)。 或者,或者,在所有其他操作中檢測昂貴的溢出...記住,現在你需要進行模運算,以便在溢出時解決,而在處理器為我們做之前。

我還想補充一點,雖然你得到的東西可能很快就可以了,但請記住,作為一個bignum庫gmp為你做了很多工作,比如內存管理。 如果你正在使用mpz_你使用的抽象高於我在這里所描述的,對於初學者來說。 最后,gmp使用手動優化的裝配和展開的循環,幾乎可以聽到您聽過的每個平台,還有更多。 有一個很好的理由它與Mathematica,Maple等人一起發布。

現在,僅供參考,一些閱讀材料。

  • 現代計算機算術是任意精度庫的類似Knuth的工作。
  • Donald Knuth,系數算法(計算機程序設計藝術第二卷)。
  • William Hart關於為bsdnt實現算法的博客 ,其中討論了各種划分算法。 如果您對bignum圖書館感興趣,這是一個很好的資源。 在我開始關注這類東西之前,我認為自己是一名優秀的程序員

總結一下:除法匯編指令太糟糕了,所以人們通常會計算求逆並乘以,就像在模運算中定義除法時那樣。 存在的各種技術(參見MCA)主要是O(n)。


編輯:好的,並非所有技術都是O(n)。 大多數稱為div1的技術(除以不大於肢體的東西都是O(n)。當你變大時,你最終會有O(n ^ 2)的復雜性;這很難避免。

現在,您可以將bigints實現為數字數組嗎? 是的,當然可以。 但是,考慮一下這個想法

/* you wouldn't do this just before add, it's just to 
   show you the declaration.
 */
uint32_t* x = malloc(num_limbs*sizeof(uint32_t));
uint32_t* y = malloc(num_limbs*sizeof(uint32_t));
uint32_t* a = malloc(num_limbs*sizeof(uint32_t));
uint32_t m;

for ( i = 0; i < num_limbs; i++ )
{
    m = 0;
    uint64_t t = x[i] + y[i] + m;
    /* now we need to work out if that overflowed at all */
    if ( (t/somebase) >= 1 ) /* expensive division */
    {
        m = t % somebase; /* get the overflow */
    }
}

/* frees somewhere */

這是您通過計划添加內容的粗略草圖。 所以你必須在基數之間進行轉換。 因此,您需要轉換為基礎的表示 ,然后在完成后返回,因為此形式在其他任何地方都非常慢 我們這里並沒有談論O(n)和O(n ^ 2)之間的區別,但是我們討論的是每個肢體的昂貴的划分指令或者每次想要划分時的昂貴轉換 看到這個

接下來,您如何擴展您的一般案例部門的部門? 通過這個,我的意思是當你想從上面的代碼中划分這兩個數字xy 如果不采用昂貴的基於bignum的設施,你就不能這樣做。 見Knuth。 取模數大於你的大小是不行的。

讓我解釋。 嘗試21979182173 mod 1099.為了簡單起見,我們假設我們可以擁有的最大字段是三位數 這是一個人為的例子,但我所知道的最大字段大小是使用gcc擴展使用128位。 無論如何,重點是,你:

21 979 182 173

將你的號碼分成四肢。 然后你取模數和求和:

21 1000 1182 1355

它不起作用。 這是Avi正確的地方,因為這是一種鑄造9或其適應性的形式,但它在這里不起作用,因為我們的字段已經溢出一開始 - 你使用模數來確保每個字段保持在它的肢體/野外大小。

那么解決方案是什么? 將你的號碼分成一系列大小合適的bignums? 並開始使用bignum函數來計算你需要的一切? 這將比任何現有的直接操作字段的方式慢得多。

現在也許你只是提出這個案例來划分一個肢體,而不是一個bignum,在這種情況下它可以工作,但hensel划分和預先計算的反轉等沒有轉換要求 我不知道這個算法是否比hensel分區更快; 這將是一個有趣的比較; 這個問題伴隨着bignum圖書館的共同表現 在現有的bignum庫中選擇的表示是出於我擴展的原因 - 它在組裝級別上有意義,它首先完成。

作為旁注; 你不必使用uint32_t代表你的四肢。 您可以使用理想大小的系統寄存器大小(例如uint64_t),以便您可以利用程序集優化的版本。 因此,在64位系統adc rax, rbx如果結果溢出2 ^ 64位adc rax, rbx僅設置溢出(CF)。

tl;博士版:問題不是你的算法或想法; 這是在基數之間進行轉換的問題,因為你的算法所需的表示不是在add / sub / mul等中執行它的最有效方式。用來解釋knuth:這顯示了數學優雅和計算效率之間的區別。

如果你需要頻繁地除以相同的除數,使用 (或的冪)作為你的基數使得除法變得便宜,因為位移是基數為2的二進制整數。

如果你願意,可以使用base 999; 使用10次冪基數沒有什么特別之處,只是它使轉換為十進制整數非常便宜。 (你可以一次處理一個肢體而不必對整個整數進行完全除法。這就像將二進制整數轉換為十進制與將每4位轉換為十六進制數字之間的區別。二進制 - >十六進制可以啟動具有最高有效位,但是轉換為非2次冪基數必須是LSB優先使用除法。)


例如,要計算具有性能要求的代碼高爾夫問題的Fibonacci(10 9 )的前1000個十進制數字, 我的105字節的x86機器代碼答案使用與此Python答案相同的算法:通常的a+=b; b+=a a+=b; b+=a斐波納契迭代,但除以(的功率)10每次a變得太大。

斐波那契的增長速度比進位傳播的速度快,因此有時丟棄低十進制數字並不能長期改變高位數。 (你可以保留一些超出你想要的精度)。

通過2的冪划分是不行的,除非你跟蹤你有多少2的冪丟棄,因為最終的二進制文件- >末十進制轉換將取決於這一點。

所以對於這個算法,你必須進行擴展精度加法,除以10(或你想要的任何10的冪)。


我將基數為10的9個肢體存儲在32位整數元素中。 除以10 9是非常便宜的:只是一個指針增量跳過低肢。 我只是偏移下一次添加迭代使用的指針,而不是實際上做memmove

我認為除了10 ^ 9以外的10次冪除法會有些便宜,但需要在每個肢體上進行實際划分,並將其余部分傳播到下一個肢體。

這種方式的擴展精度加法比使用二進制分支更昂貴 ,因為我必須使用compare手動生成進位: sum[i] = a[i] + b[i]; carry = sum < a; (未簽名的比較)。 並且還使用條件移動指令基於該比較手動換行到10 ^ 9。 但我能夠將該結轉用作adc的輸入(x86 add-with-carry指令)。

你不需要一個完整的模數來處理附加的包裝,因為你知道你最多包裝一次。

這浪費了每個32位肢體的2比特:10 ^ 9而不是2^32 = 4.29... * 10^9 保存個10進制每一個字節是顯著更小的空間效率,以及更糟糕的表現非常多,因為8位二進制加法成本相同的64位二進制加法現代的64位CPU上。

我的目標是代碼大小:對於純粹的性能,我會使用64位肢體保持基數為10 ^ 19“數字”。 2^64 = 1.84... * 10^19 ,因此每64位浪費不到1位。)這使得每次硬件add指令的工作量增加了一倍。 嗯,實際上這可能是個問題:兩個肢體的總和可能會包裹64位整數,所以只檢查> 10^19就不夠了。 您可以在5*10^1810^18 ,或者執行更復雜的執行檢測,檢查二進制進位以及手動進位。

存儲打包的BCD,每4位半字節一位數,性能會更差,因為沒有硬件支持阻止從一個半字節到一個字節內的下一個字節。


總的來說,我的版本比同一硬件上的Python擴展精度版本快了大約10倍(但它有更大的速度優化空間,通過更少的划分)。 (70秒或80秒對12分鍾)

不過,我覺得對於這個特定的實現算法的(這里我只需要加法和除法,並每隔幾增補后發生分裂),基10 ^ 9肢體的選擇是非常好的。 對於Nth Fibonacci數,有更高效的算法不需要進行10億次擴展精度加法。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM