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

时间:2017-08-29 11:25:32

标签: performance go closures

例如,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实际上做了什么感兴趣。)

3 个答案:

答案 0 :(得分:1)

一般应该。考虑到编译器优化可能更是如此(因为关于函数的推理通常比关闭更容易,所以我希望编译器倾向于更频繁地优化函数,然后是等效的闭包)。但它并不完全是黑白两色,因为许多因素可能会影响最终生成的代码,包括您的平台和编译器本身的版本。更重要的是,你的其他代码通常会影响性能,远远超过调用的速度(算法明智和代码行),这似乎是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来分析生成的实际代码,以获得差异来源的线索。但根据经验,我建议更专注于编写更好的代码(无论对你来说意味着什么)并忽略实际调用的速度(如在&#34;避免过早优化&#34;)。

答案 1 :(得分:0)

我不认为功能声明的范围会损害性能。在调用中内联lambda也很常见。我写了

body, ok := scrape.Find(root, func (n *html.Node) bool {return n.DataAtom == atom.Body})

答案 2 :(得分:0)

似乎没有区别。我们可以签入生成的机器代码。

这是一个玩具程序:

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         

正如我们所看到的,至少在这个简单的情况下,topLevelFunctioninnerFunctioninvoke.func1)的方式与useFunction传递方式没有任何结构差异,被翻译成机器代码。

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