Nim 语言的动态分发机制

Nim 是一款 Python 风格的静态类型语言。但事实上,除了缩进语法外,Nim 和 Python 并没有太多相似之处。相对于 Python,Nim 更多地吸收了 Ada 和 Lisp 等语言的特性。

Nim 编译器通过生成 C 或 Obj-C 中间代码的方式来进行本地编译,所以需要依赖一套外部编译器。Nim 也可以生成 Javascript 代码。此外,还有以脚本方式运行的 NimScript

OOP 的一个特性就是支持多态,而动态分发(Dynamic Dispatch)就是实现多态的基础。在讲解 Nim 的动态分发实现前,先讲一下其他语言是如何实现的。

C++ 虚函数

为了实现动态分发,C++ 必须将方法声明为 virtual。编译器会为每个类创建一个虚函数表(vtable),并将其地址保存在每个创建的对象中。当调用虚函数时,需要通过虚函数表的偏移量来定位真正的方法入口。

Go 接口

Go 语言通过接口实现动态分发。Go 的接口是隐式实现的,类型只需要实现接口的所有方法,而不需要像 Java 那样显示声明。编译器仅在类型转换、参数传递、函数返回等操作时,才进行类型检查。

Go 的接口(runtime.iface)类型包含两个指针,一个是指向数据结构的 data 指针,另一个指向 itab 结构,该结构保存了接口的方法入口表。这样做的优点是编译器实现更加简单,语法上也更灵活,缺点是接口类型占用了更多的空间。

Python 鸭子类型

Python 等动态类型语言具有鸭子类型(Duck typing)的特性,对象方法都是动态分发,而无需显式声明。Python 将方法保存在一个 dict 散列表中,在运行时通过方法名称动态查找。如果在当前类中没有找到该方法,则通过 MRO 依次向父类中查找。

显然,这种动态分发方式的开销是很高的。某些动态类型语言,例如 smalltalk 和 Javascript(V8) 会使用一种叫内联缓存(inline cache)的优化方式来提升运行效率。

Nim 动态分发

Nim 的数据和方法是分开定义的,这种风格有点类似 Julia 和 Common Lisp。用 proc 关键字定义的方法使用静态分发,如果要使用动态分发,则需要使用 method 关键字。

NIMtype
  Foo = ref object of RootObj
  Bar = ref object of Foo

method dynamic_dispath(foo: Foo) {.base.} =
  echo "foo"

method dynamic_dispath(bar: Bar) =
  echo "bar"

proc static_dispath(foo: Foo) =
  echo "foo"

proc static_dispath(bar: Bar) =
  echo "bar"


var foo: Foo = new(Bar)
foo.static_dispath() # output: foo
foo.dynamic_dispath() # output: bar

同 Julia 和 Common Lisp 一样,Nim 也支持多重分发(Multiple dispatch)。

NIMtype
  Thing = ref object of RootObj
  Unit = ref object of Thing
    x: int

method collide(a, b: Thing) {.inline.} =
  quit "to override!"

method collide(a: Thing, b: Unit) {.inline.} =
  echo "1"

method collide(a: Unit, b: Thing) {.inline.} =
  echo "2"

var a, b: Unit
new a
new b
collide(a, b) # output: 2

注: 自 Nim 0.20 起,编译时需要使用参数 --multimethods:on 来启用多重分发支持。

此外,Nim 支持一种叫统一函数调用语法(Uniform Function Call Syntax)的风格:

NIMproc print(s: string) = echo s
proc print(s1: string, s2: string) = echo s1, s2

print("Hello World!")
print "Hello World!"
"Hello World!".print()
"Hello World!".print
"Hello".print (" World!")
"Hello".print " World!"

Nim 的动态分发并没有通过虚函数表的方式来实现,而是在动态方法调用处内联展开一系列的 if 判断,根据调用对象的类型,最终选择不同的方法函数。

NIMtype
  Base = ref object of RootObj
  Derived = ref object of Base

method dynamic_dispatch(foo: Base) = echo "Base"
method dynamic_dispatch(foo: Derived) = echo "Derived"

var foo: Base = new(Derived)

foo.dynamic_dispatch

生成的 C 代码如下:

C/* section: NIM_merge_DATA */
STRING_LITERAL(TM__TK8P9bk048kbmFtnhzKkOjA_3, "Base", 4);
static NIM_CONST tyArray__nHXaesL0DJZHyVS07ARPRA TM__TK8P9bk048kbmFtnhzKkOjA_2 = {((NimStringDesc*) &TM__TK8P9bk048kbmFtnhzKkOjA_3)}
;
STRING_LITERAL(TM__TK8P9bk048kbmFtnhzKkOjA_5, "Derived", 7);
static NIM_CONST tyArray__nHXaesL0DJZHyVS07ARPRA TM__TK8P9bk048kbmFtnhzKkOjA_4 = {((NimStringDesc*) &TM__TK8P9bk048kbmFtnhzKkOjA_5)}
;

// ... ...

N_LIB_PRIVATE N_NIMCALL(void, dynamic_dispatch__c5r4zNoABA7a1469bvDyeFA)(tyObject_BasecolonObjectType___9c7UwwsDx9cls9baHKBs8Py0g* foo) {
	nimfr_("dynamic_dispatch", "test.nim");
	nimln_(5, "test.nim");
	echoBinSafe(TM__TK8P9bk048kbmFtnhzKkOjA_2, 1); // method dynamic_dispatch(foo: Base) = echo "Base"
	popFrame();
}
N_LIB_PRIVATE N_NIMCALL(void, dynamic_dispatch__NGJl71YhyrlEEKIVgc1qbA)(tyObject_DerivedcolonObjectType___KIiBF5jw56vc7JjtgVaRMg* foo) {
	nimfr_("dynamic_dispatch", "test.nim");
	nimln_(6, "test.nim");
	echoBinSafe(TM__TK8P9bk048kbmFtnhzKkOjA_4, 1); // method dynamic_dispatch(foo: Derived) = echo "Derived"
	popFrame();
}

