第七章——字串(字串效能)

bestswifter發表於2017-12-27

本文系閱讀閱讀原章節後總結概括得出。由於需要我進行一定的概括提煉,如有不當之處歡迎讀者斧正。如果你對內容有任何疑問,歡迎共同交流討論。

毫無疑問,把多個UTF-16編碼的程式碼點合併成一個字形叢集比直接處理這些程式碼點更耗時。本節,我們用之前實現的正規表示式匹配器處理不同的字串檢視,具體的展示在不同檢視下效能的差異。

我們需要對此前的Regex做一些改進,因為它當時僅使用了CharacterView檢視。為了測試不同檢視下的效能差異,我們需要讓它支援另外三個檢視UTF8ViewUTF16ViewUnicodeScalarView

根據面向協議程式設計的思想,你可能會實現一個範型的Regex結構體,並且提供一個佔位型別(這通常是一個協議)。但問題在於沒有一個公共的協議同時被這四種檢視實現。另一個問題是,在匹配過程中,我們需要把字元常量"*""^"等,和正規表示式進行比較。在不同的檢視下,這些字串常量也需要有不同的表達形式。

最後一個問題是,我們希望正規表示式的初始化函式的引數依然是String型別,那麼它怎麼知道在內部應該選擇哪個檢視呢?

一種技術是把所有會變化的邏輯封裝到一個單獨的型別中,並且把它作為匹配器的一個引數,首先我們定義一個通用的協議:

protocol StringViewSelector {
typealias ViewType: CollectionType

static var carit:       ViewType.Generator.Element { get }
static var asterisk:    ViewType.Generator.Element { get }
static var period:      ViewType.Generator.Element { get }
static var dollar:      ViewType.Generator.Element { get }

static func viewFrom(s: String) -> ViewType
}
複製程式碼

這個協議中包含了一個型別別名,表示我們要用的檢視型別,以及需要用到的四個常量,還有一個viewFrom函式用於提取引數字串的對應檢視。這麼說可能有些抽象,我們看兩個實現:

struct CharacterViewSelector: StringViewSelector {
static var carit:       Character { return "^" }
static var asterisk:    Character { return "*" }
static var period:      Character { return "." }
static var dollar:      Character { return "$" }

static func viewFrom(s: String) -> String.CharacterView { return s.characters }
}

struct UTF8ViewSelector: StringViewSelector {
static var carit:       UInt8 { return UInt8(ascii: "^") }
static var asterisk:    UInt8 { return UInt8(ascii: "*") }
static var period:      UInt8 { return UInt8(ascii: ".") }
static var dollar:      UInt8 { return UInt8(ascii: "$") }

static func viewFrom(s: String) -> String.UTF8View { return s.utf8 }
}
複製程式碼

剩下的UTF16ViewSelectorUnicodeScalarViewSelector結構體的實現就不寫了,估計你也能猜到。

這就是我們所說的虛型別(Phantom Type),這種型別實際上不會儲存任何資料,因此呼叫sizeof(CharacterViewSelector)得到的結果是0。我們使用這樣的型別,讓Regex結構體的邏輯引數化,也就是說隨著StringViewSelector型別的不同,Regex結構體的邏輯也不同:

struct Regex<V: StringViewSelector
where V.ViewType.Generator.Element: Equatable,V.ViewType.SubSequence == V.ViewType> {
let regexp: String

init(_ regexp: String) {
self.regexp = regexp
}

func match(text: String) -> Bool {
let text = V.viewFrom(text)
let regexp = V.viewFrom(self.regexp)
//如果regex以^開頭,只能從引數的第一個字元開始匹配
if regexp.first == V.carit {
return Regex.matchHere(regexp.dropFirst(), text)
}

//依次從不同的位置開始,嘗試匹配每一個子串
for var idx = text.startIndex; ; ++idx {
if Regex.matchHere(regexp, text[idx..<text.endIndex]) {
return true
}
if idx == text.endIndex { break }
}

return false
}

static func matchHere(regexp: V.ViewType, _ text: V.ViewType) -> Bool {
// 以下省略具體的匹配邏輯
return true
}
}
複製程式碼

重寫了Regex結構體後,效能測試的函式就很容易實現了:

func benchmark<V: StringViewSelector
where V.ViewType.Generator.Element: Equatable,
V.ViewType.SubSequence == V.ViewType>
(_: V.Type) {
let r = Regex<V>("h..a*")
var count = 0

let startTime = CFAbsoluteTimeGetCurrent()
while let line = readLine() {
if r.match(line) { count = count &+ 1 }
}
let totalTime = CFAbsoluteTimeGetCurrent() - startTime
print("\(V.self): \(totalTime)")
}

func ~=<T: Equatable>(lhs: T, rhs: T?) -> Bool {
return lhs == rhs
}

switch Process.arguments.last {
case "ch": benchmark(CharacterViewSelector.self)
case "8": benchmark(UTF8ViewSelector.self)
case "16": benchmark(UTF16ViewSelector.self)
case "sc": benchmark(UnicodeScalarViewSelector.self)
default: print("Unrecognized view type")
}
複製程式碼

如果選擇一段很長的文字(12.8萬行,一百萬個單詞),我們得到如下的輸出結果:

UTF160.31 seconds
UnicodeScalar0.77 seconds
UTF81.7 seconds
Characters10 seconds
複製程式碼

可見,UTF16編碼下的效能最高,Characters檢視的效能遠低於另外三個。

相關文章