67

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")
    }
}
lucas clemente
  • 6,255
  • 8
  • 41
  • 61
Ory Band
  • 14,716
  • 14
  • 59
  • 66
  • 3
    Do you have a particular performance you'd like to achieve? Did you benchmark different solutions? – Ainar-G Jan 19 '15 at 13:29
  • Go keeps an internal type info attached to each interface that's how you can do type conversion. so basically keeping the type as an internal value in your class does the same but probably less efficient. – Not_a_Golfer Jan 19 '15 at 13:40
  • 1
    @Ainar-G nothing in particular. I just want to know if type assertion performance is as bad as casting in C/C++ or other similar languages. – Ory Band Jan 19 '15 at 15:00

6 Answers6

50

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.

siritinga
  • 4,063
  • 25
  • 38
  • 3
    I would remove "casting" from your answer. A type switch is different than a type cast, which go doesn't allow without the use of the `unsafe` package. You also forgot a bare type assertion, which is slightly faster than a type switch. – JimB Jan 19 '15 at 19:23
  • I haven't included the type assertion, you are right, but if you chain several if else to do what a switch do, do you think it would be faster? Regarding type casting, I'm not sure what you mean. Isn't `mynewint := any.(*myint)` type casting? – siritinga Jan 19 '15 at 19:35
  • if+else's might be about the same as a switch, but a single type assertion is very common in Go code. That isn't a cast (in the C or Java sense). It's a [type assertion](http://golang.org/ref/spec#Type_assertions), and is type-safe. A cast is more akin to `(*[8]byte)(unsafe.Pointer(&myInt))` – JimB Jan 19 '15 at 19:50
  • Thanks JimB, I thought that type assertion was the optional second `ok` parameter and the first was the cast. – siritinga Jan 19 '15 at 19:54
  • The comma-ok form gives you the bool to check; the non-ok form is when you want it to panic if the assertion is invalid. The underlying assertion is the same. – JimB Jan 19 '15 at 19:56
  • I have included the type assertion, that as you said, it is a minor improvement with respect to type switch, and removed "casting". Thanks! – siritinga Jan 19 '15 at 20:17
  • @JimB A type assertion is a cast in the Java sense, for non primative types. Casting references in Java *is* a "type assertion" – Antimony Aug 06 '16 at 11:55
  • 33
    Just tested this with go1.7 on a beefy MacBook Pro, results: BenchmarkIntmethod-8 2000000000 1.97 ns/op BenchmarkInterface-8 1000000000 2.27 ns/op BenchmarkTypeSwitch-8 2000000000 1.90 ns/op BenchmarkTypeAssertion-8 2000000000 1.89 ns/op PASS ----------- Seems like, there is almost no overhead anymore :-) – Kr0e Aug 29 '16 at 07:31
  • 1
    with the SSA added in Go 1.7, I would be shocked if this code does not get optimized out to bypass the type casting since there is only one case. I would guess that this is the reason for the similar performance seen by @kr0e – physphun Nov 25 '16 at 07:05
  • 2
    @physphun: The number of cases in the type switch statement does not affect performance of this benchmark. This playground example contains two types implementing the Inccer interface https://play.golang.org/p/s4SEk6Ell6 and performance is the same as when using only one type in the type switch. The new compiler must be doing smart things independently of the number of cases in the type assertion switch. – prl900 Jan 27 '17 at 03:04
  • Which was the Go version used for this test? – luistm Dec 29 '17 at 10:44
  • @physphun you are right. This bench https://play.golang.org/p/3Nl6ilcsfIu demos inlining and optimization out. On my machine BenchmarkTypeAssert-4 300000 4782 ns/op BenchmarkTypeAssertFast-4 5000000 356 ns/op – Larytet Nov 08 '19 at 08:59
  • I'd suggest adding `//go:noinline` to the different implementations. This is because inlining ends up devirtualizing a great deal. Just to give an example, with the code verbatim I get a ratio of 0.92 of ns/op for interface vs plain int, while with the noinline directive that becomes 1.11. For non-trivial methods and less predictable types inlining and devirtualization is more unlikely to happen, and mostly we're not comparing what we think. – Oppen Dec 30 '21 at 13:23
  • Two more examples of benchmarks https://go.dev/play/p/--hp-OGyHwY and https://go.dev/play/p/ZdJBcye1YM9 Switch is x20 slower – Larytet Mar 30 '23 at 03:11
17

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

Ted
  • 972
  • 2
  • 11
  • 20
  • Interesting. I wonder what happened. Maybe someone can provide some docs explaining this performance increase? – Ory Band Jul 10 '16 at 09:52
  • @Ted are you sure you are using Go 1.6? the SSA added in 1.7 could be responsible for optimizing this. – physphun Nov 25 '16 at 07:08
  • @physphun Go 1.7 wasn't released when I posted this answer, so yes :-) – Ted Dec 10 '16 at 15:23
  • Ah! just checking. This performance gain really feels too good to be true. – physphun Dec 14 '16 at 20:21
  • 1
    The performance issue is due to Go's inline complition. Go can't inline an interface function call, and couldn't inline for type switch (but can now, as Go1.9 can do this). adds a comment `//go:noinline` (not sure if Go1.6 supports it or not) before the declaration of `inc` and test again, the result would be all almost the same. – leaf bebop Feb 16 '18 at 22:20
  • See https://play.golang.org/p/nDXjsTcxZqX compiler optimizes the code out – Larytet Nov 08 '19 at 09:07
14

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.

zer09
  • 1,507
  • 2
  • 28
  • 48
6

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
Patrick
  • 81
  • 1
  • 3
  • It turns out go actually compiles (at least in 1.9.2) a type switch into a series of else ifs instead of a jump table. If you make the one type into all test9s instead of all test1s, the following results: BenchmarkRandomInterface-4 20000000 77.2 ns/op BenchmarkRandomTypeSwitch-4 20000000 114 ns/op BenchmarkOneInterface-4 20000000 76.6 ns/op BenchmarkOneTypeSwitch-4 10000000 133 ns/op So this suggests even more firmly to stick with interfaces if you have more than a few possible types. – Patrick Dec 14 '17 at 18:23
4

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
gwind
  • 41
  • 1
  • 6
1

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

Uvelichitel
  • 8,220
  • 1
  • 19
  • 36
  • 1
    can you post your benchmark score? Testing your suggestion on Go 1.9 make it worst. Thanks – zer09 Sep 03 '17 at 12:20