Search code examples
gopluginsbenchmarking

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


I wanted to benchmark go plugins to see what the performance difference was. So I made a main.go file with the following code:

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()
}

Then I turn this into a plugin.so file.

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

Next I wrote two benchmark functions to test the performance of running the function inline vs running it via a go plugin.

// 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())
    }
}

As I would expect the plugin is a bit slower but not by much

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

Next I wanted to add 2 more benchmarks so I added another function to generate a random integer and built the plugin in the same way as before.

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

Then my new benchmark functions added above the previous two benchmark functions.

// 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()
    }
}

Now when I run the benchmark again I get the following result:

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

I can run the benchmark over and over again and BenchmarkRandString-12 is always a bit slower than BenchmarkPluginRandString-12 which is not what I would expect. Why is the go plugin function slightly faster when benchmarking like this?

I have a Github project with all of the source code I am using here: https://github.com/uberswe/goplugins/tree/4825172e011da9578553d113bac7933ca9ecd038


Solution

  • What may be slower with a "plugin" function is its loading and the type assertion. Once you're done it, there should be no performance penalty compared to a function defined in your app.

    Such small deviations may be the result of Go's internal memory management and garbage collection. For example if in your main_test.go file I move BenchmarkPluginRandString() above BenchmarkRandString(), then the benchmark result are "reversed": BenchmarkRandString() gets slightly slower.

    To get rid of such non-deterministic factors, you may try to run the benchmarks isolated, e.g. run only one at a time with

    go test -bench BenchmarkRandString
    

    and

    go test -bench BenchmarkPluginRandString
    

    And do this multiple times, and calculate the average. This way there is no noticeable difference.