Nim 列舉型別 對效能的影響

2023-12-05 18:00:43

Nim 列舉型別 對效能的影響

繼上一篇文章《Nim 概念 Concept 對效能的影響》後,我在想,既然 method 虛方法造成效能的影響很大,那麼有沒有更快的方法實現,當然是有的,那就是列舉型別。

Enum Type

與很多的新設計的一樣,Nim語言也內建了列舉型別,比如下面的程式碼:

type
    ValueGetterKind = enum
        vkClass1,
        vkClass2
    ValueGetter2 = ref object
        sharedField : int32
        case kind:ValueGetterKind
        of vkClass1:someField1 : int32
        of vkClass2:someField2 : int32

正如你看到的,在Nim語言中,通過為型別增加一個列舉欄位(這裡是kind),就可以為不同的型別定義不同的欄位了,當然,你也可以定義共用的欄位(這裡例子的 sharedField)。

官方的例子中,演示了更復雜的情況,比如 多個型別公用相同的欄位,或者定義多個欄位。

# This is an example how an abstract syntax tree could be modelled in Nim
type
  NodeKind = enum  # the different node types
    nkInt,          # a leaf with an integer value
    nkFloat,        # a leaf with a float value
    nkString,       # a leaf with a string value
    nkAdd,          # an addition
    nkSub,          # a subtraction
    nkIf            # an if statement
  Node = ref object
    case kind: NodeKind  # the `kind` field is the discriminator
    of nkInt: intVal: int
    of nkFloat: floatVal: float
    of nkString: strVal: string
    of nkAdd, nkSub:
      leftOp, rightOp: Node
    of nkIf:
      condition, thenPart, elsePart: Node

var n = Node(kind: nkFloat, floatVal: 1.0)
# the following statement raises an `FieldDefect` exception, because
# n.kind's value does not fit:
n.strVal = ""

效能對比

接著我們之前的例子,我們看看在泛型的情況下,呼叫列舉型別對效能究竟影響有多少。

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
    ValueGetterKind = enum
        vkClass1,
        vkClass2,
        #vkClass3,
    ValueGetter2 = ref object
        sharedField : int32
        case kind:ValueGetterKind
        of vkClass1:someField1 : int32
        of vkClass2:someField2 : int32
        #of vkClass3:someField3 : int32

func getValue(this: ValueGetter2, index: int32): int64 =
    #return index.int64 + 7i64
    case this.kind:
    of vkClass1: 
        result = index.int64 + 9i64
    of vkClass2:
        result = index.int64 + 11i64
    #of vkClass3:
    #    result = index.int64 + 17i64

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 t7 = MyTestClass[ValueGetter2](valueGetter : ValueGetter2(kind:vkClass1, someField1:9))
    measureTime("ValueGetter2:1    ", proc ():int64 = t7.run())
    let t8 = MyTestClass[ValueGetter2](valueGetter : ValueGetter2(kind:vkClass2, someField2:11))
    measureTime("ValueGetter2:2    ", proc ():int64 = t8.run())
    #let t9 = MyTestClass[ValueGetter2](valueGetter : ValueGetter2(kind:vkClass3, someField3:11))
    #measureTime("ValueGetter2:3    ", proc ():int64 = t9.run())   

when isMainModule:
  main()

在我的機器中,測試結果如下(均使用release編譯,下同):

ValueGetter2:1     time = 0.725 result = 2305842980222664759
ValueGetter2:2     time = 1.967 result = 2305842982370148375

我們看到,如果是判斷分支的第一個,效能與本地呼叫稍差(0.7對比0.4),但與虛方法的5秒已經好太多。

細心的你,可能已經注意到我給的程式碼註釋掉了第三種型別vkClass3,那麼是不是作為分支的第三種情況,會更慢?讓我們取消幾個分支的註釋,看看結果:

ValueGetter2:1     time = 1.300 result = 2305842894323320179
ValueGetter2:2     time = 1.345 result = 2305842842783714180
ValueGetter2:3     time = 1.295 result = 2305842993107566484

意不意外?第一個分支竟然變慢了,而後兩個分支變快了,最後一個分支還稍稍快一點點。是否說明nim語言編譯為2個列舉型別和3個列舉型別,使用了不同的編譯程式碼? 讓我們從nim生成的c語言原始碼驗證這件事。

// 這是2個列舉的程式碼
N_LIB_PRIVATE N_NIMCALL(NI64, getValue__demo50_u28)(tyObject_ValueGetter2colonObjectType___liWfIs6eMPnbT0BuR4LSgg* this_p0, NI32 index_p1) {
	NI64 result;
	result = (NI64)0;
	switch ((*this_p0).kind) {
	case ((tyEnum_ValueGetterKind__tsIJjFnfs8zo9caQCLYJn1A)0):
	{
		result = (NI64)(((NI64) (index_p1)) + IL64(9));
	}
	break;
	case ((tyEnum_ValueGetterKind__tsIJjFnfs8zo9caQCLYJn1A)1):
	{
		result = (NI64)(((NI64) (index_p1)) + IL64(11));
	}
	break;
	}
	return result;
}
// 這是三個列舉的程式碼
N_LIB_PRIVATE N_NIMCALL(NI64, getValue__demo50_u30)(tyObject_ValueGetter2colonObjectType___liWfIs6eMPnbT0BuR4LSgg* this_p0, NI32 index_p1) {
	NI64 result;
	result = (NI64)0;
	switch ((*this_p0).kind) {
	case ((tyEnum_ValueGetterKind__tsIJjFnfs8zo9caQCLYJn1A)0):
	{
		result = (NI64)(((NI64) (index_p1)) + IL64(9));
	}
	break;
	case ((tyEnum_ValueGetterKind__tsIJjFnfs8zo9caQCLYJn1A)1):
	{
		result = (NI64)(((NI64) (index_p1)) + IL64(11));
	}
	break;
	case ((tyEnum_ValueGetterKind__tsIJjFnfs8zo9caQCLYJn1A)2):
	{
		result = (NI64)(((NI64) (index_p1)) + IL64(17));
	}
	break;
	}
	return result;
}

可以看出,程式碼完全是一樣的,可能是編譯器做了手腳。當然,我也測試了更多列舉的可能,比如7種,我發現所有分支時間都變長了(達到2秒),但總體是比虛方法好的。

共用的欄位

在 getValue 方法中,如果你沒有對型別進行判斷,那麼實際測試的成績和靜態方法呼叫是一樣的,這可以被利用到我們物件導向程式設計中,經常用到的基礎類別統一實現的場景。

總結

在 nim 程式設計中,簡單的派生關係可以使用 列舉型別來提高效能。