Golang中的达夫设备(Duff‘s device)

什么是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也在编译器层面引入达夫设备来优化代码。在实际项目中并不建议大家滥用达夫设备的技巧的来优化代码,不少高级语言已经在编译器层面已经做了相应的优化,用不好反而阻碍了编译器的优化效果。

参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值