Всем привет. В дополнении к моей предыдущей статье был интересный диалог с
kirill_danshin. В конце концов мы это сделали. Встречайте — efaceconv, тулза для go generate, с помощью которой можно приводить типы из interface{} без аллокаций и в ~4 раза быстрее.
Как с этим работать?
Всё просто:
- Устанавливаете: 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
Злодеи! Я сделал всё как написано для мутабельного типа и у меня появилось странное поведение в коде!
ССЗБ
UPD: Про возможные проблемы, если не подумать.
Автор: t0pep0