写点什么

泛型真的会让程序变慢吗?(Go1.18 新特性)

作者:蔡超
  • 2022 年 4 月 16 日
  • 本文字数:3039 字

    阅读完需:约 10 分钟

泛型真的会让程序变慢吗?(Go1.18新特性)

基本语法

定义可接受类型列表(constraint)

简单来说,Go 的泛型就是将参数类型定义为一个可接受类型的列表(称为 constraint),直接上例子。

例 1: 方法定义上使用范型

func Max[T int|int32|int64|float32|float64] (a, b T) T {  if a > b {    return a  }  return b}
复制代码

例 2: 结构类型定义上使用范型

type Node[T any] struct{  Value T    Next *Node[T]}
func (n *Node[T]) Append(v T) *Node[T]{ next := &Node[T]{v,nil} n.Next = next return next}
复制代码

以上例子中使用了一个预置的 constraint “any”,这里 any 就相当于 interface{},表示任何类型。

另一个预置的 constraint 是 comparable,代表可以支持==和!=比较的类型。

让列表更简单

用 “~”简化衍生类型的声明 

上面范型函数只能接受 constraint 列表中的类型作为调用的参数,包括这些类型的别名。但对于列表中类型的衍生类型就不能接受了。

例如:

type MyInt int64func TestMax(t *testing.T) {  var a,b MyInt = 2,3  Max(a,b)}
复制代码

编译以上程序会引发编译错误,在 Go 中为了简化扩展类型的定义,我们可以通过"~"来表示支持类型扩展类型。利用这种方式修改方法定义即可修复上面的编译问题。

func Max[T int|int32|~int64|float32|float64] (a, b T) T {  if a > b {    return a  }    return b}
复制代码

定义 constraint 类型

虽然有了上面的对于扩展类型的简化定义方式,但还是会很容易产生很长的 contraint 类型列表,而且这个列表往往会需要在很多方法定义中重复。

为了简化和复用复杂的类型列表(constraint)声明,在 Go1.18 中可以通过 interface 来定义一个 constraint 类型。


1 interface 中定义 constraint 类型支持的方法

这种方式下方法所接受的类型必须要实现了该接口定义,即实现了该接口中包含的方法。

type Addable interface {  Add(a Addable) Addable}
type type1 struct { value int64}
func (t type1) Add(a Addable) Addable { b,_:=a.(type1) return type1{t.value + b.value}}
type type2 struct { value float64}
func (t type2) Add(a Addable) Addable { b,_:=a.(type2) return type2{t.value + b.value}}
func AddGen[T Addable](a T, b T) Addable { return a.Add(b) }
复制代码

这里你可能会有一个问题,那么既然有使用了 interface,那完全可以使用多态--以接口 Addable 作为方法的参数,定义如下:


func AddPoly(a Addable, b Addable) Addable {    return a.Add(b)}
复制代码

问题来了,这里的这个 AddPoly 和上面使用范型定义的 AddGen 一样吗?

答案是不一样的,

AddGen 的传入参数(a,b) 必须是同一类型,而不仅仅是都实现了 Addable 接口,上列中要么都是 type1,要么就都是 type2。

而 AddPoly 方法就不同了,传入参数只要是实现了 Addable 就行,即(a,b)可以一个是 type1,另一个是 type2。


2 interface 中直接包含可接受类型列表

这里就比较直接可以在一个 interface 中定义可接受类型的列表

type SupportTypes interface{  int | int32 | ~int64 | float32 | float64}
func Max[T SupportTypes] (a, b T) T { if a > b { return a } return b}
复制代码

这个 constraint interface 可以在不同的地方被复用,Google 在"golang.org/x/exp/constraints"包中也贴心的提供了很多的常用的 constraint interface。


关于性能

Go 的泛型实现影响性能吗?这是很多程序员最关心的问题。我们直接通过 Benchmark 程序来看结果。

1 constraint 为基础计算类型

