Всем привет. В дополнении к моей предыдущей статье был интересный диалог с kirill_danshin.
В конце концов мы это сделали. Встречайте — efaceconv, тулза для go generate, с помощью которой можно приводить типы из interface{} без аллокаций и в ~4 раза быстрее.
https://github.com/t0pep0/efaceconv
Как с этим работать?
Всё просто:
- Устанавливаете: go get github.com/t0pep0/efaceconv
- Добавляете в Ваши исходники вызов go generate //go:generate efaceconv
- Описывайте типы, конвертация которых необходим (об этом ниже)
- Запускаете 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/
Добавить комментарий