Nim 概念 Concept 對性能的影響

Nim 概念 Concept 對性能的影響

繼上一篇文章《C# 泛型編譯特性對性能的影響》後,我又研究了 Nim 語言相關的設計,由於 Nim 語言與 C# 語言有些差異,比如Nim 沒有接口,也沒有直接的 class 關鍵字,所以某些實現是變通的辦法。

概念 Concept

在Nim中沒有 Interface 的概念,雖然有多次提案,但似乎語言設計者一直拒絕這一設計,他們提供了 Concept 這一方案。他與C#的interface的相似點包括:

  • conceptinterface都用於定義抽象類型和行爲規範,允許在沒有提供具體實現的情況下定義方法簽名。
  • 兩者都支持多態,允許不同的類型實現相同的接口或概念,並以相同的方式被操作或使用。

但也有不同之處,比如:

  • 在Nim的concept中,方法聲明不需要關鍵字(如fn),而是直接指定方法的簽名。在C#中,interface中的方法聲明需要使用method或property等關鍵字。
  • concept在Nim中比interface更加靈活。concept可以用於限制多個類型的行爲,但不一定需要顯式聲明實現。它允許在函數中使用未顯式聲明爲實現concept的類型。interface在C#中更加嚴格,要求類型明確地聲明並實現接口中的所有成員。
  • 在C#中,interface可以在字段、方法參數、方法返回值等多個地方使用,允許動態地訪問實現。在Nim中,concept主要用於參數約束,只允許在函數或過程中使用,無法在字段或返回值中聲明

下面是這個案例中聲明 Concept 的例子:

type
    IValueGetter = concept s
        s.getValue(int32) is int64

有了 concept,我們就可以在泛型中約束 泛型參數的行爲,例如下面的泛型類型 MyTestClass[T] ,其類型聲明中沒有約束 T,但在相關的方法中,允許約束。

type
    MyTestClass[T] = object
        valueGetter: T

proc run[T: IValueGetter](this: MyTestClass[T]): int64 =
    var r = 0i64
    let n = high(int32) - rand(100).int32
    for i in 0 ..< n:
      r += this.valueGetter.getValue(i)
    return r

Nim 中 Struct 和 Class

在Nim語言中, struct 是使用 object 關鍵字,同樣的,如果在object 的前面加上 ref 關鍵字,就變成了 struct 引用,自然就相當於 class 了,下面是示例代碼:

type
    StructValueGetter = object
    StructValueGetter2 = object
        someField: int

    ValueGetter = ref object of RootObj
    ClassValueGetter1 = ref object of ValueGetter
    ClassValueGetter2 = ref object of ValueGetter

在這個例子中,我們看見如何聲明 Struct,以及聲明字段, 同時也使用 ref object 聲明瞭 class ,他使用 of 關鍵字表示繼承關係。

你也許注意到,RootObj ,他是nim語言提供的默認基類,你可以理解爲c#和java 的object 基類,但nim語言具有很強的靈活性,可以不繼承任何基類,或使用其他的基類。

Nim中定義方法或動態方法

一般情況下,Nim建議使用 proc 和 func 作爲方法前的關鍵字,因爲這樣的話方法就是靜態編譯的,具有最佳的性能,func 其實是 proc 的特例,即表示無副作用的 proc,比如你僅僅是讀取對象的數據,不會破壞結構,這就是無副作用。

# 實現 IValueGetter 的 concept
func getValue(this: StructValueGetter, index: int32): int64 =
    result = index.int64 + 3i64

func getValue(this: StructValueGetter2, index: int32): int64 =
    result = index.int64 + 5i64

func getValue(this: ClassValueGetter1, index: int32): int64 =
    result = index.int64 + 5i64

func getValue(this: ClassValueGetter2, index: int32): int64 =
    result = index.int64 + 7i64

爲實現諸如 c# 的虛方法功能(即動態調用),nim 設計了 method 關鍵字。

method getValueCore(this: ValueGetter, index: int32): int64 {.base.} =
    quit "to overrdie"

method getValueCore(this: ClassValueGetter1, index: int32): int64 =
    return getValue(this,index)

method getValueCore(this: ClassValueGetter2, index: int32): int64 =
    return getValue(this,index)

