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