什么是Duff’s device设备
达夫设备是串行复制的一种优化实现,主要是利用汇编语言编程的优化思路,该思路要求“在复制时最小化判断数和分支数”。1983年11月,当时在影视公司工作的Tom Duff,为了提高动画实时程序的速度发明这种实现。
我们来看看达夫设备实现的示例代码:
send(to, from, count)
register short *to, *from;
register count;
{
register n=(count+7)/8;
switch(count%8){
case 0: do{ *to = *from++;
case 7: *to = *from++;
case 6: *to = *from++;
case 5: *to = *from++;
case 4: *to = *from++;
case 3: *to = *from++;
case 2: *to = *from++;
case 1: *to = *from++;
}while(--n>0);
}
}
这段代码看起来很奇怪,但仍可与C语言兼容。主要原因有:发明达夫设备的当时,switch语句的规范较为宽松;C语言对跳转到循环内部提供了支持,此处的switch/case可以跳到循环的内部。
分析这段代码:
- count%8,得到余数 $mod
- switch/case 跳转到 case $mod 位置,这个位置处于do/while的内循环。
- 接着从case $mod 位置往下执行,一直执行到while处
- while(–n>0), 若条件满足,则继续执行do/while
这段代码怎么做到“在复制时最小化判断数和分支数”的?这要从汇编角度来分析,我们可以在godbolt上看看这段代码的的汇编,我们选择编译器x86-64 clang 11.0.0。点我查看汇编代码
do…while(–n>0)的的汇编指令为
// --n
mov eax, dword ptr [rbp - 32]
add eax, -1
mov dword ptr [rbp - 32], eax
// n>0
cmp eax, 0
// 跳到循环开始的地方
jg .LBB0_2
// 跳出循环
jmp .LBB0_12
除了跳出循环体的那一条指令,用于控制循环体的指令共5条,也就是一个循环内,这5条指令一定会执行。
假设count为20,如果按照do/while流程执行,即:
do{
*to = *from++;
}while(--count>0);
只算用于控制循环体的指令数,控制循环体的总指令数为:5 * 20 = 120 条。使用上面的达夫设备,控制循环体的总执行次数为:5 * 3 = 15 。对比可以发现达夫设备的代码大大减少了控制循环体的总指令数,同时也减少了jump的次数,降低了指令流水线被中断的次数。
Golang编译器中的达夫设备
我们研究的代码版本为golang 1.14.4,生成达夫设备的代码在 go/1.14.4/libexec/src/runtime/mkduff.go 可以找到。
func zero386(w io.Writer) {
// AX: zero
// DI: ptr to memory to be zeroed
// DI is updated as a side effect.
fmt.Fprintln(w, "TEXT runtime·duffzero(SB), NOSPLIT, $0-0")
for i := 0; i < 128; i++ {
fmt.Fprintln(w, "\tSTOSL")
}
fmt.Fprintln(w, "\tRET")
}
func copy386(w io.Writer) {
// SI: ptr to source memory
// DI: ptr to destination memory
// SI and DI are updated as a side effect.
//
// This is equivalent to a sequence of MOVSL but
// for some reason MOVSL is really slow.
fmt.Fprintln(w, "TEXT runtime·duffcopy(SB), NOSPLIT, $0-0")
for i := 0; i < 128; i++ {
fmt.Fprintln(w, "\tMOVL\t(SI), CX")
fmt.Fprintln(w, "\tADDL\t$4, SI")
fmt.Fprintln(w, "\tMOVL\tCX, (DI)")
fmt.Fprintln(w, "\tADDL\t$4, DI")
fmt.Fprintln(w)
}
fmt.Fprintln(w, "\tRET")
}
mkduff用于生成不同指令集的达夫设备代码,以上为x86清零和复制的达夫设备优化代码。可见在golang中,在编译器层面已经用达夫设备对代码做了优化。
在实际项目中,也会遇到 runtime·duffcopy 的cpu使用率较高的情况,这种情况下可以减少直接赋值操作,改为指针赋值操作。
总结
达夫设备是一种汇编层面的优化的手段,尽管高级语言层面的代码看起来不容易理解,同时golang也在编译器层面引入达夫设备来优化代码。在实际项目中并不建议大家滥用达夫设备的技巧的来优化代码,不少高级语言已经在编译器层面已经做了相应的优化,用不好反而阻碍了编译器的优化效果。