# 爲 ValueGetter 實現契約,相當於使用契約調用的是虛方法。
proc getValue(this: ValueGetter, index: int32): int64 =
    return getValueCore(this, index)

測試

下面我們將使用 stuct 和 class 兩種形式,確定 concept 和 泛型結合,對性能的影響。同時我們也觀察動態方法調用,即虛方法的調用對性能的影響。

import std/[times],std/random
type
    IValueGetter = concept s
        s.getValue(int32) is int64

    MyTestClass[T] = object
        valueGetter: T

proc run[T: IValueGetter](this: MyTestClass[T]): int64 =
    var r = 0i64
    let n = high(int32) - rand(100).int32
    for i in 0 ..< n:
      r += this.valueGetter.getValue(i)
    return r

type
    StructValueGetter = object
    StructValueGetter2 = object
        someField: int

    ValueGetter = ref object of RootObj
    ClassValueGetter1 = ref object of ValueGetter
    ClassValueGetter2 = ref object of ValueGetter

# 實現 IValueGetter 的 concept
func getValue(this: StructValueGetter, index: int32): int64 =
    result = index.int64 + 3i64

func getValue(this: StructValueGetter2, index: int32): int64 =
    result = index.int64 + 5i64

func getValue(this: ClassValueGetter1, index: int32): int64 =
    result = index.int64 + 5i64

func getValue(this: ClassValueGetter2, index: int32): int64 =
    result = index.int64 + 7i64

# 爲接口 ValueGetter 實現契約,相當於使用契約調用的是委託。
method getValueCore(this: ValueGetter, index: int32): int64 {.base.} =
    quit "to overrdie"

method getValueCore(this: ClassValueGetter1, index: int32): int64 =
    return getValue(this,index)

method getValueCore(this: ClassValueGetter2, index: int32): int64 =
    return getValue(this,index)

proc getValue(this: ValueGetter, index: int32): int64 =
    return getValueCore(this, index)

proc measureTime(caption: string, procToMeasure: proc(): int64) =
  var startTime = cpuTime()
  let r = procToMeasure()
  var endTime = cpuTime()
  echo caption, " time = ", endTime - startTime, " result = ", r

# 運行測試
proc main() =
    randomize()
    let t1 = MyTestClass[StructValueGetter](valueGetter : StructValueGetter())
    measureTime("StructValueGetter ", proc ():int64 = t1.run() )
    let t2 = MyTestClass[ClassValueGetter1](valueGetter : new(ClassValueGetter1))
    measureTime("ClassValueGetter1 ", proc ():int64 = t2.run() )
    let t3 = MyTestClass[ClassValueGetter2](valueGetter : new(ClassValueGetter2))
    measureTime("ClassValueGetter2 ", proc ():int64 = t3.run())
    let t4 = MyTestClass[ValueGetter](valueGetter : new(ClassValueGetter1))
    measureTime("IValueGetter-1    ", proc ():int64 = t4.run())

    let t5 = MyTestClass[ClassValueGetter1](valueGetter : new(ClassValueGetter1))
    measureTime("ClassValueGetter1 ", proc ():int64 = t5.run())
    let t6 = MyTestClass[StructValueGetter2](valueGetter : StructValueGetter2())
    measureTime("StructValueGetter2", proc ():int64 = t6.run())


when isMainModule:
  main()

編譯的命令行如下:

nim c -d:release -x --checks:off  -r ".\demo1.nim"

請自己編譯時將 demo1.nim 替換成自己的文件名,同時你也可能注意到 我關閉了檢查,我發現如果不關閉的話,性能會慢很多。在我的筆記本電腦的參考結果如下:

StructValueGetter  time = 0.429 result = 2305842997402533900
ClassValueGetter1  time = 0.421 result = 2305842825603845693
ClassValueGetter2  time = 0.421 result = 2305842971632730244
IValueGetter-1     time = 4.74 result = 2305842913650672596
ClassValueGetter1  time = 0.4210000000000003 result = 2305842870701000726
StructValueGetter2 time = 0.4190000000000005 result = 2305842920093123411

我們注意到:

  • 在nim語言實現中 , struct 和 class 在泛型和 concept 的環境下,性能幾乎沒有影響;
  • 與c#不同,nim仍然爲每種 class 編譯不同的程序,所以性能不會變化;
  • 儘量避免使用虛方法,性能的損失很大;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章