[Программирование, Go] Указатель или копия структуры: что выгоднее использовать в Golang?
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Прямо сейчас в OTUS открыт набор на новый поток курса «Разработчик Golang». В преддверии старта курса делимся с вами авторской статьей от нашего коллеги.
Всем привет! Сегодня я хотел бы поговорить об использовании указателя и копии структуры в Golang, и обсудить, какой из вариантов выгоднее по памяти и по производительности.
Что такое структура?
Структура — это тип, который содержит именованные поля. Да, изначально кажется, что это определение объекта или хеш-таблицы, но в Go это структура. Структуры используются для создания кастомных типов данных, например, при получении привычного набора данных из базы данных.
Если вы хотите немного почитать, что из себя представляют структуры, вы можете сделать это здесь.
Что такое указатель?
Некоторые книги вообще предлагают в качестве объяснения спросить у C программиста, что же такое указатели. На самом деле ничего в них такого сложного нет, указатели просто указывают на участок памяти, где хранится значение. Если вам непонятно, что это такое, можете почитать это в go учебнике
Создание данных для тестирования
Давайте создадим структуру для тестирования:
type S struct {
a, b, c int64
d, e, f string
g, h, i float64
}
Ниже у нас представлено создание копии нашей структуры:
func asCopy() S {
return S{
a: 10, b: 20, c: 30,
e: "data", f: "foo",
g: 1.0, h: 2.0, i: 3.0,
}
}
А здесь у нас будет представлено копирование при помощи указателя:
func asPointer() *S {
return &S{
a: 10, b: 20, c: 30,
e: "data", f: "foo",
g: 1.0, h: 2.0, i: 3.0,
}
}
Используя вышеперечисленные функции, мы можем написать два бенчмарка, первый будет основан на передаче структуры как копии:
func BenchmarkMemoryStack(b *testing.B){
var s S
f, err :=os.Create("stack.out")
if err != nil{
panic(err)
}
defer f.Close()
err = trace.Start(f)
if err != nil{
panic(err)
}
for i:= 0; i < b.N; i++{
s = byCopy()
}
trace.Stop()
b.StopTimer()
_ = fmt.Sprintf("%v", s.a)
}
Также мы создадим другой бенчмарк, в котором будем передавать структуры как указатель:
func BenchMemoryHeap(b *testing.B) {
var s *S
f, err := os.Create("heap.out")
if err != nil {
panic(err)
}
defer f.Close()
err = trace.Start(f)
if err != nil {
panic(err)
}
for i := 0; i < b.N; i++ {
s = byPointer()
}
trace.Stop()
b.StopTimer()
_ = fmt.Sprintf("%v", s.a)
}
Давайте запустим наши бенчмарки:
go test ./... -bench=BenchmarkMemoryHeap -benchmem -run=^$ -count=10 > head.txt && benchstat head.txt
go test ./... -bench=BenchmarkMemoryStack -benchmem -run=^$ -count=10 > stack.txt && benchstat stack.txt
Здесь я с приведу статистики, которые у нас получились:
MemoryHeap
name
time/op
MemoryHeap-4
75.0ns ± 5%
name
alloc/op
MemoryHeap-4
96.0B ± 0%
name
allocs/op
MemoryHeap-4
1.00 ± 0%
MemoryStack
name
time/op
MemoryStack-4
8.93ns ± 4%
name
alloc/op
MemoryStack-4
0.00B
name
allocs/op
MemoryStack-4
0.00
Использование копии структуры вместо указателя дает 8-кратный выигрыш в скорости.
Чтобы понять почему, давайте посмотрим на графы, которые были сгенерированы нашим трейсом. Первым пойдет граф, в котором у нас структура сгенерирована как копия:
Потом где структура сгенерирована с помощью указателей:
Первый граф достаточно простой. Там не используется куча, и нет сборщика мусора и дополнительной горутины.
На втором графе, использование указателя заставляет компилятор выкидывать переменную из кучи и направляет в работу наш сборщик мусора. Если мы немного приблизим граф, увидим, что сборщик мусора оказывает значительное влияние на важные части нашего процесса:
Мы можем увидеть, что на этом графе сборщик мусора должен срабатывать каждые 4ms.
Если приблизить опять, мы получим дополнительные детали, что у нас действительно происходит:
Голубая, розовая и красная полоски выражают фазы сборщика мусора, в то время коричневые связаны с выделением кучи (помечено на "runtime.bsweep" на этом графе).
Даже если этот пример покажется вам немного специфичным, он вполне показателен, чтобы дать понять, что стоимость выделения переменной в куче больше, чем в стеке. В нашем примере, код быстрее выделяет структуру в стеке и копирует ее, чем выделяет память в куче и передает адрес.
Если вам не совсем понятно, что из себя представляет стек/куча, но вы хотели бы понимать больше, вы можете прочитать эту замечательнуюстатью на нашем сайте.
Интенсивные вызовы функций
Давайте разберем второй кейс, в котором добавим два пустых метода к нашей структуре с небольшой адаптацией к нашим бенчмаркам.
func (s S) stack(s1 S) {}
func (s *S) heap(s1 *S) {}
Давайте посмотрим на выделение оперативной памяти в нашем стеке, в котором мы создаем структуру и передаем ее как копию:
func BenchMemoryStack(b * testing.B){
var s S
var s1 S
s = byCopy()
s1 = byCopy()
for i :=0; i< b.N; i++{
for i:0; i < 10000; i++{
s.stack(s1)
}
}
}
И бенчмарк для кучи передаст структуру в качестве указателя:
func BenchMemoryHeap(b * testing.B){
var s *S
var s1 *S
s = byPointer()
s1 = byPointer()
for i: = 0; i < b.N; i++{
for i := 0; i < 100000; i++{
s.heap(s1)
}
}
}
Вполне ожидаемо, результаты сейчас достаточно разные:
MemoryHeap
name
time/op
MemoryHeap-4
301µs ± 4%
name
alloc/op
MemoryHeap-4
0.00B
name
allocs/op
MemoryHeap-4
0.00
MemoryStack
name
time/op
MemoryStack-4
595µs ± 2%
name
alloc/op
MemoryStack-4
0.00B
name
allocs/op
MemoryStack-4
0.00
Небольшое заключение
Использование указателя вместо копии структуры в Go не всегда лучшая идея. Для закрепления материала я предлагаю вам прочитать вот этот пост. Он больше вам расскажет про стратегии использования структур и встроенных типов данных в Golang.
Кроме того, профайлинг вашей памяти действительно помогает понять, что происходит с выделением памяти и кучей. Используйте его по мере необходимости.
По традиции, некоторые полезные ссылочки, которые могут вам пригодиться:
- Немного примеров использования структур в Golang
- Несложно и подробно про указатели в Golang
- Немного про устройство кучи в Golang
Узнать о курсе подробнее.
Читать ещё:
- Полное руководство по массивам и срезам в Golang
- Обработка ошибок в Go
- Управление пакетами с помощью модулей Go: Прагматическое руководство
===========
Источник:
habr.com
===========
Похожие новости:
- [] Игровая статика, или как я перестал бояться и полюбил Google Apps Script
- [Программирование, Беспроводные технологии, Хакатоны, Интервью, IT-компании] Победитель хакатона: права на цифровое решение остались за нами
- [Программирование, Серверное администрирование] Запуск Camunda BPM в Kubernetes (перевод)
- [Ruby, Python, Программирование] Как работают профайлеры в Ruby и Python?
- [Разработка под iOS, Swift] Создаем калькулятор на Swift 5
- [Информационная безопасность, Программирование, Администрирование баз данных, Хранение данных] Firebase снова стала предметом исследований
- [Python, Программирование, .NET, Scala] Вы просто не знаете зачем нужны языки с динамической типизацией
- [Звук, IT-компании] Google в октябре заменит Google Play Music на сервис YouTube Musiс
- [Облачные сервисы, Программирование, Разработка веб-сайтов] Serverless и полтора программиста
- [Python, Программирование, Отладка] 8 продвинутых возможностей модуля logging в Python, которые вы не должны пропустить (перевод)
Теги для поиска: #_programmirovanie (Программирование), #_go, #_go, #_golang, #_blog_kompanii_otus._onlajnobrazovanie (
Блог компании OTUS. Онлайн-образование
), #_programmirovanie (
Программирование
), #_go
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 17:04
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Прямо сейчас в OTUS открыт набор на новый поток курса «Разработчик Golang». В преддверии старта курса делимся с вами авторской статьей от нашего коллеги. Всем привет! Сегодня я хотел бы поговорить об использовании указателя и копии структуры в Golang, и обсудить, какой из вариантов выгоднее по памяти и по производительности. Что такое структура? Структура — это тип, который содержит именованные поля. Да, изначально кажется, что это определение объекта или хеш-таблицы, но в Go это структура. Структуры используются для создания кастомных типов данных, например, при получении привычного набора данных из базы данных. Если вы хотите немного почитать, что из себя представляют структуры, вы можете сделать это здесь. Что такое указатель? Некоторые книги вообще предлагают в качестве объяснения спросить у C программиста, что же такое указатели. На самом деле ничего в них такого сложного нет, указатели просто указывают на участок памяти, где хранится значение. Если вам непонятно, что это такое, можете почитать это в go учебнике Создание данных для тестирования Давайте создадим структуру для тестирования: type S struct {
a, b, c int64 d, e, f string g, h, i float64 } Ниже у нас представлено создание копии нашей структуры: func asCopy() S {
return S{ a: 10, b: 20, c: 30, e: "data", f: "foo", g: 1.0, h: 2.0, i: 3.0, } } А здесь у нас будет представлено копирование при помощи указателя: func asPointer() *S {
return &S{ a: 10, b: 20, c: 30, e: "data", f: "foo", g: 1.0, h: 2.0, i: 3.0, } } Используя вышеперечисленные функции, мы можем написать два бенчмарка, первый будет основан на передаче структуры как копии: func BenchmarkMemoryStack(b *testing.B){
var s S f, err :=os.Create("stack.out") if err != nil{ panic(err) } defer f.Close() err = trace.Start(f) if err != nil{ panic(err) } for i:= 0; i < b.N; i++{ s = byCopy() } trace.Stop() b.StopTimer() _ = fmt.Sprintf("%v", s.a) } Также мы создадим другой бенчмарк, в котором будем передавать структуры как указатель: func BenchMemoryHeap(b *testing.B) {
var s *S f, err := os.Create("heap.out") if err != nil { panic(err) } defer f.Close() err = trace.Start(f) if err != nil { panic(err) } for i := 0; i < b.N; i++ { s = byPointer() } trace.Stop() b.StopTimer() _ = fmt.Sprintf("%v", s.a) } Давайте запустим наши бенчмарки: go test ./... -bench=BenchmarkMemoryHeap -benchmem -run=^$ -count=10 > head.txt && benchstat head.txt
go test ./... -bench=BenchmarkMemoryStack -benchmem -run=^$ -count=10 > stack.txt && benchstat stack.txt Здесь я с приведу статистики, которые у нас получились: MemoryHeap name time/op MemoryHeap-4 75.0ns ± 5% name alloc/op MemoryHeap-4 96.0B ± 0% name allocs/op MemoryHeap-4 1.00 ± 0% MemoryStack name time/op MemoryStack-4 8.93ns ± 4% name alloc/op MemoryStack-4 0.00B name allocs/op MemoryStack-4 0.00 Использование копии структуры вместо указателя дает 8-кратный выигрыш в скорости. Чтобы понять почему, давайте посмотрим на графы, которые были сгенерированы нашим трейсом. Первым пойдет граф, в котором у нас структура сгенерирована как копия: Потом где структура сгенерирована с помощью указателей: Первый граф достаточно простой. Там не используется куча, и нет сборщика мусора и дополнительной горутины. На втором графе, использование указателя заставляет компилятор выкидывать переменную из кучи и направляет в работу наш сборщик мусора. Если мы немного приблизим граф, увидим, что сборщик мусора оказывает значительное влияние на важные части нашего процесса: Мы можем увидеть, что на этом графе сборщик мусора должен срабатывать каждые 4ms. Если приблизить опять, мы получим дополнительные детали, что у нас действительно происходит: Голубая, розовая и красная полоски выражают фазы сборщика мусора, в то время коричневые связаны с выделением кучи (помечено на "runtime.bsweep" на этом графе). Даже если этот пример покажется вам немного специфичным, он вполне показателен, чтобы дать понять, что стоимость выделения переменной в куче больше, чем в стеке. В нашем примере, код быстрее выделяет структуру в стеке и копирует ее, чем выделяет память в куче и передает адрес. Если вам не совсем понятно, что из себя представляет стек/куча, но вы хотели бы понимать больше, вы можете прочитать эту замечательнуюстатью на нашем сайте. Интенсивные вызовы функций Давайте разберем второй кейс, в котором добавим два пустых метода к нашей структуре с небольшой адаптацией к нашим бенчмаркам. func (s S) stack(s1 S) {}
func (s *S) heap(s1 *S) {} Давайте посмотрим на выделение оперативной памяти в нашем стеке, в котором мы создаем структуру и передаем ее как копию: func BenchMemoryStack(b * testing.B){
var s S var s1 S s = byCopy() s1 = byCopy() for i :=0; i< b.N; i++{ for i:0; i < 10000; i++{ s.stack(s1) } } } И бенчмарк для кучи передаст структуру в качестве указателя: func BenchMemoryHeap(b * testing.B){
var s *S var s1 *S s = byPointer() s1 = byPointer() for i: = 0; i < b.N; i++{ for i := 0; i < 100000; i++{ s.heap(s1) } } } Вполне ожидаемо, результаты сейчас достаточно разные: MemoryHeap name time/op MemoryHeap-4 301µs ± 4% name alloc/op MemoryHeap-4 0.00B name allocs/op MemoryHeap-4 0.00 MemoryStack name time/op MemoryStack-4 595µs ± 2% name alloc/op MemoryStack-4 0.00B name allocs/op MemoryStack-4 0.00 Небольшое заключение Использование указателя вместо копии структуры в Go не всегда лучшая идея. Для закрепления материала я предлагаю вам прочитать вот этот пост. Он больше вам расскажет про стратегии использования структур и встроенных типов данных в Golang. Кроме того, профайлинг вашей памяти действительно помогает понять, что происходит с выделением памяти и кучей. Используйте его по мере необходимости. По традиции, некоторые полезные ссылочки, которые могут вам пригодиться:
Узнать о курсе подробнее. Читать ещё:
=========== Источник: habr.com =========== Похожие новости:
Блог компании OTUS. Онлайн-образование ), #_programmirovanie ( Программирование ), #_go |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 17:04
Часовой пояс: UTC + 5