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 !ок {
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 !ок {
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 і у мене з'явилося дивне поведінка в коді!

ССЗБ

UPD:
Про можливі проблеми, якщо не подумати:
gist.github.com/t0pep0/a14f56c8fde80a3b5e351c44c3584238
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.