четверг, 7 октября 2021 г.

Пустой интерфейс в Golang

Пустой интерфейс может использоваться для хранения любых данных, и он может быть полезным параметром, поскольку может работать с любым типом. Чтобы понять, как работает пустой интерфейс и как он может содержать любой тип, мы должны сначала понять концепцию, лежащую в его основе.

Пустой интерфейс

Вот хорошее определение пустого интерфейса:

Интерфейс - это два компонента: это набор методов и тип.

Тип interface{} - это интерфейс, не имеющий методов. Поскольку в Go нет ключевого слова implements (реализует), все типы реализуют по крайней мере ноль методов, и удовлетворение интерфейса выполняется автоматически, все типы удовлетворяют пустой интерфейс. Следовательно, метод с пустым интерфейсом в качестве аргумента может принимать любой тип. Go перейдет к преобразованию в тип интерфейса, который будет выполнять эту функцию.

Расс Кокс, один из ведущих разработчиков Go, написал статью о внутреннем представлении интерфейсов и объяснил, что интерфейс состоит из двух слов:

  • указатель на информацию о сохраненном типе
  • указатель на связанные данные

Эта было в 2009 году, когда среда выполнения была написана на C.

Среда выполнения теперь написана на Go, но представление осталось прежним. Мы можем убедиться в этом, распечатав пустой интерфейс:

package main

func main() {
    var myVar int32 = 1
    printInterface(myVar)
}

//go:noinline
func printInterface(val interface{}) {
    println(val)
}

Вывод:

(0x459dc0,0xc00003476c)

Оба адреса представляют собой два указателя на тип информации и значение.

Базовая структура

Базовое представление пустого интерфейса задокументировано в пакете reflect:

type emptyInterface struct {
   typ  *rtype            // слово 1 с описанием типа
   word unsafe.Pointer    // слово 2 со значением
}

Как объяснялось ранее, мы ясно видим, что пустой интерфейс имеет слово описания типа, за которым следует слово, содержащее данные.

Структура rtype содержит основу описания типа:

type rtype struct {
	size       uintptr
	ptrdata    uintptr // количество байтов в типе, которое может содержать указатели
	hash       uint32  // хэш типа; избегает вычислений в хэш-таблицах
	tflag      tflag   // флаги дополнительной информации типа
	align      uint8   // выравнивание переменной с этим типом
	fieldAlign uint8   // выравнивание поля структуры с этим типом
	kind       uint8   // перечисление для C
	// функция для сравнения объектов этого типа
	// (ptr to object A, ptr to object B) -> ==?
	equal     func(unsafe.Pointer, unsafe.Pointer) bool
	gcdata    *byte   // данные сборки мусора
	str       nameOff // строковая форма
	ptrToThis typeOff //  тип для указателя на этот тип, может быть нулевым
}

Среди этих полей некоторые довольно простые и хорошо известные:

  • size - это размер в байтах
  • kind содержит тип: int8, int16, bool и т. д.
  • align - это выравнивание переменной с этим типом

В зависимости от типа, встроенного в пустой интерфейс, мы можем сопоставить экспортируемые поля или перечислить методы:

// structType представляет struct тип.
type structType struct {
	rtype
	pkgPath name
	fields  []structField
}

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

Давайте теперь посмотрим, какие виды конверсии действительно возможны из пустого интерфейса.

Конверсии

Давайте попробуем простую программу, которая использует пустой интерфейс с неправильным преобразованием:

package main

func main() {
    var myVar int32 = 1
    printInterface(myVar)
}

//go:noinline
func printInterface(val interface{}) {
    n := val.(int64)
    println(n)
}

Хотя преобразование из int32 в int64 допустимо, программа запаникует:

panic: interface conversion: interface {} is int32, not int64

goroutine 1 [running]:
main.printInterface(0xe49f20, 0xc00002ff74)
	/home/myuser/main.go:10 +0x85
main.main()
	/home/myuser/main.go:5 +0x46

Давайте сгенерируем asm-код, чтобы увидеть, какая проверка выполняется Go:

