原文:Optimized abs() for int64 in Go,译文:在 Golang 中针对 int64 类型优化 abs(),欢迎转载。
前言
abs()
abs()
mathabs()float64int64float64int64
math.Abs
文章中的源码和测试用例在 cavaliercoder/go-abs
类型转换 VS 分支控制的方法
abs.WithBranch
package abs
func WithBranch(n int64) int64 {
if n < 0 {
return -n
}
return n
}
math.Abs
package abs
func WithStdLib(n int64) int64 {
return int64(math.Abs(float64(n)))
}
int64float64math.Absint64
$ go test -bench=. goos: darwin goarch: amd64 pkg: github.com/cavaliercoder/abs BenchmarkWithBranch-8 2000000000 0.30 ns/op BenchmarkWithStdLib-8 2000000000 0.79 ns/op PASS ok github.com/cavaliercoder/abs 2.320s
WithBranch
abs.WithBranch(-9223372036854775807)WithStdLib(-9223372036854775807)WithStdLib(9223372036854775807)
不依赖分支控制的方法取绝对值的方法对有符号整数显然更快更准,不过还有更好的办法吗?
我们都知道不依赖分支控制的方法的代码破坏了程序的运行顺序,即 pipelining processors 无法预知程序的下一步动作。
与不依赖分支控制的方法不同的方案
Hacker’s Delight 第二章介绍了一种无分支控制的方法,通过 Two’s Complement 计算有符号整数的绝对值。
为计算 x 的绝对值:
func WithASM(n int64) int64
// abs_amd64.s TEXT ·WithASM(SB),$0 MOVQ n+0(FP),AX // copy input to AX MOVQ AX,CX // y ← x SARQ $63,CX // y ← y >> 63 XORQ CX,AX // x ← x ⨁ y SUBQ CX,AX // x ← x - y MOVQ AX,ret+8(FP) // copy result to return value RET
WithASM_amd64.s
WithASM
$ go test -bench=. goos: darwin goarch: amd64 pkg: github.com/cavaliercoder/abs BenchmarkWithBranch-8 2000000000 0.29 ns/op BenchmarkWithStdLib-8 2000000000 0.78 ns/op BenchmarkWithASM-8 2000000000 1.78 ns/op PASS ok github.com/cavaliercoder/abs 6.059s
这就比较尴尬了,这个简单的基准测试显示无分支控制结构高度简洁的代码跑起来居然很慢:1.78 ns/op,怎么会这样呢?
编译选项
WithASM-mgo buildgo test-gcflags=-m
运行效果:
$ go tool compile -m abs.go # github.com/cavaliercoder/abs ./abs.go:11:6: can inline WithBranch ./abs.go:21:6: can inline WithStdLib ./abs.go:22:23: inlining call to math.Abs
对于我们这个简单的函数,Go 的编译器支持 function inlining,函数内联是指在调用我们函数的地方直接使用这个函数的函数体来代替。举个例子:
package main
import (
"fmt"
"github.com/cavaliercoder/abs"
)
func main() {
n := abs.WithBranch(-1)
fmt.Println(n)
}
实际上会被编译成:
package main
import "fmt"
func main() {
n := -1
if n < 0 {
n = -n
}
fmt.Println(n)
}
WithBranchWithStdLibWithASMWithStdLibmath.Abs
WithASMWithASM
如果我们在其他函数中不使用内联会怎么样?可以写个简单的示例程序:
package abs
//go:noinline
func WithBranch(n int64) int64 {
if n < 0 {
return -n
}
return n
}
重新编译,我们会看到编译器优化内容变少了:
$ go tool compile -m abs.go abs.go:22:23: inlining call to math.Abs
基准测试的结果:
$ go test -bench=. goos: darwin goarch: amd64 pkg: github.com/cavaliercoder/abs BenchmarkWithBranch-8 1000000000 1.87 ns/op BenchmarkWithStdLib-8 1000000000 1.94 ns/op BenchmarkWithASM-8 2000000000 1.84 ns/op PASS ok github.com/cavaliercoder/abs 8.122s
可以看出,现在三个函数的平均执行时间几乎都在 1.9 ns/op 左右。
WithBranch
WithASM
只使用一个内联函数
Go 编译器无法内联由汇编实现的函数,但是内联我们重写后的普通函数是很容易的:
package abs
func WithTwosComplement(n int64) int64 {
y := n >> 63 // y ← x >> 63
return (n ^ y) - y // (x ⨁ y) - y
}
编译结果说明我们的方法被内联了:
$ go tool compile -m abs.go ... abs.go:26:6: can inline WithTwosComplement
WithBranch
$ go test -bench=. goos: darwin goarch: amd64 pkg: github.com/cavaliercoder/abs BenchmarkWithBranch-8 2000000000 0.29 ns/op BenchmarkWithStdLib-8 2000000000 0.79 ns/op BenchmarkWithTwosComplement-8 2000000000 0.29 ns/op BenchmarkWithASM-8 2000000000 1.83 ns/op PASS ok github.com/cavaliercoder/abs 6.777s
WithTwosComplementWithASMWithASM
-S
$ go tool compile -S abs.go
...
"".WithTwosComplement STEXT nosplit size=24 args=0x10 locals=0x0
0x0000 00000 (abs.go:26) TEXT "".WithTwosComplement(SB),NOSPLIT,$0-16
0x0000 00000 (abs.go:26) FUNCDATA $0,gclocals·f207267fbf96a0178e8758c6e3e0ce28(SB)
0x0000 00000 (abs.go:26) FUNCDATA $1,gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (abs.go:26) MOVQ "".n+8(SP),AX
0x0005 00005 (abs.go:26) MOVQ AX,CX
0x0008 00008 (abs.go:27) SARQ $63,AX
0x000c 00012 (abs.go:28) XORQ AX,CX
0x000f 00015 (abs.go:28) SUBQ AX,CX
0x0012 00018 (abs.go:28) MOVQ CX,"".~r1+16(SP)
0x0017 00023 (abs.go:28) RET
...
WithASMWithTwosComplementGOARCH=386
go test -bench=. -benchme
总结
WithTwosComplement
最后,我对 int64 的 abs 实现如下:
func abs(n int64) int64 {
y := n >> 63
return (n ^ y) - y
}