通過減少動態調度來提高性能
與許多其他語言一樣,Swift允許類重寫其父類中聲明的方法和屬性。
這意味着程序必須在運行時確定要引用哪個方法或屬性,然後執行間接調用或間接訪問。
這種稱爲動態調度的技術以每次間接使用時恆定的運行時開銷爲代價提高了語言的表達能力。
在對性能敏感的代碼中,我們不希望花費這些開銷。
這篇博客文章展示了通過消除這種動態性來提高性能的三種方法:final,private和Whole Module Optimization。
看一下以下示例:
class ParticleModel {
var point = ( 0.0, 0.0 )
var velocity = 100.0
func updatePoint(newPoint: (Double, Double), newVelocity: Double) {
point = newPoint
velocity = newVelocity
}
func update(newP: (Double, Double), newV: Double) {
updatePoint(newPoint: newP, newVelocity: newV)
}
}
var p = ParticleModel()
for i in stride(from: 0.0, through: 360, by: 1.0) {
p.update(newP: (i * sin(i), i), newV:i*1000)
}
按照編寫的方式,編譯器將發出一個動態調度指令:
- 變量p調用update函數.
- 變量p的update函數又調用了updatePoint函數.
- 獲取p的point元組屬性.
- 獲取p的velocity屬性.
這可能不是你所希望看到的。但是動態調用是必需的,因爲ParticleModel的子類可能會重寫屬性point和velocity,或者使用新的實現覆蓋updatePoint或update函數。
在Swift中,動態調度調用是通過從函數表中查找函數然後執行間接調用來實現的(函數表派發),這比直接派發要慢。
此外,間接調用還會阻止許多編譯器優化,從而使間接調用的成本更高。
在對性能有要求的代碼中,有一些技巧可以用來限制不需要提高性能的動態行爲。
當不需要重寫這個函數時,請使用final
final關鍵字可以限制類,方法,或者屬性被子類重寫,這使編譯器可以安全地消除動態調度間接尋址,例如,在下面的point和velocity屬性將通過對象的存儲屬性中直接訪問,而updatePoint將被直接派發。另一方面,update仍將通過動態調度來調用,從而允許子類可以重寫update方法:
class ParticleModel {
final var point = ( x: 0.0, y: 0.0 )
final var velocity = 100.0
final func updatePoint(newPoint: (Double, Double), newVelocity: Double) {
point = newPoint
velocity = newVelocity
}
func update(newP: (Double, Double), newV: Double) {
updatePoint(newPoint: newP, newVelocity: newV)
}
}
通過將屬性附加到類本身,可以將整個類標記爲final。這禁止對類進行子類化,這意味着該類的所有功能和屬性也都是final。
final class ParticleModel {
var point = ( x: 0.0, y: 0.0 )
var velocity = 100.0
// ...
}
通過應用private關鍵字推斷在一個文件中引用的final聲明
將private關鍵字應用於聲明會限制該聲明對當前文件的可見性。這使編譯器可以找到所有可能覆蓋的聲明。缺少任何此類覆蓋聲明,可使編譯器自動推斷final關鍵字,並刪除對方法和屬性訪問的間接調用。
假如沒有什麼類在當前文件中重寫ParticleModel,編譯器可以將所有的動態分派調用與直接調用用私有聲明代替。
class ParticleModel {
private var point = ( x: 0.0, y: 0.0 )
private var velocity = 100.0
private func updatePoint(newPoint: (Double, Double), newVelocity: Double) {
point = newPoint
velocity = newVelocity
}
func update(newP: (Double, Double), newV: Double) {
updatePoint(newPoint: newP, newVelocity: newV)
}
}
與前面的示例一樣,子類不能直接訪問point和velocity,並不能直接調用updatePoint。因爲update不是私有方法,因此可以再次被子類重寫和調用。
就像final一樣,可以將private屬性應用於類聲明本身,從而導致該類是私有的,因此也可以讓該類的所有屬性和方法都變爲私有。
private class ParticleModel {
var point = ( x: 0.0, y: 0.0 )
var velocity = 100.0
// ...
}
使用整個模塊的優化來推斷最終的內部聲明
具有內部訪問權限的聲明(如果未聲明,則爲默認值)僅在聲明它們的模塊中可見。
由於Swift通常單獨編譯組成模塊的文件,所以編譯器無法確定是否在其他文件中重寫了內部聲明。
但是,如果啓用了整個模塊優化,則所有模塊將同時編譯在一起。
這允許編譯器一起對整個模塊進行推斷,並在沒有可見重寫的情況下對內部聲明進行final推斷。
讓我們回到原始代碼段,這一次向ParticleModel添加了一些額外的public關鍵字:
public class ParticleModel {
var point = ( x: 0.0, y: 0.0 )
var velocity = 100.0
func updatePoint(newPoint: (Double, Double), newVelocity: Double) {
point = newPoint
velocity = newVelocity
}
public func update(newP: (Double, Double), newV: Double) {
updatePoint(newPoint: newP, newVelocity: newV)
}
}
var p = ParticleModel()
for i in stride(from: 0.0, through: 360, by: 1.0) {
p.update(newP: (i * sin(i), i), newV:i*1000)
}
在使用整個模塊優化編譯此代碼段時,編譯器可以根據屬性point、velocity和方法updatePoint推斷是需要用final關鍵字修飾的。
相反,update不用final關鍵字修飾,因爲update具有公共訪問權限。