Go Programlama Dilinde Escape Analysis

Ömer Burak Demirpolat
4 min readDec 21, 2021

--

Selamlar, bu yazı aşağıdaki konuları içerecek:

  • Escape Analysis nedir?
  • Go programlama dili için Escape Analysis nasıl gerçekleşiyor?
  • Escape Analysis’den nasıl faydalanabiliriz?

Escape Analysis nedir?

Escape Analysis bir değişkenin belleğin hangi alanında yer alacağına karar veren bir süreçtir, runtime’da değişkenin heap’de mi yoksa stack’de mi bulunacağına karar verilmesini sağlar.

Heap ve Stack nedir -> Heap ve Stack Kavramları

Bir fonksiyon yaratıldığında içerisindeki veriler genellikle stack’de oluşturulur, burada genellikle dememin sebebi bazı büyük boyuttaki veriler örneğin 20Mb boyutundaki bir slice heap’de oluşturulur. Peki bu fonksiyon return statement’ına geldiğinde neler oluyor, aşağıdaki kod bloğu ile yorumlayalım.

func main() {
result := do() // 0x1400018c008
}
func do() int {
a := 5 // 0x1400018c010
return a
}

do fonksiyonu içerisinde 0x1400018c01 adresinde bir integer değişken yaratıldı, ardından return ile birlikte bu değişkenin değeri, value’su dönüyor.

Dönen değer kopyalanıp main fonksiyonundaki 0x1400018c008 adresinde oluşturulmuş integer değişkene teslim ediliyor.

do fonksiyonu return statement’ını aldığı anda stack frame siliniyor yani a değişkeni artık yok ediliyor.

Şimdi kod bloğunu biraz güncelleyelim.

func main() {
result := do() // 0x14000016080
}
func do() *int {
a := 5 // 0x14000016080
return &a
}

Burada da do fonksiyonunda bir integer değer yaratıldı, adresi 0x14000016080 fakat bu sefer do fonksiyonu bir pointer döndürüyor. Bu durumda fonksiyondan dönen integer değer kopyalanmadı, sadece referansı main fonksiyonundaki result adlı değişkene atandı. Yine bu işlemin sonunda da stack frame siliniyor fakat bu sefer farklı bir durum var, do fonksiyonunda oluşturulan a değişkenin pointer’ı main fonksiyonundaki result adlı değişkene atandığı için halen erişilebilir durumda, yani bu sefer a değişkeni bellekten silinmedi diyebiliriz.

2. örneğimizde do fonksiyonunda 5 değeri ile birlikte integer bir değer ilk başta stack’de yaratılıyor fakat daha sonrasında bu yaratılan değişken return ile birlikte başka bir fonksiyon tarafından kullanıldığı için return ile birlikte stack memory’den heap memory’e aktarılıyor ve bu sayede main fonksiyonu tarafından kullanılabilir hale geliyor. İşte Escape Analysis tam olarak bu işe yarıyor, eğer bir değişkenin referansı yaratıldığı fonksiyonun stack frame’i haricinde başka bir fonksiyon tarafından kullanılabilir durumda ise bu değişken heap’de yaratılıyor.

Şimdi bu durumu aşağıdaki kod bloğu ile inceleyelim.

//go:noinline
func
main() {
_ = do()
}

//go:noinline
func
do() *int {
a := 5
return &a
}

Kodumuzu optimizasyonları gösterecek şekilde çalıştıralım.

go run -gcflags=’-m’ main.go

“m” flag’i bu işe yarıyor.

./main.go:10:2: moved to heap: a

Bu kodun çıktısından da anlaşılacağı üzere önce stack’de yaratılan a daha sonrasında heap’e aktarıldı.

Aynı kodu aşağıdaki hale getirirsek pointer atamak yerine value kopyalanacağı için böyle bir durum olmayacaktı.

func do() int {
a := 5
return a
}

Peki stack’den heap’e aktarım maliyetli mi, acaba value döndürmek mi yoksa pointer döndürmek mi mantıklı, hangisi daha hızlı olur, bunu nasıl anlarız?

İki fonksiyon yaratalım, birisi value, birisi pointer döndürsün ve benchmark sonuçlarına bakalım.

Fonksiyonlar:

func ReturnPointer() *int {
a := 5
return &a
}

