當最佳化軟體時字串比較可能和你想的有些不同。特別是包括拆分跨 goroutines 的迴圈, 找到一個更快的雜湊演算法,或者一些聽起來更科學的方式。當我們做出這樣的修改時,會有一種成就感。然而, 字串比較通常是資訊傳遞中(in a pipeline)的最大瓶頸。下面的程式碼片段經常被使用,但它是最糟糕的解決方案 (參見下面的 benchmarks),並導致了實際問題。```gostrings.ToLower(name) == strings.ToLower(othername)```這是一種很直接的寫法。把字串轉換成小寫,然後在比較。要理解為什麼這是一個差的解決方案,你需要知道字串是如何表示的,以及 `ToLower` 是如何工作的。但是首先,讓我們討論一下字串比較中主要的使用情境,當使用 `==` 操作符時,我們得到最快和最佳化的解決方案。通常 APIs 或類似的軟體通常都會考慮這些使用情境。我們使用 `ToLower` 稱之為 eature-complete。[^注1]。[^注1] This is when we drop in ToLower and call it feature-complete.在 Go 中,字串是一系列*不可變*的 runes。Rune 是 Go 的一個術語,代表一個碼點(Code Point)。你可以在 [Go blog](https://blog.golang.org/strings) 擷取更多關於 Strings, bytes, runes 和 characters 的資訊。 `ToLower` 是一個標準庫函數迴圈處理字串中的每個 rune 轉換成小寫,然後返回新的字串。所以上面的代碼在比較之前遍曆了整個字串。這就和字串的長度十分相關了。下面的虛擬碼大概的展示了上面程式碼片段的複雜度。注意:因為字串是不可變的,`strings.ToLower` 為兩個字串分配了新的記憶體空間。這增加了時間複雜度,但是現在這不是我們的關注點。為了簡化示範,下面的虛擬碼認為字串是可變的。```go// Pseudo codefunc CompareInsensitive(a, b string) bool { // loop over string a and convert every rune to lowercase for i := 0; i < len(a); i++ { a[i] = unicode.ToLower(a[i]) } // loop over string b and convert every rune to lowercase for i := 0; i < len(b); i++ { b[i] = unicode.ToLower(b[i]) } // loop over both a and b and return false if there is a mismatch for i := 0; i < len(a); i++ { if a[i] != b[i] { return false } } return true}```時間複雜度是 O(n) `n` 是 `len(a) + len(b) + len(a)` 請看下面的例子:```goCompareInsensitive("fizzbuzz", "buzzfizz")```意味著我們需要迴圈 24 次來確定兩個完全不相同的字串不匹配。這是非常低效的,我們可以通過比較 `unicode.ToLower(a[0])` 和 `unicode.ToLower(b[0])` (虛擬碼)來區分這些字串。因此,需要把這種情況考慮在內。最佳化一下,我們可以去掉 `CompareInsensitive` 前面的兩個迴圈,比較相應位置的每個字元。如果 runes 不相等,我們轉換成小寫再比較。如果仍然不相等,我們結束迴圈,認為兩個字串不相等。如果他們相等,就繼續比較下一個 rune 直到結束或者發現不相等的地方。現在重寫一下代碼```go// Pseudo codefunc CompareInsensitive(a, b string) bool { // a quick optimization. If the two strings have a different // length then they certainly are not the same if len(a) != len(b) { return false } for i := 0; i < len(a); i++ { // if the characters already match then we don't need to // alter their case. We can continue to the next rune if a[i] == b[i] { continue } if unicode.ToLower(a[i]) != unicode.ToLower(b[i]) { // the lowercase characters do not match so these // are considered a mismatch, break and return false return false } } // The string length has been traversed without a mismatch // therefore the two match return true}```新函數效率更高。上限是一個字串的長度而不是兩個字串的長度和。怎麼看我們上面的比較?迴圈比較最多隻有 8 次。甚至,如果第一個字元不同,那就只迴圈一次。我們最佳化使得比較操作減少了大約 20 倍!幸運的是在 `strings` 包裡面有這樣的函數。叫做 `strings.EqualFold`。## 效能測試```// When both strings are equalBenchmarkEqualFoldBothEqual-8 20000000 124 ns/opBenchmarkToLowerBothEqual-8 10000000 339 ns/op// When both strings are equal until the last runeBenchmarkEqualFoldLastRuneNotEqual-8 20000000 129 ns/opBenchmarkToLowerLastRuneNotEqual-8 10000000 346 ns/op// When both strings are distinctBenchmarkEqualFoldFirstRuneNotEqual-8 300000000 11.2 ns/opBenchmarkToLowerFirstRuneNotEqual-8 10000000 333 ns/op// When both strings have a different case at rune 0BenchmarkEqualFoldFirstRuneDifferentCase-8 20000000 125 ns/opBenchmarkToLowerFirstRuneDifferentCase-8 10000000 433 ns/op// When both strings have a different case in the middleBenchmarkEqualFoldMiddleRuneDifferentCase-8 20000000 123 ns/opBenchmarkToLowerMiddleRuneDifferentCase-8 10000000 428 ns/op```當字串的第一個字元不同時的差異很驚人 (30x)。因為不需要迴圈比較兩個字串,而是只迴圈一次就直接返回 false。在每個情況中 `EqualFold` 都要比開始的比較方式好出幾個量級。## 這很重要嗎?你可能認為 400 納秒並不重要。大多數情況下你可能是對的。不管怎樣,一些微小的最佳化處理像其他的處理一樣簡單。在這個例子中,要比原來的處理方式更簡單。合格的工程師在日常工作中就會使用這些微小的最佳化處理方式。他們不會等到變成問題的時候才去最佳化軟體,他們從開始的時候就寫出最佳化的軟體。就算是最優秀的工程師也不可能從 0 開始寫出最佳化的軟體。不可能憑空想象出每個極端的案例然後最佳化它。並且,在我們提供給使用者軟體的時候,我們也無法預知使用者的行為。不管怎樣,在日常工作中加入這些簡單的處理方式有益於延長軟體的生命週期,預防將來可能不必要的瓶頸。就算那個瓶頸沒什麼影響,你也不會浪費你的付出。
via: https://www.digitalocean.com/community/questions/how-to-efficiently-compare-strings-in-go
作者:blockloop 譯者:tyler2018 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
618 次點擊