Pull to refresh

Comments 14

Я вот только не очень понял, почему именно с конкурентным сборщиком мусора стало нельзя совать значения в интерфейсы «как есть»
Там есть ссылка на github. Или ты не очень понял пояснение по ссылке?
В принципе, по ссылке все более-менее понятно, да. Но на первый взгляд кажется непонятным, какая связь между хранением данных в интерфейсах и конкурентным сборщиком мусора.
Думаю, было бы полезнее ответить: первое слово (word в оригинале) это тип данных, а второе либо указатель, либо данные. Но поскольку атомарности чтения нет, может сложиться ситуация, когда будет прочитан тип (первое слово), но перед чтением второго слова тип поменяется, тогда может произойти, например, обращение к данным как к указателю. Чтение двух слов атомарно поддерживается не на всех платформах.

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

Спасибо за перевод и ссылки!


Возник следующий вопрос (больше даже относящийся к статье Russ Cox):


package main

import (
        "testing"
)

type Stringer interface {
        String() string
}

func GetString(s Stringer) string {
        return s.String()
}

type Text struct {
        val string
        pad [24]byte
}

func (t Text) String() string {
        return t.val
}

func BenchmarkGetString(b *testing.B) {
        for i := 0; i < b.N; i++ {
                t := Text{val: "hello"} // Moved to heap, 24 bytes for pad and 8 bytes for string.
                _ = GetString(&t)       // Allocation for Stringer 8 + 8 bytes.
        }
}

Если запустить следующую команду:


go test iface_test.go -gcflags="-m -N -l" -bench=. -benchmem

Мы видим, среди прочего:


./iface_test.go:26: moved to heap: t
...
BenchmarkGetString-4    30000000                56.7 ns/op            48 B/op          1 allocs/op

Т.е. в каждой итерации выделяется память на heap под структуру Text, и интерфейс Stringer. Почему нельзя в данном случае ограничиться стеком?

Привет. Я не специалист в компиляторах, но предполагаю что это возможно. Просто Go-шный escape анализатор этого не умеет пока.

Я взял и упростил твой код до такого, когда никаких строк нет. И даже return value нет. Только вызов функции интерфейса.

$ cat lala_test.go 
package lala

import (
	"testing"
)

type Fooer interface {
	Foo()
}

func GetString(s Fooer) {
	s.Foo()
}

type Text struct {
	val string
	pad [24]byte
}

func (t Text) Foo() {
}

func BenchmarkGetString(b *testing.B) {
	for i := 0; i < b.N; i++ {
		t := Text{val: "hello"} // Moved to heap, 24 bytes for pad and 8 bytes for string.
		GetString(&t)           // Allocation for Stringer 8 + 8 bytes.
	}
}


Если убрать вызов

s.Foo()


из

func GetString(s Fooer)


то аллокации не будет. Предполагаю что любой такой вызов по интерфейсу приведет к escape, т.к. анализатор не знает что именно произойдет в вызываемой функции.

P.S. Чтобы получить больше подробностей, можно передать -m -m (два раза).

Чуть-чуть похожий тикет есть https://github.com/golang/go/issues/17332. Но я бы порекомендовал создать еще один с этим конкретным тест кейсом.

А искать по коду нужно по строке «receiver in indirect call»:

marko@marko-ubuntu:~/go/src ((go1.8.1)) $ ack "receiver in indirect call"
cmd/compile/internal/gc/esc.go
1511:				e.escassignSinkWhy(call, r, "receiver in indirect call")

Еще немного упростил пример:


package main

import (
        "testing"
)

type Fooer interface {
        Foo()
}

type Concrete struct {
        data byte // Just to see in allocation.
}

func (c Concrete) Foo() {}

func HandleFooer(f Fooer) {}

func BenchmarkCallFooer(b *testing.B) {
        for i := 0; i < b.N; i++ {
                c := Concrete{} // Moved to heap.
                Fooer(&c).Foo()
        }
}

func BenchmarkPassFooer(b *testing.B) {
        for i := 0; i < b.N; i++ {
                c := Concrete{} // Not moved to heap.
                HandleFooer(Fooer(&c))
        }
}

Результат:


BenchmarkCallFooer-4    100000000               17.6 ns/op             1 B/op          1 allocs/op
BenchmarkPassFooer-4    500000000                2.99 ns/op            0 B/op          0 allocs/op

Да, видимо, анализатор не учитывает конкретную реализацию Foo() объекта внутри Fooer, и поэтому кладет его на heap.


Попробую создать тикет.


P.S. Спасибо за -m -m! Не знал. =)

Кинь линк на тикет, как создашь! :-)
Смотрите, если eface (interface{}), состоит из двух слов (указатель на структуру описывающую тип и указатель на данные), то в iface (в Вашем случае — Stringer) в себя ещё включает таблицу методов.
В данном случае аллокация, на данный момент неизбежна, так-как в рантайме происходят несколько вещей:
  • Проверка на соответсвие передаваемого типа интерфейсу (наличию методов)
  • Создание интерфейса
  • И самое главное: вызов метода через интерфейс(переходим к объекту в памяти, от него к таблице методов и вызываем)

Сделать это не засунув t в кучу — довольно проблематично, насколько мне известно. (А вот если GetString будет принимать напрямую Text, а не интерфейс, то аллокаций не будет)
Другое дело, что в данном примере t — статичен и можно было произвести оптимизацию, но, я достаточно плохо разбираюсь в компиляторах, что бы оценить сложность таких оптимизаций
Sign up to leave a comment.