📜 ⬆️ ⬇️

Type paper dissertation without allocations

Hello. In addition to my previous article, there was an interesting dialogue with kirill_danshin . In the end we did it. Meet - efaceconv , tool for go generate, with which you can bring types from interface {} without allocations and ~ 4 times faster.

How to work with it?


It's simple:

  1. Install: go get github.com/t0pep0/efaceconv
  2. Add go generate to your sources go generate // go: generate efaceconv
  3. Describe the types, the conversion of which is necessary (more on that below)
  4. Run go generate and enjoy (ZY as a bonus - tests with 100% coverage on the generated code)

How to describe types


Again, everything is simple. Types are described in the comments. The description format is as follows:

//ec: ( )::  

Example:
')
 //ec:net/http:http.ResponseWriter:ResWriter //ec::string:String 

After go generate will work in the package directory there will be 2 new files:

efaceconv_generated.go - generated methods
efaceconv_generated_test.go - tests and benchmarks for them

Example demo.go:

 //go:generate efaceconv //ec::string:String //ec::[]uint64:SUint64 package demo 

efaceconv_generated.go:

 //generated by efaceconv DO NOT EDIT! package demo import ( "github.com/t0pep0/efaceconv/ecutils" ) var ( _StringKind uintptr _SUint64Kind uintptr ) func init(){ var sString string _StringKind = ecutils.GetKind(sString) var sSUint64 []uint64 _SUint64Kind = ecutils.GetKind(sSUint64) } // Eface2String returns pointer to string and true if arg is a string // or nil and false otherwise func Eface2String(arg interface{}) (*string, bool) { if ecutils.GetKind(arg) == _StringKind { return (*string)(ecutils.GetDataPtr(arg)), true } return nil, false } // Eface2SUint64 returns pointer to []uint64 and true if arg is a string // or nil and false otherwise func Eface2SUint64(arg interface{}) (*[]uint64, bool) { if ecutils.GetKind(arg) == _SUint64Kind { return (*[]uint64)(ecutils.GetDataPtr(arg)), true } return nil, false } 

efaceconv_generated_test.go:

 //generated by efaceconv DO NOT EDIT! package demo import ( "reflect" "testing" ) func TestEface2String(t *testing.T) { var String string res, ok := Eface2String(String) if !ok { t.Error("Wrong type!") } if !reflect.DeepEqual(*res, String) { t.Error("Not equal") } _, ok = Eface2String(ok) if ok { t.Error("Wrong type!") } } func benchmarkEface2String(b *testing.B) { var String string var v *string var ok bool for n := 0; n < bN; n++ { v, ok = Eface2String(String) } b.Log(v, ok) //For don't use compiler optimization } func _StringClassic(arg interface{}) (v string, ok bool) { v, ok = arg.(string) return v, ok } func benchmarkStringClassic(b *testing.B) { var String string var v string var ok bool for n := 0; n < bN; n++ { v, ok = _StringClassic(String) } b.Log(v, ok) //For don't use compiler optimization } func TestEface2SUint64(t *testing.T) { var SUint64 []uint64 res, ok := Eface2SUint64(SUint64) if !ok { t.Error("Wrong type!") } if !reflect.DeepEqual(*res, SUint64) { t.Error("Not equal") } _, ok = Eface2SUint64(ok) if ok { t.Error("Wrong type!") } } func benchmarkEface2SUint64(b *testing.B) { var SUint64 []uint64 var v *[]uint64 var ok bool for n := 0; n < bN; n++ { v, ok = Eface2SUint64(SUint64) } b.Log(v, ok) //For don't use compiler optimization } func _SUint64Classic(arg interface{}) (v []uint64, ok bool) { v, ok = arg.([]uint64) return v, ok } func benchmarkSUint64Classic(b *testing.B) { var SUint64 []uint64 var v []uint64 var ok bool for n := 0; n < bN; n++ { v, ok = _SUint64Classic(SUint64) } b.Log(v, ok) //For don't use compiler optimization } 

