把並行化的思想用在編解碼驗證算法優化上的實踐

編碼驗證算法是一種驗證數據序列編碼格式的算法,比較典型的有UTF-8編碼驗證算法。UTF-8驗證算法用於檢查數據序列是否符合UTF-8編碼規範,好比說對經常使用UTF-8編碼的郵件、網頁及應用數據作編碼驗證時,就可使用UTF-8驗證算法。 本文以Go語言開源社區的UTF-8驗證算法優化案例爲例,講解通用的算法分析、優化方法。git

摘自OptimizeLab: https://github.com/OptimizeLab/docs github

做者:zaneChou1


1. Go語言的UTF-8編碼驗證算法

Go語言實現了UTF-8編碼驗證算法用於檢查UTF-8編碼數據,主要基於UTF-8的可變長編碼特色設計了驗證算法,UTF-8編碼使用1到4個字節爲每一個字符編碼,ASCII編碼屬於UTF-8編碼長度爲1個字節的狀況。 UTF-8驗證算法針對這個特色,先取一個字符判斷是否屬於ASCII編碼,再檢查是否屬於其餘類型的UTF-8編碼。代碼以下:golang

// 優化前的UTF-8驗證算法,先取一個字節檢查是否爲ASCII,再驗證字符是否屬於其餘類型的UTF-8編碼
func Valid(p []byte) bool {
    n := len(p)
    for i := 0; i < n; {
        // 驗證byte字符是否屬於ASCII編碼,當ASCII編碼數量不少時,算法運算效率會很慢。
        pi := p[i]
        if pi < RuneSelf {
            i++
            continue
        }

        // 驗證byte字符是否屬於其餘類型的UTF8編碼
        x := first[pi]
        if x == xx {
            return false // Illegal starter byte.
        }
        size := int(x & 7)
        if i+size > n {
            return false // Short or invalid.
        }
        accept := acceptRanges[x>>4]
        if c := p[i+1]; c < accept.lo || accept.hi < c {
            return false
        } else if size == 2 {
        } else if c := p[i+2]; c < locb || hicb < c {
            return false
        } else if size == 3 {
        } else if c := p[i+3]; c < locb || hicb < c {
            return false
        }
        i += size
    }
    return true
}

2. 場景及問題分析

經過分析Go語言優化前的UTF-8驗證算法,發現它也用於ASCII字符的檢查。在長篇的英文電子郵件中,連續出現大量的ASCII編碼也是常見的,下圖這篇英文郵件包含1705個ASCII字符。算法

image

使用上述的UTF-8驗證算法對英文郵件內容進行編碼檢查時,須要進行1705次比較操做,以下圖:編程

image

因而可知,在須要驗證大量ASCII編碼數據的場景下,優化前的UTF-8編碼驗證算法採用單個字符比較的方式檢查編碼,直到循環檢查完整個數據,算法的運行耗時大,性能有待提高。函數

3. 優化方案及實現

針對UTF-8編碼驗證算法中處理ASCII編碼字符檢查次數多、運行耗時大的問題,能夠利用並行化編程思想,一次同時處理多個ASCII編碼字符的檢查,減小比較的次數,加快驗證速度,提高算法性能。 Go語言的UTF-8驗證算法應用了基於並行化編程思想的算法優化方案,一次同時檢查8個ASCII編碼,大大提高了算法的運行性能。 算法優化先後的代碼對好比下:性能

image

優化後的代碼分析以下:學習

// 優化後的代碼,基於並行化編程思想,一次檢查8個byte是否爲ASCII字符
func Valid(p []byte) bool {
    // 每一個輪次同時檢查8個byte是否爲ASCII字符
    for len(p) >= 8 {
        // 使用兩個 uint32 變量 first32 和 second32,分別存儲了4個byte數據
        first32 := uint32(p[0]) | uint32(p[1])<<8 | uint32(p[2])<<16 | uint32(p[3])<<24
        second32 := uint32(p[4]) | uint32(p[5])<<8 | uint32(p[6])<<16 | uint32(p[7])<<24
        // 任一個ASCII字符與0x80的與結果都是0,經過(first32|second32)與0x80808080的與操做實現了8個byte的ASCII字符檢驗。
        if (first32|second32)&0x80808080 != 0 {
            break
        }
        p = p[8:]
    }

    // 每次驗證一個byte是否爲UTF-8編碼
    ......
}

爲了進一步分析並行化的優化技術,使用反彙編的方法獲得了算法優化先後的彙編代碼。優化前的彙編代碼以下:測試

image

優化後的彙編代碼以下:優化

image

分析發現,優化前的代碼,使用1個unit8變量存儲數據進行編碼驗證,對應到彙編代碼使用MOVBU指令取一個B(1個byte)的數據到寄存器R4,一次驗證1個byte數據的編碼。 優化後的代碼,使用2個unit32變量存儲數據進行編碼驗證,對應到彙編代碼使用MOVWU指令分別取一個W(4個byte)的數據到寄存器R3和R4,一次驗證8個byte數據的編碼。 該優化方法經過在Go語言代碼中使用存儲數據更多的unit32變量類型,增長了彙編指令中寄存器存儲的數據量,在寄存器作比較操做時,實現了更多編碼數據的並行化驗證

4. 優化結果

使用Go benchmark測試優化先後的算法性能,再用Go benchstat對比優化先後的性能測試結果,整理到以下表格:

測試項 測試用例 優化前每操做耗時 time/op 優化後每操做耗時 time/op 耗時對比
BenchmarkValidTenASCIIChars-8 長度爲10的byte切片 15.8 ns/op 8.00 ns/op -49.37%
BenchmarkValidStringTenASCIIChars-8 長度爲10的字符串 12.8 ns/op 8.04 ns/op -37.19%

[注]-8表示函數運行時的GOMAXPROCS值,ns/op表示函數每次執行的平均納秒耗時。

性能測試結果顯示,UTF-8編碼驗證算法優化後,驗證ASCII編碼的平均耗時減少,性能提高明顯最高達49%。

5. 總結

Go語言的UTF-8驗證算法優化案例,從一個具體的場景出發分析算法存在的性能問題,給出了基於並行化編程思想的優化方案,並最終驗證了優化結果,是一個值得學習借鑑的算法優化實踐。 案例中的並行化編程思想不只能夠用於優化數據的編碼驗證,更是能用於優化數據的編碼、解碼、壓縮和存儲等多種場景。

若是對開源或優化技術感興趣,歡迎下方留言或者經過https://github.com/OptimizeLab/docs/issues聯繫咱們。

相關文章
相關標籤/搜索