Pregunta ¿Tiene un interruptor de aserción / tipo de tipo mal rendimiento / es lento en Go?


¿Qué tan lento es el uso de las aserciones de tipo / interruptores de tipo en Go, como método de descubrimiento de tipo de tiempo de ejecución?

He escuchado que en C / C ++, por ejemplo, descubrir tipos en tiempo de ejecución tiene un mal rendimiento. Para eludir eso, generalmente agrega miembros de tipo a las clases, por lo que puede compararlos con estos en lugar de emitir.

No he encontrado una respuesta clara para esto a lo largo de www.

Aquí hay un ejemplo de lo que estoy preguntando: ¿esto es considerado? rápido cuando se compara con otras metodologías de verificación de tipos (como se mencionó anteriormente, u otras que desconozco)

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")
    }
}

32
2018-01-19 12:39


origen


Respuestas:


Es muy fácil escribir una prueba de referencia para verificarlo: 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()
        }
    }
}

En mi máquina amd64, obtengo el siguiente horario:

$ 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

Por lo tanto, parece que acceder al método mediante el cambio de tipo o la aserción de tipo es aproximadamente 5-6 veces más lento que llamar al método directamente o a través de la interfaz.

No sé si C ++ es más lento o si esta ralentización es tolerable para su aplicación.


27
2018-01-19 15:21



Quería verificar la respuesta de siritinga por mí mismo y comprobar si quitar el cheque en TypeAsertion lo haría más rápido. Agregué lo siguiente en su punto de referencia:

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

y volví a ejecutar los puntos de referencia en mi máquina.

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

Parece que el costo de hacer un cambio de tipo se redujo significativamente de Go 1.4 (que supongo que siritinga usó) a Go 1.6 (que estoy usando): de 5-6 veces más lento a menos de 2 veces más lento para un tipo de cambio, y no hay desaceleración para una afirmación de tipo (con o sin control).


10
2017-07-07 12:18



Ejecuto ejemplo de banco por @siritinga en mi computadora portátil (go1.7.3 linux / amd64), obtuve este resultado:

$ 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

3
2018-01-19 08:33



Mis resultados usando 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

La aserción de tipo ahora es mucho más rápida, pero la eliminación más interesante de la verificación de tipo la hace lenta.


3
2017-09-02 14:51



En tus

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

si no necesitas SomeCustomType.Fields o métodos como en fmt.Println(v)haciendo

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

debería ser aproximadamente dos veces más rápido


1
2018-01-19 18:56



TL; DR: realmente depende de la distribución del tipo, pero las interfaces son la opción más segura a menos que esté seguro de que los tipos aparecerán en fragmentos regulares. También considere que si su código se ejecuta con poca frecuencia, el predictor de bifurcación tampoco se calentará.

Larga explicación

En go1.9.2 en 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

Una cosa importante a tener en cuenta aquí es que un cambio de tipo con una sola rama no es una comparación muy justa contra el uso de una interfaz. El predictor de la rama de la CPU se pondrá muy caliente, muy rápido y dará muy buenos resultados. Un mejor benchmark usaría tipos pseudoaleatorios y una interfaz con receptores pseudoaleatorios. Obviamente, necesitamos eliminar el envío de métodos estáticos y atenernos solo a las interfaces frente a los conmutadores de tipo (la aserción de tipo también se vuelve menos significativa ya que requeriría muchas declaraciones if, y nadie escribiría eso en lugar de usar un interruptor de tipo). Aquí está el código:

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()
                        }
                }
        }
}

Y los resultados:

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

Cuantos más tipos y más aleatoria sea la distribución, más grandes serán las interfaces ganadoras.

Para mostrar esta disparidad, cambié el código para comparar la elección aleatoria versus siempre elegir el mismo tipo. En este caso, el interruptor de tipo es de nuevo más rápido, mientras que la interfaz tiene la misma velocidad, aquí está el código:

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()
                        }
                }
        }
}

Aquí están los resultados:

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

1
2017-12-14 17:10