這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
寫在前面
某一種對象是通過兩個ID唯一確定的,如何處理這種資料結構以便快速尋找以及節約記憶體?先說一種笨方法——用字串來處理。這是比較容易想到的(我覺得一般最容易想到的也是最簡單粗暴的方法都是用字串來搞搞搞)。
fmt.Sprintf("%d_%d", id1, id2)
這樣就成了。儲存的時候用字串來儲存,查詢比較的時候用字串的方法來計算。當然,把數字當作字串來儲存和計算本身就是極其浪費記憶體和CPU的。
Cantor pairing function 簡介
康托爾配對 - Cantor pairing function,是一種將兩個自然數轉成唯一一個自然數的方法。具體原理我就不說了,我也看不懂。。。簡單地說:
- 只支援自然數。自然數是整數(自然數包括正整數和零);
- 支援反解;
f(k1, k2)
和f(k2, k1)
得到的結果是不同的。當然,如果你想得到相同的也是可以的,計算支援把兩個數字排個序;
- 計算結果有可能比
k1
和k2
都大很多,需要注意溢出的問題。
Example
這個演算法只有一個公式,實現起來很容易。我索性自己造了一個輪子,pairing。公式的推導請移步維基百科。
import "github.com/mnhkahn/pairing"pair := pairing.Encode(k1, k2)k3, k4 := pairing.Decode(pair2)
支援正向編碼以及反向解碼。
實現
import "math"func Encode(k1, k2 uint64) uint64 {pair := k1 + k2pair = pair * (pair + 1)pair = pair / 2pair = pair + k2return pair}
-
func Decode(pair uint64) (uint64, uint64) { w := math.Floor((math.Sqrt(float64(8pair+1)) - 1) / 2) t := (ww + w) / 2
k2 := pair - uint64(t) k1 := uint64(w) - k2 return k1, k2 }
與其它資料結構的對比
其中一個要對比的就是和前面說的字串的比較。還有一種代替方案:兩個整數int32
,這64位元字拼接一個int64
裡面,第一個數字占前32位,後一個數字佔後32位,也是一個可行的方案。我們可以把這個方法叫做bit方法。
func EncodeBit(k1, k2 uint32) uint64 {pair := uint64(k1)<<32 | uint64(k2)return pair}func DecodeBit(pair uint64) (uint32, uint32) {k1 := uint32(pair >> 32)k2 := uint32(pair) & 0xFFFFFFFFreturn k1, k2}
實現起來更簡單。那和Cantor pair相比效能怎麼樣呢?
import ("fmt""testing")var TEST_PAIRS = [][]uint64{[]uint64{0, 0},[]uint64{0, 1},[]uint64{1, 0},}var TEST_RESs = []uint64{0,2,1,}var TEST_RESBits = []uint64{0,1,4294967296,}func TestPair(t *testing.T) {for i, p := range TEST_PAIRS {a, b := p[0], p[1]if pair := Encode(a, b); pair != TEST_RESs[i] {t.Error(a, b, pair)}}for i, p := range TEST_PAIRS {a, b := p[0], p[1]pair := TEST_RESs[i]if x, y := Decode(pair); x != a || y != b {t.Error(a, b, pair)}}fmt.Println(Encode(559, 83792))for i, p := range TEST_PAIRS {a, b := uint32(p[0]), uint32(p[1])if pair := EncodeBit(a, b); pair != TEST_RESBits[i] {t.Error(a, b, pair)}}for i, p := range TEST_PAIRS {a, b := uint32(p[0]), uint32(p[1])pair := TEST_RESBits[i]if x, y := DecodeBit(pair); x != a || y != b {t.Error(a, b, pair)}}fmt.Println(EncodeBit(559, 83792))}func BenchmarkEncode(b *testing.B) {for i := 0; i < b.N; i++ {Encode(559, 83792)}}func BenchmarkDecode(b *testing.B) {for i := 0; i < b.N; i++ {Decode(3557671568)}}func BenchmarkEncodeBit(b *testing.B) {for i := 0; i < b.N; i++ {EncodeBit(559, 83792)}}func BenchmarkDecodeBit(b *testing.B) {for i := 0; i < b.N; i++ {DecodeBit(2400886802256)}}func BenchmarkEncodeStr(b *testing.B) {for i := 0; i < b.N; i++ {_ = fmt.Sprintf("%d_%d", 559, 83792)}}/*go test -bench=. -benchmemBenchmarkEncode-2 2000000000 0.37 ns/op 0 B/op 0 allocs/opBenchmarkDecode-2 50000000 26.9 ns/op 0 B/op 0 allocs/opBenchmarkEncodeBit-2 2000000000 0.37 ns/op 0 B/op 0 allocs/opBenchmarkDecodeBit-2 2000000000 0.37 ns/op 0 B/op 0 allocs/opBenchmarkEncodeStr-2 5000000 271 ns/op 32 B/op 3 allocs/op*/
結論
- 用字串儲存的效能最差,剩餘兩個是這個效能的400倍;
- Cantor pair和bit方法的效能相當,反解的時候效能還查一些;
- 既然效能相當,雖然本文著重介紹的是Cantor pair演算法,但我還是建議,如果bit方法能滿足你的需求,尤其是數字範圍較小的時候,還是用這個比較好。簡單的方法在維護方面會讓你更加得心應手,即使你懂那些複雜的公式是如何推匯出來的^ ^。
本文所涉及到的完整源碼請參考。
原文連結:Golang 最佳化之路——Cantor pair,轉載請註明來源!