Golang中for循环的性能优化探讨

Golang中for循环的性能优化探讨 你好,如果不介意的话,我想了解一下为什么 Go 语言的 “for” 循环比较慢。我尝试对一些简单的 “for” 循环进行基准测试,比较了 Go 默认编译器、TinyGo 和 Graal Native Image 的性能,结果非常令人失望。即使在我关闭了垃圾回收器之后,TinyGo 和 Graal 的表现非常接近,而默认的 Go 编译器则相差甚远。Go 编译器到底有没有对循环进行任何优化呢?

顺便提一下,TinyGo 在调用 C 代码(CGO)方面也表现得更好。

11 回复

Dean_Davidson:

Rust 可能是更适合这项工作的工具。不过,可以看看这个:

谢谢,在我看来 Rust 对心理健康不太好。

更多关于Golang中for循环的性能优化探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我并不是在比较低级语言与低级语言,而是在比较高级语言与高级语言,并尝试不同的编译器,请不要对此感到不快,谢谢。

从Go调用C函数比从Go调用Go慢281倍,而从TinyGo调用C函数比从Go调用Go快107倍。我希望随着时间的推移,Go编译器能有很多改进。

Dean_Davidson: 这似乎与你的发现一致。听起来答案可能是“使用TinyGo”。它缺少哪些你需要的Go语言特性?

谢谢,我决定使用它了。至于TinyGo缺少什么,有些功能可能无法按预期工作。

Dean_Davidson: 对于你正在尝试构建的项目。

游戏引擎。我希望Go和C语言对此来说足够好。

分享具体的基准测试数据以获得具体的反馈。根据你的描述,在我看来,Go 可能并不适合你正尝试完成的工作。如果底层性能至关重要,但你同时又希望拥有现代化的工具链/生态系统,不妨看看 Rust。我个人也相当偏爱 Zig,不过它目前尚未准备好投入生产环境。

游戏引擎。我希望 Go 和 C 语言对此足够胜任。

Rust 可能是更适合这项工作的工具。话虽如此,可以看看:

Ebitengine - A dead simple 2D game engine for Go

Ebitengine - 一个极其简单的 Go 语言 2D 游戏引擎

Ebitengine 是一个用于 Go 编程语言的开源游戏引擎。Ebitengine 简单的 API 允许你快速轻松地开发可以跨多个平台部署的 2D 游戏。

我怎么会觉得是针对我呢?Go 是一种工具;我在某些适合它的任务中使用它,在其他任务中则不用。根据你所说的,我是在质疑它是否是你应该使用的正确工具,并提出了一些替代方案。我还建议你如何改进你的问题,以便可能获得更有用的回复。

我重申一下:如果你想获得具体的反馈,请发布具体的基准测试。就目前而言,你的帖子很模糊,没有包含任何代码,因此很可能只会得到模糊的回应。话虽如此,你甚至可能没有对你认为正在测试的内容进行基准测试。看看这些链接:

为什么 Golang 的 for 循环比 Python 的 for 循环慢?

Golang 在遍历集合时速度慢

https://www.reddit.com/r/golang/comments/2p46ix/golang_simple_loop_slower_than_java/

众所周知,合成基准测试很难做到正确,这也是人们往往更倾向于真实场景的原因。我对在 TinyGo 中调用 C 不是特别熟悉,但我的理解是,它的开销确实更小:

https://aykevl.nl/2021/11/cgo-tinygo/

并且来自 TinyGo 常见问题解答

标准的 Go 编译器为 CGo 调用做了一些特殊处理。这是必要的,因为只有 Go 代码可以使用(较小的)Go 栈,而 C 代码需要一个更大的栈。如果新编译器能确保栈足够大以容纳 C,就可以避免这个限制,从而大大减少 C ↔ Go 调用的开销。

这似乎与你的发现一致。听起来答案可能是“使用 TinyGo”。它缺少哪些你需要的 Go 特性?在语言设计中,几乎每件事都是一系列权衡。再次强调——我会质疑 Go 为你正在尝试构建的项目所做的权衡(易用性、goroutine 的简单并发、编译速度、垃圾回收、强大的标准库、工具、生态系统等)是否正确。

你好,Dean,忘掉我之前说的吧。我尝试了更多纯粹的复杂“for”循环(这些循环无法被编译器优化忽略),以确保循环确实在执行。结果发现,Graal Native Image 比 PureGo 和 TinyGo 慢得多,而 TinyGo 稍微快一点:

