go variant底层原理深入解析

varint

今天本来在研究 OpenTelemetry 的基准性能测试 github.com/zdyj3170101…,测试不同网络协议:grpc, grpc-stream, http, websocket 在发送不同大小数据包时消耗 cpu,吞吐 的区别,由 tigrannajaryan 这位大神所写。

好奇翻了翻该大神的 github 仓库,发现了一个同样神奇的库。

这个库也是基准的性能测试,用来测试 go 中不同方法实现的 多类型变量 在消耗 cpu 以及 内存上的区别。

旨在实现一个能存储多类型变量并且具有最小 cpu 消耗以及 内存消耗的数据结构。

github.com/tigrannajar…

benchmarks

  • Interface: 接口
  • struct:struct,多个 field 存放不同类型的结构体
  • variant:该库的时间

struct

struct 是一个结构体,typ 表示当前结构体的类型,不同的 field 分别存储不同的类型。

type Variant struct {
   typ   variant.Type
   bytes []byte
   str   string
   i     int
   f     float64
}

struct 还有两种不同的类型。

根据是否返回指针分别为 plainstruct 和 ptrstruct。

而 ptrstruct 相比 plainstruct 就多出一次内存分配,以及增加 cpu 耗时(栈上内存分配几个移位指令就能完成)。

func StringVariant(v string) Variant {
  return Variant{typ: variant.TypeString, str: v}
} 
​
func StringVariant(v string) *Variant {
  return &Variant{typ: variant.TypeString, str: v}
}

进行 benchmark 后发现 plainstruct 已经 0 byte 分配了,我也想不出还有其他的优化思路。

yangjie05-mac:plainstruct jie.yang05$ go test -bench=. -benchmem plainstruct_test.go  plainstruct.go
Variant size=64 bytes
goos: darwin
goarch: amd64
cpu: VirtualApple @ 2.50GHz
BenchmarkVariantIntGet-10                       1000000000               0.3111 ns/op          0 B/op          0 allocs/op
BenchmarkVariantFloat64Get-10                   1000000000               0.3117 ns/op          0 B/op          0 allocs/op
BenchmarkVariantIntTypeAndGet-10                1000000000               0.3189 ns/op          0 B/op          0 allocs/op
BenchmarkVariantStringTypeAndGet-10             141588165                8.435 ns/op           0 B/op          0 allocs/op
BenchmarkVariantBytesTypeAndGet-10              140932470                8.465 ns/op           0 B/op          0 allocs/op
BenchmarkVariantIntSliceGetAll-10                7293846               165.7 ns/op           640 B/op          1 allocs/op
BenchmarkVariantIntSliceTypeAndGetAll-10         7491408               170.6 ns/op           640 B/op          1 allocs/op
BenchmarkVariantStringSliceTypeAndGetAll-10      7061575               170.1 ns/op           640 B/op          1 allocs/op
​

variant

一个 variant 由指向真实数据的指针 ptr,一个紧凑的 lenandtype 同时表示长度和类型,这个数据结构还根据不同位的系统做了优化,以及 capOrVal(在slice类型数据时,就是 cap,非slice类型数据时就是val )。

  • 32位系统下,type 占3位,len 用29位表示
  • 64 位系统下,type占3位,len用63位表示。

Variant 设计主要是为了同时满足存储 float64 和 string 的需求。 因为 float64 的存在,必须要有一个 int64 类型的字段存储 float64 的值。 而 string 的 len 是int类型的字段,就不需要用int64。

type Variant struct {
  // Pointer to the slice start for slice-based types.
  ptr unsafe.Pointer
​
  // Len and Type fields.
  // Type uses `typeFieldBitCount` least significant bits, Len uses the rest.
  // Len is used only for the slice-based types.
  lenAndType int
​
  // Capacity for slice-based types, or the value for other types. For Float64Val type
  // contains the 64 bits of the floating point value.
  capOrVal int64
}

比如创建一个string的时候,ptr 中存放指向数据的指针,而lenAndType 中存储slice的长度以及 type。 ``

// NewString creates a Variant of TypeString type.
func NewString(v string) Variant {
  hdr := (*reflect.StringHeader)(unsafe.Pointer(&v))
  if hdr.Len > maxSliceLen {
    panic("maximum len exceeded")
  }
​
  return Variant{
    ptr:        unsafe.Pointer(hdr.Data),
    lenAndType: (hdr.Len << typeFieldBitCount) | int(TypeString),
  }
}

为什么 variant 要比 plainstruct 快

分别测试 variant 和 plainstruct 创建 string 的性能:

func createVariantString() Variant { // 防止编译优化掉?
   for i := 0; i < 1; i++ {
      return StringVariant(testutil.StrMagicVal)
   }
   return StringVariant("def")
}
func BenchmarkVariantStringTypeAndGet(b *testing.B) {
   for i := 0; i < b.N; i++ {
      v := createVariantString()
      if v.Type() == variant.TypeString {
         if v.String() == "" {
            panic("empty string")
         }
      } else {
         panic("invalid type")
      }
   }
}

使用 go tool 做性能测试,并查看plainstruct的profile文件:

go test -o=bin -bench=. -v -test.cpuprofile=cpuprofile plainstruct_test.go plainstruct.go
go tool pprof -http=:  bin cpuprofile

同理 variant:

go test -o=bin -bench=. -v -test.cpuprofile=cpuprofile variant_test.go variant.go variant_64.go
 go tool pprof -http=:  bin cpuprofile

variant 的汇编:

plainstruct的汇编:

主要区别还是plainstrutc的指令数太多,因为struct的字段更多。

variant 可能的优化?

variant 其实这里还有一个优化的方向,就是在 32 位机器存储 float64 的时候。 将 float64 拆成两个 int32,分别用 ptr 和 capOrVal 来存储。 这样在 32位系统下,capOrVal 可以由 int64 变成 int,节省了 4 个字节。

type Variant struct {
  // Pointer to the slice start for slice-based types.
  ptr unsafe.Pointer
  // Len and Type fields.
  // Type uses `typeFieldBitCount` least significant bits, Len uses the rest.
  // Len is used only for the slice-based types.
  lenAndType int
  capOrVal int
}

以上就是go variant原理深入解析的详细内容,更多关于go variant的资料请关注其它相关文章!

若文章对您有帮助,帮忙点个赞!

0
-5
发布时间 2022-11-24 06:03:36
0 条回复(回复会通过微信通知作者)
点击加载更多评论
登录 后再进行评论
(微信扫码即可登录,无需注册)