在 Go 中,非捕获闭包会损害性能吗?

Vas*_*nov 5 performance closures go

例如,github.com/yhat/scrape建议使用这样的闭包:

func someFunc() {
    ...
    matcher := func(n *html.Node) bool {
        return n.DataAtom == atom.Body
    }
    body, ok := scrape.Find(root, matcher)
    ...
}
Run Code Online (Sandbox Code Playgroud)

由于matcher实际上并没有捕获任何局部变量,因此这可以等效地写为:

func someFunc() {
    ...
    body, ok := scrape.Find(root, matcher)
    ...
}

func matcher(n *html.Node) bool {
    return n.DataAtom == atom.Body
}
Run Code Online (Sandbox Code Playgroud)

第一种形式看起来更好,因为 matcher 函数非常特定于代码中的那个地方。但是它在运行时的性能是否更差(假设someFunc可能经常被调用)?

我想创建一个闭包肯定会有一些开销,但是这种闭包可以被编译器优化成一个常规函数吗?

(显然语言规范不需要这个;我对 gc 实际做什么感兴趣。)

Vas*_*nov 6

好像没什么区别。我们可以检入生成的机器码。

这是一个玩具程序:

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()
}
Run Code Online (Sandbox Code Playgroud)

这是它的反汇编:

$ 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         
Run Code Online (Sandbox Code Playgroud)

正如我们所见,至少在这个简单的例子中,topLevelFunctioninnerFunction( invoke.func1) 以及它们传递到 的方式没有结构差异useFunction,被转换为机器代码。

(将其与innerFunction捕获局部变量的情况进行比较是有益的;此外,innerFunction与通过全局变量而不是函数参数传递的情况进行比较是有益的——但这些留给读者作为练习。)

  • 使用 Go 1.14.7 重新审视这一点,“-gcflags '-m -m'”表示“topLevelFunction”和“innerFunction”都非常小,以至于它们会被内联,因此汇编输出相同是有意义的。为了这是一个真正的比较,我认为函数需要稍微大一些,这样它们就不会被内联。 (6认同)