Does a type assertion / type switch have bad performance / is slow in Go?

CastingGoType ConversionType Assertion

Casting Problem Overview


How slow is using type assertions / type switches in Go, as a method of run-time type discovery?

I've heard that in C/C++ for example, discovering types at run time has bad performance. To bypass that, you usually add type members to classes, so you can compare against these instead of casting.

I haven't found a clear answer for this throughout the www.

Here's an example of what I'm asking about - Is this considered fast when compared to other type checking methodologies (like mentioned above, or others I'm not aware of)?

func question(anything interface{}) {
    switch v := anything.(type) {
        case string:
            fmt.Println(v)
        case int32, int64:
            fmt.Println(v)
        case SomeCustomType:
            fmt.Println(v)
        default:
            fmt.Println("unknown")
    }
}

Casting Solutions


Solution 1 - Casting

It is very easy to write a Benchmark test to check it: http://play.golang.org/p/E9H_4K2J9-

package main

import (
	"testing"
)

type myint int64

type Inccer interface {
	inc()
}

func (i *myint) inc() {
	*i = *i + 1
}

func BenchmarkIntmethod(b *testing.B) {
	i := new(myint)
	incnIntmethod(i, b.N)
}

func BenchmarkInterface(b *testing.B) {
	i := new(myint)
	incnInterface(i, b.N)
}

func BenchmarkTypeSwitch(b *testing.B) {
	i := new(myint)
	incnSwitch(i, b.N)
}

func BenchmarkTypeAssertion(b *testing.B) {
	i := new(myint)
	incnAssertion(i, b.N)
}

func incnIntmethod(i *myint, n int) {
	for k := 0; k < n; k++ {
		i.inc()
	}
}

func incnInterface(any Inccer, n int) {
	for k := 0; k < n; k++ {
		any.inc()
	}
}

func incnSwitch(any Inccer, n int) {
	for k := 0; k < n; k++ {
		switch v := any.(type) {
		case *myint:
			v.inc()
		}
	}
}

func incnAssertion(any Inccer, n int) {
	for k := 0; k < n; k++ {
		if newint, ok := any.(*myint); ok {
			newint.inc()
		}
	}
}

EDIT Oct. 09, 2019

It appears that the methods demonstrated above are equal and have no advantage over one another. Here are the results from my machine (AMD R7 2700X, Golang v1.12.9):

BenchmarkIntmethod-16        	2000000000	         1.67 ns/op
BenchmarkInterface-16        	1000000000	         2.03 ns/op
BenchmarkTypeSwitch-16       	2000000000	         1.70 ns/op
BenchmarkTypeAssertion-16    	2000000000	         1.67 ns/op
PASS

AND AGAIN:

BenchmarkIntmethod-16        	2000000000	         1.68 ns/op
BenchmarkInterface-16        	1000000000	         2.01 ns/op
BenchmarkTypeSwitch-16       	2000000000	         1.66 ns/op
BenchmarkTypeAssertion-16    	2000000000	         1.67 ns/op

PREVIOUS RESULTS on Jan. 19, 2015

On my amd64 machine, I'm getting the following timing:

$ go test -bench=.
BenchmarkIntmethod	1000000000	         2.71 ns/op
BenchmarkInterface	1000000000	         2.98 ns/op
BenchmarkTypeSwitch	100000000	        16.7 ns/op
BenchmarkTypeAssertion	100000000	    13.8 ns/op

So it looks like accessing the method via type switch or type assertion is about 5-6 times slower than calling the method directly or via interface.

I don't know if C++ is slower or if this slowdown is tolerable for your application.

Solution 2 - Casting

I wanted to verify siritinga's answer by myself, and check whether removing the check in TypeAssertion would make it faster. I added the following in their benchmark:

func incnAssertionNoCheck(any Inccer, n int) {
    for k := 0; k < n; k++ {
		any.(*myint).inc()
	}
}

func BenchmarkTypeAssertionNoCheck(b *testing.B) {
	i := new(myint)
	incnAssertionNoCheck(i, b.N)
}

and re-ran the benchmarks on my machine.

BenchmarkIntmethod-12           	2000000000	         1.77 ns/op
BenchmarkInterface-12           	1000000000	         2.30 ns/op
BenchmarkTypeSwitch-12          	500000000	         3.76 ns/op
BenchmarkTypeAssertion-12       	2000000000	         1.73 ns/op
BenchmarkTypeAssertionNoCheck-12	2000000000	         1.72 ns/op

So it seems that the cost of doing a type switch went down significantly from Go 1.4 (that I assume siritinga used) to Go 1.6 (that I'm using): from 5-6 times slower to less than 2 times slower for a type switch, and no slow-down for a type assertion (with or without check).

Solution 3 - Casting

My Results using Go 1.9

BenchmarkIntmethod-4              	1000000000	         2.42 ns/op
BenchmarkInterface-4              	1000000000	         2.84 ns/op
BenchmarkTypeSwitch-4             	1000000000	         2.29 ns/op
BenchmarkTypeAssertion-4          	1000000000	         2.14 ns/op
BenchmarkTypeAssertionNoCheck-4   	1000000000	         2.34 ns/op

Type Assertion is much faster now, but the most interesting removing the type check makes it slow.

Solution 4 - Casting

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

Solution 5 - Casting

I run bench example by @siritinga in my laptop (go1.7.3 linux/amd64), got this result:

$ go test -bench .
BenchmarkIntmethod-4            2000000000               1.99 ns/op
BenchmarkInterface-4            1000000000               2.30 ns/op
BenchmarkTypeSwitch-4           2000000000               1.80 ns/op
BenchmarkTypeAssertion-4        2000000000               1.67 ns/op

Solution 6 - Casting

In your

switch v := anything.(type) {
    case SomeCustomType:
        fmt.Println(v)
...

if you need not SomeCustomType.Fields or methods like in fmt.Println(v), doing

switch anything.(type) { //avoid 'v:= ' interface conversion, only assertion
    case SomeCustomType:
        fmt.Println("anything type is SomeCustomType", anything)
...

should be approximately two times faster

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionOry BandView Question on Stackoverflow
Solution 1 - CastingsiritingaView Answer on Stackoverflow
Solution 2 - CastingTedView Answer on Stackoverflow
Solution 3 - Castingzer09View Answer on Stackoverflow
Solution 4 - CastingPatrickView Answer on Stackoverflow
Solution 5 - CastinggwindView Answer on Stackoverflow
Solution 6 - CastingUvelichitelView Answer on Stackoverflow