type SupportTypes interface{  int | int32 | ~int64 | float32 | float64}
func Max[T int | int32 | ~int64 | float32 | float64] (a, b T) T { if a > b { return a } return b}
func MaxI[T SupportTypes] (a, b T) T { if a > b { return a } return b}
func MaxInt(a, b int64) int64 { if a > b { return a } return b}
// Benchmarkfunc BenchmarkMaxInt(b *testing.B) { a := rand.Int63n(1000) c := rand.Int63n(1000) b.ResetTimer() for i:=0;i<b.N;i++{ _ = MaxInt(a,c) }}
func BenchmarkMaxGen(b *testing.B) { a := rand.Int63n(1000) c := rand.Int63n(1000) b.ResetTimer() for i:=0;i<b.N;i++{ _ = Max(a,c) }}
func BenchmarkMaxIGen(b *testing.B) { a := rand.Int63n(1000) c := rand.Int63n(1000) b.ResetTimer() for i:=0;i<b.N;i++{ _ = MaxI(a,c) }}
复制代码

Benchmark 结果

cpu: Intel(R) Core(TM) i5-3317U CPU @ 1.70GHzBenchmarkMaxIntBenchmarkMaxInt-4      1000000000           0.4331 ns/opBenchmarkMaxGenBenchmarkMaxGen-4      1000000000           0.4276 ns/opBenchmarkMaxIGenBenchmarkMaxIGen-4     1000000000           0.4353 ns/op
复制代码


可以看出 Go 泛型对于基本类型的影响几乎没有。


2 constraint 为接口定义的类型(这里指类型需要实现接口中的方法)

type Addable interface {  Add(a Addable) Addable}
type type1 struct { value int64}
func (t type1) Add(a Addable) Addable { b,_:=a.(type1) return type1{t.value + b.value}}
type type2 struct { value float64}
func (t type2) Add(a Addable) Addable { b,_:=a.(type2) return type2{t.value + b.value}}
func AddPoly(a Addable, b Addable) Addable { return a.Add(b)}
func AddGen[T Addable](a T, b T) Addable { return a.Add(b)
}
var a type1 = type1{1}var c type1 = type1{1}
func BenchmarkAddPoly(b *testing.B) { b.ResetTimer() for i:=0;i<b.N;i++{ AddPoly(a,c) }}
func BenchmarkAddGen(b *testing.B) { b.ResetTimer() for i:=0;i<b.N;i++{ AddGen(a,c) }}
复制代码

Benchmark 的结果

cpu: Intel(R) Core(TM) i5-3317U CPU @ 1.70GHzBenchmarkAddPolyBenchmarkAddPoly-4        146483520           8.360 ns/opBenchmarkAddGenBenchmarkAddGen-4     100000000          11.83 ns/op
复制代码

这种情况下范型的性能明显要差于原有的多态实现。所以,类似情况最好不要使用范型来替代多态,不仅没有简化程序,反而对性能有一定影响。


最后,写给 Java 程序员“关于类型擦除”

Java 程序员都知道,Java 的泛型实现是编译时期的,在运行时是被擦除的,例如:

// Example generic classpublic class ArrayList<E extends Number> {    // ...};
ArrayList<Integer> li = new ArrayList<Integer>();ArrayList<Float> lf = new ArrayList<Float>();if (li.getClass() == lf.getClass()) { // 这里是true System.out.println("Equal");}
复制代码

由于类型擦除的缘故,li.getClass() == lf.getClass() 的结果是 true。


而 Go 的泛型则不是通过这种方式实现的,Go 采用了一种不完全的单态化(monomorphization)称之为“GCShape stenciling with Dictionaries” 。这种实现方式也导致了我们在前面看到的性能测试的结果,这里我就不展开了,未来在别的文章中和大家讨论。

type Numeric interface {    int | int64 | uint | float32 | float64}
type ArrayList[T Numeric] struct {}
var li ArrayList[int64]var lf ArrayList[float64]if reflect.TypeOf(li) == reflect.TypeOf(lf) { // 这里是false fmt.Printf("they're equal") return}
复制代码

由于没有类型擦除,reflect.TypeOf(li) == reflect.TypeOf(lf)结果返回为 false。


更多关于 Go 语言的编程分享可以观看我的视频 《Go语言入门到实践》


发布于: 2022 年 04 月 16 日阅读数: 71
用户头像

蔡超

关注

程序员是我的终身职业 2017.10.19 加入

蔡超,SpotMax创始人,Mobvista 集团副总裁/首席架构师,前亚马逊(中国)首席架构师,前HP(中国)软件架构师

评论

发布
暂无评论
泛型真的会让程序变慢吗?(Go1.18新特性)_golang_蔡超_InfoQ写作社区