TinyGo :              432.148669ms 443.742183ms 440.125731ms
Go :                  485.346782ms 487.894507ms 477.667357ms
GraalNM:              961.963131ms 974.069681ms 957.945913ms

TinyGo 确实做了一些优化,但遗憾的是它不支持最新 Go 版本的所有功能。有没有一些可以传递给 Go 编译器以进行优化的标志呢?

编辑:在 C 语言中进行了相同的测试并从 Go 调用后,TinyGo 表现得更好得多,我认为 TinyGo 调用 C 的效率更高:

Go :      445.494291ms 446.961961ms 442.149793ms
TinyGo :  85.634284ms  61.546723ms  88.512513ms

抱歉,这里提供一个在 PureGo、TinyGo 和 Kotlin/Graal Native Image(它们都没有垃圾回收器)中的简单“for”循环基准测试:

Go 和 TinyGo 代码:

使用的命令是:

$ GOGC=off go build src/main.go $ GOGC=off tinygo build -opt=2 src/main.go

package main
import (
    "fmt"
    "time"    
    )
func main ( ) {
    var timeBefore = time.Now()
    doSomething()
    var timeAfter = time.Now()
    fmt.Println ( timeAfter.Sub(timeBefore) )

}

func doSomething () int {

    var x int = 0
    for i := 1 ; i <= 1000000 ; i++ {
        
        x += i 

    }
    return x
    
}

Kotlin 代码:

$ native-image --gc=epsilon -march=native -O3 -jar main.jar // 将 jar 文件编译为没有垃圾回收器的原生可执行文件

import kotlin.time.Duration
import kotlin.time.measureTime

fun main () {

    var executionTime = measureTime { doSomething() }
    println ( executionTime )

 
}

fun doSomething () :Int {

    var x:Int = 0
    for ( i in 1..1000000 ) {
        x += i
    }
    return x


}
结果 : 
----------------------------- 第一次 | 第二次 | 第三次----------------------
kotlin / Graal                1.816us      1.816us     1.537us
Go                            455.654µs    672.655µs   457.749µs
TinyGo                        5.448µs      5.308µs     3.562µs

即使调用一个 C 语言循环,TinyGo 也更快,正如你在这里看到的,该循环没有调用任何函数(如 println)。

在Go语言中,for循环的性能通常与编译器的优化策略、代码生成方式以及运行时环境有关。标准Go编译器(gc)确实会对循环进行优化,但可能不如TinyGo或Graal Native Image那样激进,特别是在特定场景下如嵌入式系统或AOT编译。以下是一些关键点,结合示例代码说明:

  1. 循环优化:Go编译器会应用常见的循环优化,如循环展开(loop unrolling)和边界检查消除(bounds check elimination)。例如,在遍历切片时,编译器可能优化掉索引的边界检查:

    func sumSlice(s []int) int {
        total := 0
        for i := 0; i < len(s); i++ {
            total += s[i] // 编译器可能消除边界检查
        }
        return total
    }
    

    使用go build -gcflags="-d=ssa/check_bce/debug=1"可以查看边界检查消除情况。

  2. 编译器差异:TinyGo专注于生成小型、高效的代码,适用于资源受限环境,它可能采用更积极的优化策略。Graal Native Image通过AOT编译和逃逸分析来提升性能。标准Go编译器则平衡了编译速度、可调试性和性能,这可能在某些循环基准测试中表现不同。

  3. 性能基准测试示例:以下是一个简单的基准测试,展示如何测试循环性能。确保在真实环境中运行,避免微基准测试的陷阱:

    package main
    
    import (
        "testing"
    )
    
    func BenchmarkLoop(b *testing.B) {
        slice := make([]int, 1000)
        for i := range slice {
            slice[i] = i
        }
        b.ResetTimer()
        for n := 0; n < b.N; n++ {
            total := 0
            for _, v := range slice {
                total += v
            }
        }
    }
    

    运行go test -bench=. -benchmem查看结果。

  4. CGO性能:TinyGo在CGO调用上可能更优,因为它减少了运行时开销。标准Go的CGO涉及上下文切换和垃圾回收影响,可能导致性能下降。例如:

    // #include <stdio.h>
    import "C"
    
    func callC() {
        C.puts(C.CString("Hello")) // TinyGo可能优化此调用
    }
    

总体而言,Go编译器的循环优化是存在的,但针对不同使用场景(如默认编译、嵌入式或AOT),性能表现会有差异。建议根据具体应用场景选择工具链,并通过基准测试验证优化效果。

回到顶部