最近遇到一個需求,需要做基於日誌標籤的實時分發,基本原理是一條日誌傳遞到程序後,系統採集該日誌的標籤屬性,跟後臺的預定義的多個規則標籤集合進行匹配,如果Ok則將規則與該日誌綁定,等待後期的處理。
比如一條業務日誌具有標籤: dns, usa, 192.168.0.1, 規則庫的當前規則示例:
- 規則1 : dns
- 規則2: dns, usa
- 規則3: dns, usa, 192.168.0.2
則前兩個規則分別捕獲該日誌,用於下一步處理。所以這就是一個簡單的匹配問題。針對集合匹配的話,最簡單直接的方式是兩次循環,查詢是否包含子集合。爲簡單起見這裏使用標籤ID,也就是一個int數組表示一組標籤:
func isSubsetWithLoop(originalSet []int, newSet []int) bool {
sizeOfOriginal := len(originalSet)
sizeOfNewSet := len(newSet)
found := 0
if sizeOfNewSet == 0 || sizeOfOriginal < sizeOfNewSet {
return false
}
for _, i := range newSet {
inner:
for _, j := range originalSet {
// println(i, j)
if i == j {
found++
break inner
}
}
}
return sizeOfNewSet == found
}
但是考慮到兩次循環程序的執行復雜度基本上O(n*m)的時間複雜度。所以考慮有沒有更快的方式解決問題。主要的思路有兩個:
- 排序集合一,對於集合二的數據進行二分查找,獲取是否存在集合一中。時間複雜度 基本上O(mlogm + nlogm) +
- 排序集合一和集合二, 通過位移的方式比較兩個集合是否包含的關係,時間複雜度 基本上是O(mlogm + nlogn)
- 使用map集合保存集合一的數據,查詢集合二中數據是否存在map中,時間複雜度 基本上是O(m + n)
看上去使用map可以獲得比較好的時間複雜度算法,具體實施上如下面的所示:
func isSubsetWithMap(originalSet []int, newSet []int) bool {
sizeOfOriginal := len(originalSet)
sizeOfNewSet := len(newSet)
if sizeOfNewSet == 0 || sizeOfOriginal < sizeOfNewSet {
return false
}
mapOfArray := make(map[int]bool, sizeOfOriginal)
for _, v := range originalSet {
mapOfArray[v] = true
}
for _, i := range newSet {
if !mapOfArray[i] {
return false
}
}
return true
}
壓力測試
根據上面的代碼編寫對應的壓力測試,看一下性能是否和之前猜想的一致, 下面的代碼生成對應的測試矩陣, 設定初始規則爲3個標籤,而日誌標籤的範圍則從8-1024, 儘管可能實際並不會那麼多個標籤。
func BenchmarkIsSubset(b *testing.B) {
subsetFuncs := []struct {
name string
fun func(originalSet []int, newSet []int) bool
}{
{"map", isSubsetWithMap},
{"loop", isSubsetWithLoop},
}
for _, subsetFuncStruct := range subsetFuncs {
subset := getRandomArr(3, 100)
for k := 3.; k <= 10; k++ {
max := int(math.Pow(2, k))
arr := shufferArray(getRandomArr(max, max*2))
b.Run(fmt.Sprintf("%s/%d/%d", subsetFuncStruct.name, len(subset), len(arr)), func(b *testing.B) {
for i := 0; i < b.N; i++ {
subsetFuncStruct.fun(arr, subset)
}
})
}
}
}
實際執行的時候測試結果如下, 可以發現對於map和loop兩種方式基本上都符合線性的增長趨勢,但是明顯loop會更低一些。按道理來說map查詢應該更快一些纔對?
BenchmarkIsSubset/map/3/8-4 10000000 233 ns/op
BenchmarkIsSubset/map/3/16-4 2000000 737 ns/op
BenchmarkIsSubset/map/3/32-4 1000000 1467 ns/op
BenchmarkIsSubset/map/3/64-4 500000 2951 ns/op
BenchmarkIsSubset/map/3/128-4 200000 5636 ns/op
BenchmarkIsSubset/map/3/256-4 200000 11243 ns/op
BenchmarkIsSubset/map/3/512-4 100000 22432 ns/op
BenchmarkIsSubset/map/3/1024-4 30000 44178 ns/op
BenchmarkIsSubset/loop/3/8-4 50000000 35.5 ns/op
BenchmarkIsSubset/loop/3/16-4 30000000 61.3 ns/op
BenchmarkIsSubset/loop/3/32-4 20000000 118 ns/op
BenchmarkIsSubset/loop/3/64-4 10000000 195 ns/op
BenchmarkIsSubset/loop/3/128-4 5000000 283 ns/op
BenchmarkIsSubset/loop/3/256-4 2000000 616 ns/op
BenchmarkIsSubset/loop/3/512-4 2000000 723 ns/op
BenchmarkIsSubset/loop/3/1024-4 1000000 1615 ns/op
性能分析pporf
使用go tool工具中的pporf工具可以對於程序執行的時間和內存進行分析,因爲內存並不是我們主要考慮的問題,我們僅對於CPU時間進行分析。
再次執行測試並生成對應的測試性能文件:
go test -cpuprofile cpu.prof -bench . -benchtime 2s
go tool pprof cpu.prof
上面的操作會進入命令行模式,執行top10, top20分別返回花費最大的命令執行。儘管isSubsetWithLoop排名第一,但是由於實際上執行的次數也要高於isSubsetWithMap, 所以按照之前的測試看不出單一的函數執行的性能比較。
flat flat% sum% cum cum%
23.57s 42.07% 42.07% 23.57s 42.07% github.com/zhangmingkai4315/workspace/01-subset.isSubsetWithLoop
13.63s 24.33% 66.40% 13.63s 24.33% runtime.pthread_cond_signal
7.94s 14.17% 80.58% 11.85s 21.15% runtime.mapassign_fast64
2.60s 4.64% 85.22% 2.60s 4.64% runtime.aeshash64
1.46s 2.61% 87.83% 15.27s 27.26% github.com/zhangmingkai4315/workspace/01-subset.isSubsetWithMap
1.38s 2.46% 90.29% 40.22s 71.80% github.com/zhangmingkai4315/workspace/01-subset.BenchmarkIsSubset.func1
1.08s 1.93% 92.22% 1.08s 1.93% runtime.pthread_cond_wait
0.56s 1% 93.22% 0.56s 1% runtime.add
修改測試文件:
func BenchmarkIsSubset(b *testing.B) {
arr := shufferArray(getRandomArr(20, 100))
subset := getRandomArr(5, 100)
for i := 0; i < b.N; i++ {
isSubsetWithMap(arr, subset)
isSubsetWithLoop(arr, subset)
}
}
重新執行上面的流程:
flat flat% sum% cum cum%
1.08s 29.59% 29.59% 1.08s 29.59% runtime.pthread_cond_signal
0.94s 25.75% 55.34% 1.54s 42.19% runtime.mapassign_fast64
0.33s 9.04% 64.38% 0.33s 9.04% runtime.aeshash64
0.25s 6.85% 71.23% 2.25s 61.64% github.com/zhangmingkai4315/workspace/01-subset.isSubsetWithMap
0.19s 5.21% 76.44% 0.19s 5.21% github.com/zhangmingkai4315/workspace/01-subset.isSubsetWithLoop
0.12s 3.29% 79.73% 0.12s 3.29% runtime.memclrNoHeapPointers
0.07s 1.92% 81.64% 0.07s 1.92% runtime.add
0.07s 1.92% 83.56% 0.08s 2.19% runtime.overLoadFactor
0.06s 1.64% 85.21% 0.06s 1.64% runtime.pthread_cond_wait
0.05s 1.37% 86.58% 0.05s 1.37% runtime.bucketShift
0.04s 1.10% 87.67% 0.06s 1.64% runtime.mapaccess1_fast64
這樣通過相同次數的執行我們可以比較出不同函數的性能, 可以看到對於isSubsetWithMap函數執行高於Loop的執行(至少在測試子集爲5個,源集合爲20個的情況下)。上面的表格顯示可能生成map結構和分配數據的效率導致整體map的性能低於loop。我們可以在交互窗口中輸入list命令查看具體的每一個函數的執行情況:
(pprof) list github.com/zhangmingkai4315/workspace/01-subset.isSubsetWithMap
Total: 3.65s subset.isSubsetWithMap in /main.go
250ms 2.25s (flat, cum) 61.64% of Total
. . 31: }
. . 32: }
. . 33: return sizeOfNewSet == found
. . 34:}
. . 35:
10ms 10ms 36:func isSubsetWithMap(originalSet []int, newSet []int) bool {
. . 37: sizeOfOriginal := len(originalSet)
. . 38: sizeOfNewSet := len(newSet)
. . 39: if sizeOfNewSet == 0 || sizeOfOriginal < sizeOfNewSet {
. . 40: return false
. . 41: }
10ms 410ms 42: mapOfArray := make(map[int]bool, sizeOfOriginal)
110ms 110ms 43: for _, v := range originalSet {
120ms 1.66s 44: mapOfArray[v] = true
. . 45: }
. . 46: for _, i := range newSet {
. 60ms 47: if !mapOfArray[i] {
. . 48: return false
. . 49: }
. . 50: }
. . 51: return true
. . 52:}
對比 loop版本的代碼執行情況, 基本上沒有太複雜的邏輯和執行花費比較高的代碼。看來不能完全的依賴於O(n)來判斷一個函數是否高效率的依據。
190ms 190ms (flat, cum) 5.21% of Total
. . 15:
. . 16:func isSubsetWithLoop(originalSet []int, newSet []int) bool {
. . 17: sizeOfOriginal := len(originalSet)
. . 18: sizeOfNewSet := len(newSet)
. . 19: found := 0
10ms 10ms 20: if sizeOfNewSet == 0 || sizeOfOriginal < sizeOfNewSet {
. . 21: return false
. . 22: }
10ms 10ms 23: for _, i := range newSet {
. . 24: inner:
160ms 160ms 25: for _, j := range originalSet {
. . 26: // println(i, j)
10ms 10ms 27: if i == j {
. . 28: found++
. . 29: break inner
. . 30: }
. . 31: }
. . 32: }
同時在交互終端中執行web可以在瀏覽器打開自動生成的的svg文件,查看流程圖。
假如不需要臨時生成map,而是提前生成完成,每次都直接進行比對的話,其實map的版本還是相對較快的。對於代碼不確定性能的時候,還是要多謝一些測試來檢查性能。
另外原始代碼還實現了sort版本的兩個函數,放在github上,大家感興趣的話可以查看, 對於上述的代碼或者文章描述有任何疑問,歡迎大家交流討論。
具體的代碼可以參考查看Github Gist
另外我將該文章同時放在個人的Blog上,歡迎大家訪問:https://zhangmingkai.cn