类型断言/类型切换是不是性能不佳/在 Go 中运行缓慢?

Posted

技术标签:

【中文标题】类型断言/类型切换是不是性能不佳/在 Go 中运行缓慢?【英文标题】:Does a type assertion / type switch have bad performance / is slow in Go?类型断言/类型切换是否性能不佳/在 Go 中运行缓慢? 【发布时间】:2015-03-17 11:21:56 【问题描述】:

在 Go 中使用类型断言/类型切换作为运行时类型发现的方法有多慢?

我听说例如在 C/C++ 中,在运行时发现类型的性能很差。为了绕过这一点,您通常将类型成员添加到类中,这样您就可以与这些成员进行比较而不是强制转换。

我在整个 www 中都没有找到明确的答案。

这是我要问的一个示例 - 与其他类型检查方法(如上面提到的或我不知道的其他方法)相比,这是否被认为 快速

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

【问题讨论】:

您有什么特别想达到的表现吗?您是否对不同的解决方案进行了基准测试? Go 会为每个接口保留一个内部类型信息,这就是您可以进行类型转换的方式。因此,基本上将类型保留为类中的内部值也是如此,但效率可能较低。 @Ainar-G 没什么特别的。我只是想知道类型断言性能是否与 C/C++ 或其他类似语言中的强制转换一样差。 【参考方案1】:

很容易写一个 Benchmark 测试来检查它: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()
        
    

2019 年 10 月 9 日编辑

似乎上面演示的方法是相同的,彼此之间没有优势。以下是我机器上的结果(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

再次:

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

2015 年 1 月 19 日的先前结果

在我的 amd64 机器上,我得到以下时间:

$ 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

所以看起来通过类型切换或类型断言访问方法比直接或通过接口调用方法慢大约 5-6 倍。

我不知道 C++ 是否较慢,或者您的应用程序是否可以容忍这种减速。

【讨论】:

我会从你的答案中删除“casting”。类型切换与类型转换不同,如果不使用 unsafe 包,则类型转换是不允许的。您还忘记了一个裸类型断言,它比类型切换稍快。 我没有包含类型断言,你是对的,但是如果你链接几个 if else 来做一个 switch 做的事情,你认为它会更快吗?关于类型转换,我不确定您的意思。 mynewint := any.(*myint) 不是类型转换吗? 刚刚在功能强大的 MacBook Pro 上使用 go1.7 进行了测试,结果: 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 ----------- 好像,几乎没有开销了:-) 在 Go 1.7 中添加了 SSA,如果此代码没有经过优化以绕过类型转换,我会感到震惊,因为只有一种情况。我猜这就是@kr0e 看到类似性能的原因 @physphun:类型 switch 语句中的案例数量不会影响此基准测试的性能。这个 Playground 示例包含两种实现 Inccer 接口 play.golang.org/p/s4SEk6Ell6 的类型,性能与在类型开关中仅使用一种类型时相同。新编译器必须独立于类型断言开关中的案例数量来做智能事情。【参考方案2】:

TL;DR:它确实取决于类型分布,但接口是最安全的选择,除非您确定类型会以常规块的形式出现。还要考虑如果你的代码不经常执行,分支预测器也不会被预热。

详细解释:

在 darwin/amd64 上的 go1.9.2

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

这里要注意的重要一点是,只有一个分支的类型切换与使用接口的比较不是很公平。 CPU 分支预测器将变得非常热、非常快并给出非常好的结果。更好的基准测试将使用伪随机类型和与伪随机接收器的接口。显然,我们需要删除静态方法调度并坚持只使用接口而不是类型开关(类型断言也变得不那么有意义,因为它需要大量的 if 语句,而且没有人会写它而不是使用类型开关)。代码如下:

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 = []DoStuffmyint0(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()
                        
                
        

结果:

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

类型越多,分布越随机,win接口越大。

为了显示这种差异,我将代码更改为对随机选择进行基准测试,而不是始终选择相同的类型。在这种情况下,typeswitch 再次更快,而 interface 速度相同,代码如下:

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 = []DoStuffmyint0(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 = []DoStuffmyint1(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()
                        
                
        

结果如下:

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

【讨论】:

事实证明,go 实际上将(至少在 1.9.2 中)类型切换编译为一系列 else ifs 而不是跳转表。如果将一种类型放入所有 test9s 而不是所有 test1s,结果如下: 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因此,如果您有多个可能的类型,这建议更坚定地坚持使用接口。【参考方案3】:

我使用 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

类型断言现在要快得多,但最有趣的是删除类型检查会使其变慢。

【讨论】:

几年前人们已经对此发表评论/发布了答案... @Seer 您对您的评论有何暗示?抱歉,我没听懂。【参考方案4】:

我在笔记本电脑(go1.7.3 linux/amd64)上运行@siritinga 的bench example,得到了这个结果:

$ 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

【讨论】:

【参考方案5】:

我想自己验证 siritinga 的答案,并检查删除 TypeAssertion 中的检查是否会使其更快。我在他们的基准测试中添加了以下内容:

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)

并在我的机器上重新运行基准测试。

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

因此,似乎进行类型切换的成本从 Go 1.4(我假设使用 siritinga)到 Go 1.6(我正在使用)显着下降:从 5 到 6 次类型切换慢到不到 2 倍,类型断言没有减速(有或没有检查)。

【讨论】:

有趣。我不知道发生了什么。也许有人可以提供一些文档来解释这种性能提升? @Ted 你确定你使用的是 Go 1.6 吗? 1.7 中添加的 SSA 可能负责对此进行优化。 @physphun Go 1.7 在我发布这个答案时没有发布,所以是的:-) 啊!只是检查。这种性能提升真的让人难以置信。 性能问题是由于 Go 的内联编译。 Go 不能内联接口函数调用,也不能内联类型切换(但现在可以,因为 Go1.9 可以这样做)。在inc的声明前添加注释//go:noinline(不确定Go1.6是否支持),再测试,结果几乎一样。【参考方案6】:

在你的

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

如果您不需要SomeCustomType.Fieldsfmt.Println(v) 中的方法,请执行

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

应该快大约两倍

【讨论】:

你能发布你的基准分数吗?在 Go 1.9 上测试你的建议会使情况变得更糟。谢谢

以上是关于类型断言/类型切换是不是性能不佳/在 Go 中运行缓慢?的主要内容,如果未能解决你的问题,请参考以下文章

go语言接口断言

Go 编码规范建议

Go 编码规范建议

Go语言的断言返回值和类型转换的区别

golang 使用接口和类型断言在Go中使用任意键名解析JSON对象

Golang关于Go中的类型转换