为什么通过 go 插件调用函数比直接调用函数更快

Why is calling a function via a go plugin faster than calling the function directly

我想对 go 插件进行基准测试,看看性能差异是什么。所以我用以下代码制作了一个 main.go 文件:

package main

import (
    "math/rand"
    "strings"
)

// RandString generates and returns a random 50 character string
func RandString(n int) string {
    rand.Seed(int64(n))
    chars := []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
        "abcdefghijklmnopqrstuvwxyz" +
        "0123456789")
    var b strings.Builder
    for i := 0; i < 50; i++ {
        b.WriteRune(chars[rand.Intn(len(chars))])
    }
    return b.String()
}

然后我把它变成一个 plugin.so 文件。

go build -buildmode=plugin -o plugin.so main.go

接下来,我编写了两个基准函数来测试 运行 内联函数与 运行 通过 go 插件内联函数的性能。

// BenchmarkRandString tests generating a random string without a go plugin
func BenchmarkRandString(b *testing.B) {
    for i := 0; i < b.N; i++ {
        RandString(rand.Int())
    }
}

// BenchmarkPluginRandString tests generating a random string with a go plugin
func BenchmarkPluginRandString(b *testing.B) {
    plug, err := plugin.Open("./plugin.so")
    if err != nil {
        panic(err)
    }

    randString, err := plug.Lookup("RandString")
    if err != nil {
        panic(err)
    }

    randFunc, ok := randString.(func(n int) string)
    if !ok {
        panic("unexpected type from module symbol")
    }

    b.ResetTimer()

    for i := 0; i < b.N; i++ {
        randFunc(rand.Int())
    }
}

正如我所预料的那样,插件会慢一点,但不会慢很多

BenchmarkRandString-12 128064 8600 ns/op
BenchmarkPluginRandString-12 132007 8713 ns/op

接下来我想再添加 2 个基准测试,所以我添加了另一个函数来生成一个随机整数并以与之前相同的方式构建插件。

// RandInt uses math/rand to return a random integer
func RandInt() int {
    return rand.Int()
}

然后我的新基准函数添加在前两个基准函数之上。

// BenchmarkRandInt tests math/rand for generating random integers without a go plugin
func BenchmarkRandInt(b *testing.B) {
    for i := 0; i < b.N; i++ {
        RandInt()
    }
}

// BenchmarkPluginRandInt uses a go plugin and tests math/rand for generating random integers
func BenchmarkPluginRandInt(b *testing.B) {
    plug, err := plugin.Open("./plugin.so")
    if err != nil {
        panic(err)
    }

    randInt, err := plug.Lookup("RandInt")
    if err != nil {
        panic(err)
    }

    randFunc, ok := randInt.(func() int)
    if !ok {
        panic("unexpected type from module symbol")
    }

    b.ResetTimer()

    for i := 0; i < b.N; i++ {
        randFunc()
    }
}

现在,当我再次 运行 基准时,我得到以下结果:

BenchmarkRandInt-12 77320668 13.2 ns/op
BenchmarkPluginRandInt-12 76371756 13.9 ns/op
BenchmarkRandString-12 136243 8600 ns/op
BenchmarkPluginRandString-12 142112 8564 ns/op

我可以 运行 一遍又一遍地进行基准测试,BenchmarkRandString-12 总是比 BenchmarkPluginRandString-12 慢一点,这不是我所期望的。为什么像这样进行基准测试时 go 插件的功能会稍微快一些?

我有一个 Github 项目,其中包含我在此处使用的所有源代码:https://github.com/uberswe/goplugins/tree/4825172e011da9578553d113bac7933ca9ecd038

“插件”功能可能较慢的是它的加载和类型断言。完成后,与您的应用程序中定义的函数相比,应该没有性能损失。

这么小的偏差可能是Go内部内存管理和垃圾回收的结果。例如,如果在你的 main_test.go 文件中我将 BenchmarkPluginRandString() 移动到 BenchmarkRandString() 之上,那么基准测试结果是“相反的”:BenchmarkRandString() 变得稍微慢一些。

要摆脱此类不确定因素,您可以尝试 运行 隔离基准,例如运行一次只有一个

go test -bench BenchmarkRandString

go test -bench BenchmarkPluginRandString

重复多次,然后计算平均值。这样就没有明显的区别了。