...
0x0024 00036 (/home/myuser/main.go:10)  LEAQ    type.int64(SB), AX // Шаг 1
0x002b 00043 (/home/myuser/main.go:10)  MOVQ    "".val+48(SP), CX
0x0030 00048 (/home/myuser/main.go:10)  CMPQ    AX, CX
0x0033 00051 (/home/myuser/main.go:10)  JNE     105                // Шаг 2
0x0035 00053 (/home/myuser/main.go:10)  MOVQ    "".val+56(SP), AX
0x003a 00058 (/home/myuser/main.go:10)  MOVQ    (AX), AX
0x003d 00061 (/home/myuser/main.go:10)  MOVQ    AX, "".n+24(SP)
0x0042 00066 (/home/myuser/main.go:11)  PCDATA  $1, $1
0x0042 00066 (/home/myuser/main.go:11)  CALL    runtime.printlock(SB)
0x0047 00071 (/home/myuser/main.go:11)  MOVQ    "".n+24(SP), AX
0x004c 00076 (/home/myuser/main.go:11)  MOVQ    AX, (SP)
0x0050 00080 (/home/myuser/main.go:11)  CALL    runtime.printint(SB)
0x0055 00085 (/home/myuser/main.go:11)  CALL    runtime.printnl(SB)
0x005a 00090 (/home/myuser/main.go:11)  CALL    runtime.printunlock(SB)
0x005f 00095 (/home/myuser/main.go:12)  MOVQ    32(SP), BP
0x0064 00100 (/home/myuser/main.go:12)  ADDQ    $40, SP
0x0068 00104 (/home/myuser/main.go:12)  RET
0x0069 00105 (/home/myuser/main.go:10)  MOVQ    CX, (SP)                
0x006d 00109 (/home/myuser/main.go:10)  MOVQ    AX, 8(SP)
0x0072 00114 (/home/myuser/main.go:10)  LEAQ    type.interface {}(SB), AX
0x0079 00121 (/home/myuser/main.go:10)  MOVQ    AX, 16(SP)
0x007e 00126 (/home/myuser/main.go:10)  NOP
0x0080 00128 (/home/myuser/main.go:10)  CALL    runtime.panicdottypeE(SB) // Шаг 3
0x0085 00133 (/home/myuser/main.go:10)  XCHGL   AX, AX                    // Шаг 4
0x0086 00134 (/home/myuser/main.go:10)  NOP
0x0086 00134 (/home/myuser/main.go:9)   PCDATA  $1, $-1
0x0086 00134 (/home/myuser/main.go:9)   PCDATA  $0, $-2
0x0086 00134 (/home/myuser/main.go:9)   CALL    runtime.morestack_noctxt(SB) 
0x008b 00139 (/home/myuser/main.go:9)   PCDATA  $0, $-1
0x008b 00139 (/home/myuser/main.go:9)   JMP     0
...

Вот несколько шагов:

  • Шаг 1: сравнить (инструкция CMPQ) тип int64 (загруженный инструкцией LEAQ, Load Effective Address) с внутренним типом пустого интерфейса (инструкция MOVQ, которая считывает память со смещением 48 байтов из сегмента памяти пустого интерфейса)
  • Шаг 2: инструкция JNE "Перейти, если не равно" перейдет к сгенерированным инструкциям, которые будут обрабатывать ошибку на шаге 3.
  • Шаг 3: код запаникует и выдаст сообщение об ошибке, которое мы видели ранее
  • Шаг 4: это конец инструкций по ошибке. На эту конкретную инструкцию ссылается сообщение об ошибке, которое показывает инструкцию: main.go: 10 + 0x85

Любое преобразование из внутреннего типа пустого интерфейса должно выполняться после преобразования исходного типа. Это преобразование в пустой интерфейс, а затем обратно в исходный тип, требует затрат для вашей программы. Давайте проведем несколько тестов, чтобы получить общее представление об этом.

Производительность

Вот два теста (Golang 1.16.2). Один с копией структуры, а другой с использованием пустого интерфейса:

package maintest

import (
    "testing"
)

