Type assertation without allocations

от автора

Всем привет. В дополнении к моей предыдущей статье был интересный диалог с kirill_danshin.
В конце концов мы это сделали. Встречайте — efaceconv, тулза для go generate, с помощью которой можно приводить типы из interface{} без аллокаций и в ~4 раза быстрее.
https://github.com/t0pep0/efaceconv

Как с этим работать?

Всё просто:

  1. Устанавливаете: go get github.com/t0pep0/efaceconv
  2. Добавляете в Ваши исходники вызов go generate //go:generate efaceconv
  3. Описывайте типы, конвертация которых необходим (об этом ниже)
  4. Запускаете go generate и наслаждаетесь (З.Ы. в качестве бонуса — тесты с 100% покрытием на сгенерированный код)

Как описать типы

Опять таки всё просто. Типы описываются в комментариях. Формат описания вот такой:
//ec: Имя пакета(Если нужно): Тип: Кастомное имя
Пример:

//ec:net/http:http.ResponseWriter:ResWriter //ec::string:String

После того, как go generate отработает в директории пакета появится 2 новых файла:
efaceconv_generated.go — сгенерированные методы
efaceconv_generated_test.go — тесты и бенчмарки для них

Пример 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 < b.N; 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 < b.N; 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 < b.N; 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 < b.N; n++ { 		v, ok = _SUint64Classic(SUint64) 	} 	b.Log(v, ok) //For don't use compiler optimization } 

Как можно увидеть efaceconv генерирует методы вида
Eface2<Наше кастомное имя>(arg interface{}) (*<Наш тип>, bool)
Вместе с документацией к ним, тестами и бенчмарками, также бенчмарки генерируются и для классического типа приведения ( v, ok := arg.(type) ) что бы была возможность сравнить выигрыш в производительности.

Как это работает

Как мы знаем (из моей предыдущей статьи) пустые интерфейсы это просто структура с двумя полями — *TypeDescriptor и указатель на объект. TypeDescriptor генерируется в runtime, в единичном экземпляре для каждого типа, соответственно для всех пустых интерфейсов от одного типа *TypeDescriptor будет равен и нет необходимости разбирать сам TypeDescriptor. Мы можем просто сравнивать числовое значение указателя, а уже при совпадении их можем вернуть указатель на объект будучи уверенными что он имеет нужный нам тип.

Почему это быстрее чем стандартный метод?

Стандартный метод приведения типа после сравнения TypeDescriptor’ов копирует данные по значению, мы же просто отдаем указатель на исходный объект

Тогда почему так не сделали авторы Go?

Это не безопасно. Точнее не так, это безопасно ровно до тех пор, пока вы используете иммутабельные типы данных (строки, слайсы, массивы). В случае использования не иммутабельных типов данных, при не аккуратном написании кода, возможны слайд эффекты.

Где-то уже используется?

kirill_danshin внедрил первую версию у себя в продакшене, о результатах достоверно не знаю, но судя по коммитам он доволен

А где цифры? Про производительность и аллокации

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 

Злодеи! Я сделал всё как написано для int64 и у меня появилось странное поведение в коде!

ССЗБ
ссылка на оригинал статьи https://habrahabr.ru/post/315752/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *