Lua大整數的實現

大整數

程序中基礎的數據類型,如doubleint64_t之類的,其大小都是有上限的,假如有一個數10000000000...(後面接10000個0),那麼現在的數據類型是表示不了的,這時候就需要可以無限增長的整數,即大整數。作爲一個遊戲開發的程序員,我怎麼也沒想到需要用到大整數。雖然這幾年遊戲的數值比之前大幅提升(小時候玩的遊戲,攻擊、防禦這些基本都是三位數以下,現在輕鬆達到十幾億),但是用個64位的類型還是可以應付的。然而策劃腦洞大開,要求遊戲中的貨幣上限提高到10000個數字大小。

實現

一開始,我並沒有覺得這個事情有多麻煩,畢竟一個int類型表示不了,我就用兩個,兩個表示不了,我就用三個。。。這樣就用一個int數組實現了一個大整數,用模擬豎式運算,簡單實現了加減法。豎式運算即平時我們手工運算時用的方式

   99           198
+  99         -  32
------        ------
  198           166

由於我用的是int數組,當然是不會涉及到每一個數字的加減(也沒有必要),只是模擬了這種方式的進位和退位,例如加法的lua實現(僅示例,沒處理異常和邊界)

local bigint = {0}

-- 實現大整數和一個普通數字相加
local function add(bigint, num)
    bigint[1] = bigint[1] + num

    -- 數組中每個數字最大爲uint32上限,超過即向數組前一位進位
    local i = 1
    while bigint[i] > 0xFFFFFFFF do
        bigint[i] = bigint[i] - 0xFFFFFFFF
        bigint[i +1] = 1 + (bigint[i +1] or 0)

        i = i + 1
    end
end

不過當我實現了加減法後,棘手的事來了,乘法、除法怎麼實現?怎麼轉換爲10進制字符串?我一開始並沒有想到要實現這些功能,但是隨着開發的推進,策劃的需求裏需要計算貨幣N倍加成,調試時需要輸出10進制字符串,這顯然超出我的知識範疇了。乘除法雖然用得多,但如果需要我去實現,這個我還真不知道是如何實現的,只得硬着頭皮查資料學習。所幸kedixa的博客裏有比較詳細的C++實現,也有示例代碼,最終順利地在lua實現了大整數的四則運算及10進制字符串轉換。

到這裏,本來就結束了,然而後期在使用這個大整數的時候,偶爾會出現10進制字符串轉換、乘除法運算出錯的問題。仔細排查後,沒有發現算法的實現問題,而且只有數字比較大的時候才能重現,不得不把kedixa的C++版本拿下來交叉對比,最終發現是lua 5.3的一些實現和C++是有區別的,比如-2251624706 >> 32 在C++中是-1,在lua中是4294967295,而C++中兩個整數相除,直接得到一個整數,而lua只能用math.floor來轉換,math.floor(253923710799999577 / 100000000) = 2539237107 偶爾返回2539237108,最終不得不把一部分算法實現放到C++去。

另一個嚴重的問題是性能非常不理想。在數字非常大時(10000個10進制字符串),每秒只能運算1000次不到。轉換爲10進制字符串更是需要數秒,原因是lua中的字符串是immutable的,每一次拼接字符串都需要產生一個新的字符串,而10000個10進制字符串就需要拼接10000次,這個完全無法接受。

之所以用lua實現,是因爲遊戲服務器業務邏輯都是用lua實現的,一開始並不想給大整數開這個特例。竟然沒法滿足需求,就不得不尋找替代方案。

其他實現方案

  • faheel
    std::string保存10進制字符串的實現,即數字123456在內部實現裏就是保存爲字符串123456,可以預估這種實現的運算會比較慢,而且佔用內存比較大,最大的優點是不需要轉換成10進制字符串。不過這個實現居然是github上搜索big int結果中C++實現得星最多的,實在出乎我的意料。畢竟一開始我沒查任何資料,第一想到的方案是用int數組來實現。
  • kasparsklavins
    std::vector<int>保存10進制數字的實現,即數字123456在內部實現裏就是用一個數字分別保存了1、2、3、4、5、6這幾個數字,其實和上面的實現差不多。這個庫並沒有實現除法,我把它單獨拿出來對比是因爲它的實現數據結構和其他的不一樣。
  • kedixa
    std::vector<uint32_t>按位保存的實現,即上面數組中一個數字滿0xFFFFFFFF即向前進1的實現,這是我認爲比較正常的一個實現。
  • boost
    沒錯,這個就是大名鼎鼎的boost庫的實現,其內部實現其實和kedixa差不多,不過默認使用的是std::vector<uint64>,編譯時可配置
// cpp_int_config.hpp line 63
typedef detail::largest_unsigned_type<64>::type limb_type;
  • gmp
    GMP是The GNU Multiple Precision Arithmetic Library的簡稱,由GNU維護,號稱Arithmetic without limitations。這個庫原本爲科學研究設計的,包含大整數、大有理數、大浮點數三個庫,並且對性能進行了極致的優化,採用了大量的彙編和特定的CPU指令。不過也正因爲如此,這個庫的要移植到win下是比較有難度的,因爲其中很大一部分是彙編實現的,並且編譯這個庫的時候,會檢測CPU的類型,根據不同的CPU指令集採用不同的彙編。而且其開源協議是GNU LGPL v3GNU GPL v2,不太適合商用。

針對這些庫,我做了一些簡單的測試,其中循環10次的測試結果如下

optimize=O2 TIMES=10

# make libperf
g++ -std=c++11 -I../boost_1_74_0 -Wall -g3 -O2 -o test_lib_perf \
        ./lib_perf/kedixa/unsigned_bigint.cpp \
        ./lib_perf/kasparsklavins/bigint.cpp \
        ./lib_perf/lib_perf.cpp \
        -lgmpxx -lgmp
./test_lib_perf
faheel create time elapsed 98us
faheel add time elapsed 38256us
faheel dec time elapsed 43456us
faheel mul time elapsed 12309486us
faheel div time elapsed 191761534us
faheel to_string time elapsed 96us
kasparsklavins create time elapsed 232us
kasparsklavins add time elapsed 325us
kasparsklavins dec time elapsed 119us
kasparsklavins mul time elapsed 71310us
kasparsklavins to_string time elapsed 2100us
kedixa create time elapsed 5383us
kedixa add time elapsed 8us
kedixa dec time elapsed 11us
kedixa mul time elapsed 6604us
kedixa div time elapsed 6813us
kedixa to_string time elapsed 14514us
boost create time elapsed 1914us
boost add time elapsed 203us
boost dec time elapsed 12us
boost mul time elapsed 2625us
boost div time elapsed 10304us
boost to_string time elapsed 137713us
gmp create time elapsed 909us
gmp add time elapsed 9us
gmp dec time elapsed 6us
gmp mul time elapsed 1294us
gmp div time elapsed 1176us
gmp to_string time elapsed 1564us
test done, TIMES = 10, BASE BIT = 10000, MUL BIT = 1000

可以看到,前兩個庫的效率相當差,都不在一個量級的,甚至在循環1000次的測試中,faheel因耗時太長無法完成測試。kedixa和boost接近,畢竟實現方式基本一致。而採用了彙編和CPU指令優化的gmp一騎絕塵,性能比boost都要高出一個數量級。

最終,在考慮了代碼質量、性能、可移植性後,我基於boost寫了一個lua庫,編譯後可直接在lua使用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章