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
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.