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

前端 未结 6 726
天涯浪人
天涯浪人 2020-12-07 22:04

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

相关标签:
6条回答
  • 2020-12-07 22:46

    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.

    0 讨论(0)
  • 2020-12-07 22:47

    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.

    0 讨论(0)
  • 2020-12-07 22:50

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

    0 讨论(0)
  • 2020-12-07 22:50

    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
    
    0 讨论(0)
  • 2020-12-07 23:00

    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
    
    0 讨论(0)
  • 2020-12-07 23:04

    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

    0 讨论(0)
提交回复
热议问题