func ReturnValue() int {
a := 5
return a
}

Benchmark Testleri:

func BenchmarkReturnPointer(b *testing.B) {
for i := 0; i < b.N; i++ {
ReturnPointer()
}
}

func BenchmarkReturnValue(b *testing.B) {
for i := 0; i < b.N; i++ {
ReturnValue()
}
}

Sonuç aşağıdaki gibi

BenchmarkReturnPointer-8   1000000000     0.3156 ns/op
BenchmarkReturnValue-8 1000000000 0.3126 ns/op

Sonuçlar birbirine çok yakın olduğu için testi bir çok kez çalıştırdım fakat value dönen fonksiyon her zaman daha hızlı bir sonuç verdi. Açıkcası burada beklenmedik bir sonuç var, bize yıllardır öğretilen, “veriyi kopyalamak yerine pointer döndürün” değil miydi? Neden pointer dönen fonksiyon daha yavaş olabilir? Aslında bu durum biraz da buradaki veri tipine özel bir durum. Kısacası 32bit’lik integer değerin kopyasının döndürülmesi 8bit’lik integer pointer’ının heap’e aktarılmasından daha hızlı gerçekleşiyor. Ama eğer kopyalanan değer daha büyük bir veri olsaydı bu durum değişebilirdi. Şimdi başka bir örnekle bunu görelim.

func ReturnPointer() *[5000]int {
var a [5000]int
return &a
}

func ReturnValue() [5000]int {
var a [5000]int
return a
}

Fonksiyonlar yine aynı sadece artık fonksiyonlarımız size’ı 5000 olan integer bir value veya pointer dönüyor.

Şimdi yine benchmark sonucuna bakalım.

var x *[5000]int
var y [5000]int

func BenchmarkReturnPointer(b *testing.B) {
for i := 0; i < b.N; i++ {
x = ReturnPointer()
}
}

func BenchmarkReturnValue(b *testing.B) {
for i := 0; i < b.N; i++ {
y = ReturnValue()
}
}

Sonuçlar aşağıdaki gibi

BenchmarkReturnPointer-8    795044        1524 ns/op
BenchmarkReturnValue-8 492717 2389 ns/op

Bu örnekte pointer dönen fonksiyon çok daha hızlı, bunun sebebi de görüldüğü üzere 5000 size’lı bir array’i kopyalamak yerine pointer’ını heap’e aktarmak daha az maliyetli, daha hızlı.

Şimdi bir farklı durumu daha ele alalım. User adında bir stuct’ımızın olduğunu düşünelim.

3 farklı fonksiyona sahibiz:

  • User adlı struct’ı fonksiyon içinde yaratıp pointer’ını return edeceğiz
  • User adlı struct’ı fonksiyon içerisinde yaratıp value’sunu return edereceğiz.
  • User adlı struct’ın pointer’ını fonksiyonumuza parametre olarak alacak ve içerisindeki field’ların değerlerini atayacağız.
type User struct {
Name string
Surname string
Phone string
}

//go:noinline
func ReturnPointer
() *User {
user := &User{
Name: "burak",
Surname: "demirpolat",
Phone: "111222333444"
}
return user
}
//go:noinline
func ReturnValue
() User {
user := User{
Name: "burak",
Surname: "demirpolat",
Phone: "111222333444"
}
return user
}
//go:noinline
func SetIncomingPointer
(user *User) {
user.Name = "burak"
user.Surname = "demirpolat"
user.Phone = "111222333444"
}

Bu iki fonksiyonun benchmark’ını değerlendirelim.

BenchmarkReturnPointer-8         54575374        21.69 ns/op
BenchmarkReturnValue-8 134417302. 8.835 ns/op
BenchmarkSetIncomingPointer-8 276171676 4.304 ns/op

Pointer’ı parametre olarak aldığımız fonksiyon çok daha hızlı.

Yine ilginç bir durum daha mevcut, value return etmek heap’e giden pointer’dan daha hızlı sonuç verdi, tabii bunun içerisinde yine size etkeni mevcut eğer daha büyük bir struct ile işlem yapıyor olsaydık pointer return eden fonksiyon daha hızlı olacaktı.

Okuduğunuz için teşekkürler.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

No responses yet

Write a response