// ... ...

N_LIB_PRIVATE N_NIMCALL(void, dynamic_dispatch__5KArHhtsF2JAL9bQSgyiLMA)(tyObject_BasecolonObjectType___9c7UwwsDx9cls9baHKBs8Py0g* foo) {
	nimfr_("dynamic_dispatch", "test.nim");
	nimln_(84, "\\.choosenim\\toolchains\\nim-1.4.4\\lib\\system\\ch"
"cks.nim");
	chckNilDisp(foo);
	nimln_(5, "test.nim");
	{
		if (!((foo) && ((*foo).Sup.m_type == (&NTI__KIiBF5jw56vc7JjtgVaRMg_)))) goto LA3_;
		if (foo && !isObj((*foo).Sup.m_type, (&NTI__KIiBF5jw56vc7JjtgVaRMg_))){ raiseObjectConversionError(); }
		dynamic_dispatch__NGJl71YhyrlEEKIVgc1qbA(((tyObject_DerivedcolonObjectType___KIiBF5jw56vc7JjtgVaRMg*) (foo)));
	}
	goto LA1_;
	LA3_: ;
	{
		if (!((foo) && (isObjWithCache((*foo).Sup.m_type, (&NTI__9c7UwwsDx9cls9baHKBs8Py0g_), Nim_OfCheck_CACHE8)))) goto LA6_;
		dynamic_dispatch__c5r4zNoABA7a1469bvDyeFA(foo);
	}
	goto LA1_;
	LA6_: ;
	LA1_: ;
	popFrame();
}

// ... ...

N_LIB_PRIVATE N_NIMCALL(void, NimMainModule)(void) {
{
	TFrame FR_; FR_.len = 0;

	nimRegisterGlobalMarker(TM__TK8P9bk048kbmFtnhzKkOjA_6);

}/* preInitProc end */
{
	tyObject_DerivedcolonObjectType___KIiBF5jw56vc7JjtgVaRMg* T1_;
	nimfr_("test", "test.nim");
	nimln_(8, "test.nim");
	T1_ = (tyObject_DerivedcolonObjectType___KIiBF5jw56vc7JjtgVaRMg*)0;
	T1_ = new__hYny4BGj3LhiDCg0b9az6qQ();
	asgnRef((void**) (&foo__arkQMChktByKQJ3cIC51Bg), &T1_->Sup);
	nimln_(10, "test.nim");
	dynamic_dispatch__5KArHhtsF2JAL9bQSgyiLMA(foo__arkQMChktByKQJ3cIC51Bg); // foo.dynamic_dispatch
	popFrame();
}
}

这种方式有个缺点:如果继承树很深,那么判断分支就会很长,调用的对象类型越是靠近根部,调用的开销越大。

NIMimport times

type
  Base = ref object of RootObj
  Derived1 = ref object of Base
  Derived2 = ref object of Derived1
  Derived3 = ref object of Derived2
  Derived4 = ref object of Derived3
  Derived5 = ref object of Derived4
  Derived6 = ref object of Derived5
  Derived7 = ref object of Derived6
  Derived8 = ref object of Derived7
  Derived9 = ref object of Derived8
  Derived10 = ref object of Derived9

proc static_dispatch(foo: Base, count: int): int = count - 1
method dynamic_dispatch(foo: Base, count: int): int {.base.} = count - 1
method dynamic_dispatch(foo: Derived1, count: int): int = count + 1
method dynamic_dispatch(foo: Derived2, count: int): int = count + 2
method dynamic_dispatch(foo: Derived3, count: int): int = count + 3
method dynamic_dispatch(foo: Derived4, count: int): int = count + 4
method dynamic_dispatch(foo: Derived5, count: int): int = count + 5
method dynamic_dispatch(foo: Derived6, count: int): int = count + 6
method dynamic_dispatch(foo: Derived7, count: int): int = count + 7
method dynamic_dispatch(foo: Derived8, count: int): int = count + 8
method dynamic_dispatch(foo: Derived9, count: int): int = count + 9
method dynamic_dispatch(foo: Derived10, count: int): int = count + 10

var foo: Base = new(Base)
var start = cpuTime()
for i in 0..99999999:
    discard foo.dynamic_dispatch(i)
    
echo "Time taken: ", cpuTime() - start # Time taken: 1.342

foo = new(Derived10)
start = cpuTime()
for i in 0..99999999:
    discard foo.dynamic_dispatch(i)
    
echo "Time taken: ", cpuTime() - start # Time taken: 0.672

start = cpuTime()
for i in 0..99999999:
    discard foo.static_dispatch(i)
    
echo "Time taken: ", cpuTime() - start # Time taken: 0.055

调用 Basedynamic_dispatch 方法所消耗时间是调用 Derived10 方法的两倍多。而 proc 关键字定义的静态分发耗时仅是动态分发的 1/10,之所以性能提升那么大,主要原因大概有两个。首先是 CPU 分支预测命中导致的性能提升,其次是编译器将函数调用优化为内联展开。