例如,github.com/yhat/scrape建议使用这样的闭包:
func someFunc() {
...
matcher := func(n *html.Node) bool {
return n.DataAtom == atom.Body
}
body, ok := scrape.Find(root, matcher)
...
}
由于matcher
实际上并不捕获任何局部变量,因此可以等效地写为:
func someFunc() {
...
body, ok := scrape.Find(root, matcher)
...
}
func matcher(n *html.Node) bool {
return n.DataAtom == atom.Body
}
第一种形式看起来更好,因为匹配器功能非常特定于代码中的那个位置。但是它在运行时表现更差(假设经常可以调用someFunc
)吗?
我想创建一个闭包肯定会有一些开销,但这种闭包可以被编译器优化成常规函数吗?
(显然语言规范并不需要这个;我对gc实际上做了什么感兴趣。)
它通常应该。考虑到编译器优化可能更是如此(因为关于函数的推理通常比关闭更容易,所以我希望编译器倾向于更频繁地优化函数,然后是等效的闭包)。但它并不完全是黑白两色,因为许多因素可能会影响最终生成的代码,包括您的平台和编译器本身的版本。更重要的是,你的其他代码通常会影响性能,远远超过调用的速度(算法明智和代码行),这似乎是JimB所做的。
例如,我编写了以下示例代码,然后对其进行了基准测试。
var (
test int64
)
const (
testThreshold = int64(1000000000)
)
func someFunc() {
test += 1
}
func funcTest(threshold int64) int64 {
test = 0
for i := int64(0); i < threshold; i++ {
someFunc()
}
return test
}
func closureTest(threshold int64) int64 {
someClosure := func() {
test += 1
}
test = 0
for i := int64(0); i < threshold; i++ {
someClosure()
}
return test
}
func closureTestLocal(threshold int64) int64 {
var localTest int64
localClosure := func() {
localTest += 1
}
localTest = 0
for i := int64(0); i < threshold; i++ {
localClosure()
}
return localTest
}
在我的笔记本电脑上,funcTest每次迭代需要2.0 ns,closureTest需要2.2 ns,closureTestLocal需要1.9ns。在这里,closureTest vs funcTest出现确认你(和我的)假设闭包调用比函数调用慢。但请注意,这些测试功能是故意制作的简单和小型,以使呼叫速度差异脱颖而出,它仍然只有10%的差异。事实上,检查编译器输出显示实际上在funcTest情况下编译器执行内联funcTest而不是调用它。所以,如果没有,我会期望差异更小。但更重要的是,我想指出closureTestLocal比(内联)函数快5%,即使这个实际上是一个捕获闭包。请注意,两个闭合都没有内联或优化 - 两个闭包测试忠实地拨打所有电话。我在本地闭包的编译代码中看到的唯一区别完全在堆栈上运行,而其他两个函数都通过它的地址访问全局变量(在内存中的某个地方)。但是虽然我可以通过查看已编译的代码轻松地推断出差异,但我的观点是 - 即使在最简单的情况下,它也不是完全黑白的。
因此,如果速度在您的情况下非常重要,我建议您使用基准测试(以及实际代码)。您还可以使用go tool objdump
来分析生成的实际代码,以获得差异来源的线索。但根据经验,我建议更专注于编写更好的代码(无论对你意味着什么)并忽略实际调用的速度(如“避免过早优化”)。
我认为功能声明的范围不会影响性能。在调用中内联lambda也很常见。我写的
body, ok := scrape.Find(root, func (n *html.Node) bool {return n.DataAtom == atom.Body})
好像没什么区别。我们可以签入生成的机器代码。
这是一个玩具程序:
package main
import "fmt"
func topLevelFunction(x int) int {
return x + 4
}
func useFunction(fn func(int) int) {
fmt.Println(fn(10))
}
func invoke() {
innerFunction := func(x int) int {
return x + 8
}
useFunction(topLevelFunction)
useFunction(innerFunction)
}
func main() {
invoke()
}
这是它的反汇编:
$ go version
go version go1.8.5 linux/amd64
$ go tool objdump -s 'main\.(invoke|topLevel)' bin/toy
TEXT main.topLevelFunction(SB) /home/vasiliy/cur/work/learn-go/src/my/toy/toy.go
toy.go:6 0x47b7a0 488b442408 MOVQ 0x8(SP), AX
toy.go:6 0x47b7a5 4883c004 ADDQ $0x4, AX
toy.go:6 0x47b7a9 4889442410 MOVQ AX, 0x10(SP)
toy.go:6 0x47b7ae c3 RET
TEXT main.invoke(SB) /home/vasiliy/cur/work/learn-go/src/my/toy/toy.go
toy.go:13 0x47b870 64488b0c25f8ffffff FS MOVQ FS:0xfffffff8, CX
toy.go:13 0x47b879 483b6110 CMPQ 0x10(CX), SP
toy.go:13 0x47b87d 7638 JBE 0x47b8b7
toy.go:13 0x47b87f 4883ec10 SUBQ $0x10, SP
toy.go:13 0x47b883 48896c2408 MOVQ BP, 0x8(SP)
toy.go:13 0x47b888 488d6c2408 LEAQ 0x8(SP), BP
toy.go:17 0x47b88d 488d052cfb0200 LEAQ 0x2fb2c(IP), AX
toy.go:17 0x47b894 48890424 MOVQ AX, 0(SP)
toy.go:17 0x47b898 e813ffffff CALL main.useFunction(SB)
toy.go:14 0x47b89d 488d0514fb0200 LEAQ 0x2fb14(IP), AX
toy.go:18 0x47b8a4 48890424 MOVQ AX, 0(SP)
toy.go:18 0x47b8a8 e803ffffff CALL main.useFunction(SB)
toy.go:19 0x47b8ad 488b6c2408 MOVQ 0x8(SP), BP
toy.go:19 0x47b8b2 4883c410 ADDQ $0x10, SP
toy.go:19 0x47b8b6 c3 RET
toy.go:13 0x47b8b7 e874f7fcff CALL runtime.morestack_noctxt(SB)
toy.go:13 0x47b8bc ebb2 JMP main.invoke(SB)
TEXT main.invoke.func1(SB) /home/vasiliy/cur/work/learn-go/src/my/toy/toy.go
toy.go:15 0x47b8f0 488b442408 MOVQ 0x8(SP), AX
toy.go:15 0x47b8f5 4883c008 ADDQ $0x8, AX
toy.go:15 0x47b8f9 4889442410 MOVQ AX, 0x10(SP)
toy.go:15 0x47b8fe c3 RET
正如我们所看到的,至少在这个简单的情况下,topLevelFunction
和innerFunction
(invoke.func1
)以及它们传递给useFunction
的方式没有结构差异被转换为机器码。
(将此与innerFunction
捕获局部变量的情况进行比较是有益的;并且此外,innerFunction
通过全局变量而不是函数参数传递的情况 - 但这些留给读者作为练习。 )