As you can see, efaceconv generates methods like

 Eface2<  >(arg interface{}) (*< >, bool) 

Together with the documentation to them, tests and benchmarks, benchmarks are also generated for the classical cast type (v, ok: = arg. (Type)) so that you can compare the performance gain.

How it works


As we know (from my previous article) empty interfaces are simply a structure with two fields - * TypeDescriptor and a pointer to an object. TypeDescriptor is generated at runtime, in a single instance for each type, respectively, for all empty interfaces from one type * TypeDescriptor will be equal and there is no need to parse TypeDescriptor itself. We can simply compare the numeric value of the pointer, and already if they match, we can return a pointer to the object, being sure that it has the type we need.

Why is it faster than the standard method?


The standard type conversion method after comparing TypeDescriptors copies data by value, we just give a pointer to the original object.

Then why did not the authors of Go?


It's not safe. More precisely not so, it is safe exactly as long as you use immutable data types (strings, slices, arrays). In the case of using non-immutable data types, side-by-side writing is not neat.

Somewhere already in use?


kirill_danshin implemented the first version in his production, I don’t know the results reliably, but judging by the commits he is satisfied

Where are the numbers? Pro performance and allocation


 BenchmarkEface2SByte-4 100000000 11.8 ns/op 0 B/op 0 allocs/op --- BENCH: BenchmarkEface2SByte-4 efaceconv_generated_test.go:33: &[] true efaceconv_generated_test.go:33: &[] true efaceconv_generated_test.go:33: &[] true efaceconv_generated_test.go:33: &[] true efaceconv_generated_test.go:33: &[] true BenchmarkSByteClassic-4 30000000 50.4 ns/op 32 B/op 1 allocs/op --- BENCH: BenchmarkSByteClassic-4 efaceconv_generated_test.go:48: [] true efaceconv_generated_test.go:48: [] true efaceconv_generated_test.go:48: [] true efaceconv_generated_test.go:48: [] true efaceconv_generated_test.go:48: [] true BenchmarkEface2String-4 100000000 11.1 ns/op 0 B/op 0 allocs/op --- BENCH: BenchmarkEface2String-4 efaceconv_generated_test.go:76: 0xc42003fee8 true efaceconv_generated_test.go:76: 0xc420043ea8 true efaceconv_generated_test.go:76: 0xc420043ea8 true efaceconv_generated_test.go:76: 0xc420043ea8 true efaceconv_generated_test.go:76: 0xc420043ea8 true BenchmarkStringClassic-4 30000000 45.3 ns/op 16 B/op 1 allocs/op --- BENCH: BenchmarkStringClassic-4 efaceconv_generated_test.go:91: true efaceconv_generated_test.go:91: true efaceconv_generated_test.go:91: true efaceconv_generated_test.go:91: true efaceconv_generated_test.go:91: true BenchmarkEface2SInt-4 100000000 11.6 ns/op 0 B/op 0 allocs/op --- BENCH: BenchmarkEface2SInt-4 efaceconv_generated_test.go:119: &[] true efaceconv_generated_test.go:119: &[] true efaceconv_generated_test.go:119: &[] true efaceconv_generated_test.go:119: &[] true efaceconv_generated_test.go:119: &[] true BenchmarkSIntClassic-4 30000000 50.5 ns/op 32 B/op 1 allocs/op --- BENCH: BenchmarkSIntClassic-4 efaceconv_generated_test.go:134: [] true efaceconv_generated_test.go:134: [] true efaceconv_generated_test.go:134: [] true efaceconv_generated_test.go:134: [] true efaceconv_generated_test.go:134: [] true PASS 


Villains! I did everything as written for the mutable type and I got strange behavior in the code!


CVDF

UPD: About possible problems, if not to think .

Source: https://habr.com/ru/post/315752/


All Articles