go variant底层原理深入解析

2022-11-24 22:01:22
目录
varintbenchmarksstructvariant为什么 variant 要比 plainstruct 快variant 可能的优化?

varint

今天本来在研究>

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

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

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

github.com/tigrannajar…

benchmarks

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

    struct

    struct>

    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

    一个>

      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 创建 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的资料请关注易采站长站其它相关文章!