var instance StructWithFields

type StructWithFields struct {
    field1 int
    field2 string
    field3 float32
    field4 float64
    field5 int32
    field6 bool
    field7 uint64
    field8 *string
    field9 uint16
}

//go:noinline
func emptyInterface(i interface {}) {
    str := i.(StructWithFields)
    instance = str
}

//go:noinline
func withType(str StructWithFields) {
    instance = str
}

func BenchmarkWithType(b *testing.B) {
    str := StructWithFields{field1: 1, field2: "string", field8: new(string)}
    for i := 0; i < b.N; i++ {
        withType(str)
    }
}

func BenchmarkWithEmptyInterface(b *testing.B) {
    str := StructWithFields{field1: 1, field2: "string", field8: new(string)}
    for i := 0; i < b.N; i++ {
        emptyInterface(str)
    }
}

Вот результаты:

BenchmarkWithType-4             	74843296  16.75 ns/op
BenchmarkWithEmptyInterface-4   	48790801  33.27 ns/op

Для двойного преобразования типа в пустой интерфейс, а затем обратно в тип требуется на 16 наносекунд больше, чем копирование структуры.

Хорошим решением было бы использовать указатель и преобразовать обратно в тот же указатель на структуру.

package maintest

import (
    "testing"
)

var instance *StructWithFields

type StructWithFields struct {
    field1 int
    field2 string
    field3 float32
    field4 float64
    field5 int32
    field6 bool
    field7 uint64
    field8 *string
    field9 uint16
}

//go:noinline
func emptyInterface(i interface {}) {
    str := i.(*StructWithFields)
    instance = str
}

//go:noinline
func withType(str *StructWithFields) {
    instance = str
}

func BenchmarkWithType(b *testing.B) {
    str := StructWithFields{field1: 1, field2: "string", field8: new(string)}
    for i := 0; i < b.N; i++ {
        withType(&str)
    }
}

func BenchmarkWithEmptyInterface(b *testing.B) {
    str := StructWithFields{field1: 1, field2: "string", field8: new(string)}
    for i := 0; i < b.N; i++ {
        emptyInterface(&str)
    }
}

Результаты теперь совсем другие:

BenchmarkWithType-4             	187950846  6.374 ns/op
BenchmarkWithEmptyInterface-4   	150776062  8.141 ns/op

Что касается базового типа, такого как int или string, производительность немного отличается:

package maintest

import (
    "testing"
)

var intVar int
var stringVar string

//go:noinline
func emptyInterfaceInt(i interface {}) {
    str := i.(int)
    intVar = str
}

//go:noinline
func withTypeInt(str int) {
    intVar = str
}

//go:noinline
func emptyInterfaceString(i interface {}) {
    str := i.(string)
    stringVar = str
}

//go:noinline
func withTypeString(str string) {
    stringVar = str
}

func BenchmarkWithTypeInt(b *testing.B) {
    str := 123
    for i := 0; i < b.N; i++ {
        withTypeInt(str)
    }
}

func BenchmarkWithEmptyInterfaceInt(b *testing.B) {
    str := 123
    for i := 0; i < b.N; i++ {
        emptyInterfaceInt(str)
    }
}

func BenchmarkWithTypeString(b *testing.B) {
    str := "123"
    for i := 0; i < b.N; i++ {
        withTypeString(str)
    }
}

func BenchmarkWithEmptyInterfaceString(b *testing.B) {
    str := "123"
    for i := 0; i < b.N; i++ {
        emptyInterfaceString(str)
    }
}

BenchmarkWithTypeInt-4                	228122656  5.319 ns/op
BenchmarkWithEmptyInterfaceInt-4      	144611329  8.028 ns/op
BenchmarkWithTypeString-4             	158939217  7.508 ns/op
BenchmarkWithEmptyInterfaceString-4   	100000000  10.10 ns/op

Пустой интерфейс в большинстве случаев может быть полезен в ваших приложениях, при правильном использовании и с осторожностью, учитывая его влияние на производительность.


Читайте также:


Комментариев нет:

Отправить комментарий