TL;DR: it really depends on type distribution, but interfaces are the safest choice unless you are sure that types will appear in regular chunks. Also consider that if your code is executed infrequently, the branch predictor will also not be warmed up.
Long explanation:
On go1.9.2 on darwin/amd64
BenchmarkIntmethod-4 2000000000 1.67 ns/op
BenchmarkInterface-4 2000000000 1.89 ns/op
BenchmarkTypeSwitch-4 2000000000 1.26 ns/op
BenchmarkTypeAssertion-4 2000000000 1.41 ns/op
BenchmarkTypeAssertionNoCheck-4 2000000000 1.61 ns/op
An important thing to note here is that a type switch with only one branch is not a very fair comparison against using an interface. The CPU branch predictor is going to get very hot, very fast and give very good results. A better bench mark would use pseudo random types and an interface with pseudo random receivers. Obviously, we need to remove the static method dispatch and stick to just interfaces versus typeswitch (type assertion also becomes less meaningful since it would require a lot of if statements, and no one would write that instead of using a type switch). Here is the code:
package main
import (
"testing"
)
type myint0 int64
type myint1 int64
type myint2 int64
type myint3 int64
type myint4 int64
type myint5 int64
type myint6 int64
type myint7 int64
type myint8 int64
type myint9 int64
type DoStuff interface {
doStuff()
}
func (i myint0) doStuff() {
i += 0
}
func (i myint1) doStuff() {
i += 1
}
func (i myint2) doStuff() {
i += 2
}
func (i myint3) doStuff() {
i += 3
}
func (i myint4) doStuff() {
i += 4
}
func (i myint5) doStuff() {
i += 5
}
func (i myint6) doStuff() {
i += 6
}
func (i myint7) doStuff() {
i += 7
}
func (i myint8) doStuff() {
i += 8
}
func (i myint9) doStuff() {
i += 9
}
// Randomly generated
var input []DoStuff = []DoStuff{myint0(0), myint1(0), myint1(0), myint5(0), myint6(0), myint7(0), myint6(0), myint9(0), myint7(0), myint7(0), myint6(0), myint2(0), myint9(0), myint0(0), myint2(0), myint3(0), myint5(0), myint1(0), myint4(0), myint0(0), myi
nt4(0), myint3(0), myint9(0), myint3(0), myint9(0), myint5(0), myint0(0), myint0(0), myint8(0), myint1(0)}
func BenchmarkInterface(b *testing.B) {
doStuffInterface(b.N)
}
func BenchmarkTypeSwitch(b *testing.B) {
doStuffSwitch(b.N)
}
func doStuffInterface(n int) {
for k := 0; k < n; k++ {
for _, in := range input {
in.doStuff()
}
}
}
func doStuffSwitch(n int) {
for k := 0; k < n; k++ {
for _, in := range input {
switch v := in.(type) {
case *myint0:
v.doStuff()
case *myint1:
v.doStuff()
case *myint2:
v.doStuff()
case *myint3:
v.doStuff()
case *myint4:
v.doStuff()
case *myint5:
v.doStuff()
case *myint6:
v.doStuff()
case *myint7:
v.doStuff()
case *myint8:
v.doStuff()
case *myint9:
v.doStuff()
}
}
}
}
And the results:
go test -bench .
goos: darwin
goarch: amd64
pkg: test
BenchmarkInterface-4 20000000 74.0 ns/op
BenchmarkTypeSwitch-4 20000000 119 ns/op
PASS
ok test 4.067s
The more types and the more random the distribution, the bigger win interfaces will be.
To show this disparity I changed the code to benchmark random choice versus always picking the same type. In this case, the typeswitch is again faster, while the interface is the same speed, here is the code:
package main
import (
"testing"
)
type myint0 int64
type myint1 int64
type myint2 int64
type myint3 int64
type myint4 int64
type myint5 int64
type myint6 int64
type myint7 int64
type myint8 int64
type myint9 int64
type DoStuff interface {
doStuff()
}
func (i myint0) doStuff() {
i += 0
}
func (i myint1) doStuff() {
i += 1
}
func (i myint2) doStuff() {
i += 2
}
func (i myint3) doStuff() {
i += 3
}
func (i myint4) doStuff() {
i += 4
}
func (i myint5) doStuff() {
i += 5
}
func (i myint6) doStuff() {
i += 6
}
func (i myint7) doStuff() {
i += 7
}
func (i myint8) doStuff() {
i += 8
}
func (i myint9) doStuff() {
i += 9
}
// Randomly generated
var randInput []DoStuff = []DoStuff{myint0(0), myint1(0), myint1(0), myint5(0), myint6(0), myint7(0), myint6(0), myint9(0), myint7(0), myint7(0), myint6(0), myint2(0), myint9(0), myint0(0), myint2(0), myint3(0), myint5(0), myint1(0), myint4(0), myint0(0),
myint4(0), myint3(0), myint9(0), myint3(0), myint9(0), myint5(0), myint0(0), myint0(0), myint8(0), myint1(0)}
var oneInput []DoStuff = []DoStuff{myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0),
myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0), myint1(0)}
func BenchmarkRandomInterface(b *testing.B) {
doStuffInterface(randInput, b.N)
}
func BenchmarkRandomTypeSwitch(b *testing.B) {
doStuffSwitch(randInput, b.N)
}
func BenchmarkOneInterface(b *testing.B) {
doStuffInterface(oneInput, b.N)
}
func BenchmarkOneTypeSwitch(b *testing.B) {
doStuffSwitch(oneInput, b.N)
}
func doStuffInterface(input []DoStuff, n int) {
for k := 0; k < n; k++ {
for _, in := range input {
in.doStuff()
}
}
}
func doStuffSwitch(input []DoStuff, n int) {
for k := 0; k < n; k++ {
for _, in := range input {
switch v := in.(type) {
case *myint0:
v.doStuff()
case *myint1:
v.doStuff()
case *myint2:
v.doStuff()
case *myint3:
v.doStuff()
case *myint4:
v.doStuff()
case *myint5:
v.doStuff()
case *myint6:
v.doStuff()
case *myint7:
v.doStuff()
case *myint8:
v.doStuff()
case *myint9:
v.doStuff()
}
}
}
}
Here are the results:
BenchmarkRandomInterface-4 20000000 76.9 ns/op
BenchmarkRandomTypeSwitch-4 20000000 115 ns/op
BenchmarkOneInterface-4 20000000 76.6 ns/op
BenchmarkOneTypeSwitch-4 20000000 68.1 ns/op
io.StringWriter
is much faster than convertingstring
to[]byte
(in my case cast-assert was around 12ns-15ns, converting 1024 bytes string to[]byte
was arond 250ns-300 ns) – Greenback