Type assertation without allocations

в 5:56, , рубрики: Go, hack-runtime, runtime, type assertation

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

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

Всё просто:

  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

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

ССЗБ

UPD: Про возможные проблемы, если не подумать.

Автор: t0pep0

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js