Skip to content

defer

defer Go'nun günlük geliştirmede sık kullanılan bir anahtar kelimesidir. defer ile ilişkili fonksiyonları LIFO (Son Giren İlk Çıkar) şeklinde yürütür. Bu mekanizmayı genellikle dosya kapatma gibi kaynak bırakma işlemleri için kullanırız.

go
fd, err := os.Open("/dev/stdin")
if err != nil{
    return err
}
defer fd.Close()
...

Bu anahtar kelimenin ne sıklıkta göründüğü göz önüne alındığında, arkasındaki alt yapıyı anlamamız gerekir.

Yapı

defer anahtar kelimesi runtime._defer yapısına karşılık gelir. Yapısı karmaşık değildir:

go
type _defer struct {
  started bool
  heap    bool
  openDefer bool
  sp        uintptr // sp at time of defer
  pc        uintptr // pc at time of defer
  fn        func()  // can be nil for open-coded defers
  _panic    *_panic // panic that is running defer
  link      *_defer // next defer on G; can point to either heap or stack!
  fd   unsafe.Pointer // funcdata for the function associated with the frame
  varp uintptr        // value of varp for the stack frame
  framepc uintptr
}

fn alanı defer anahtar kelimesine karşılık gelen fonksiyondur, link bir sonraki bağlı defer'i temsil eder ve sp ile pc çağırıcının fonksiyon bilgilerini kaydeder, defer'in hangi fonksiyona ait olduğunu belirlemek için kullanılır. defer runtime'da bağlı liste olarak bulunur, listenin başı goroutine G üzerindedir, yani defer aslında doğrudan goroutine ile ilişkilidir.

go
type g struct {
    ...
  _panic    *_panic // innermost panic - offset known to liblink
  _defer    *_defer // innermost defer
    ...
}

Bir goroutine bir fonksiyon yürüttüğünde, fonksiyondan defer'i sırayla bağlı listenin başına ekler.

go
defer fn1()
defer fn2()
defer fn3()

Yukarıdaki kod bu diyagrama karşılık gelir:

Goroutine'lerin yanı sıra, P'nin de defer ile belirli bir ilişkisi vardır. P'nin yapısında, aşağıda gösterildiği gibi bir deferpool alanı vardır.

go
type p struct {
  ...
  deferpool    []*_defer // pool of available defer structs (see panic.go)
  deferpoolbuf [32]*_defer
    ...
}

deferpool önceden tahsis edilmiş defer yapılarını içerir, P ile ilişkili goroutine G için yeni defer yapıları tahsis etmek için kullanılır, bu genel gideri azaltabilir.

Tahsis

Söz dizimi açısından, defer anahtar kelimesi kullanıldığında, derleyici bunu runtime.deferproc fonksiyonuna bir çağrı olarak çevirir. Örneğin, Go kodu şöyle yazılmışsa:

go
defer fn1(x, y)

Derlenen gerçek kod şöyledir:

go
deferproc(func(){
  fn1(x, y)
})

Yani gerçekte, defer'e geçirilen fonksiyonun parametresi ve dönüş değeri yoktur. deferproc fonksiyon kodu şöyledir:

go
func deferproc(fn func()) {
  gp := getg()
  d := newdefer()
  d.link = gp._defer
  gp._defer = d
  d.fn = fn
  d.pc = getcallerpc()
  d.sp = getcallersp()
  return0()
}

Bu fonksiyon bir defer yapısı oluşturmaktan ve bunu goroutine G'nin bağlı listesinin başına eklemekten sorumludur. runtime.newdefer fonksiyonu, P'deki deferpool'dan önceden tahsis edilmiş bir defer yapısı almaya çalışır.

go
if len(pp.deferpool) == 0 && sched.deferpool != nil {
    lock(&sched.deferlock)
    for len(pp.deferpool) < cap(pp.deferpool)/2 && sched.deferpool != nil {
        d := sched.deferpool
        sched.deferpool = d.link
        d.link = nil
        pp.deferpool = append(pp.deferpool, d)
    }
    unlock(&sched.deferlock)
}

İlk olarak yerel deferpool'u global sched.deferpool'dan yarıya kadar doldurur, sonra P'nin deferpool'undan almaya çalışır.

go
if n := len(pp.deferpool); n > 0 {
    d = pp.deferpool[n-1]
    pp.deferpool[n-1] = nil
    pp.deferpool = pp.deferpool[:n-1]
}

if d == nil {
    // Allocate new defer.
    d = new(_defer)
}
d.heap = true

Sadece hiçbir şey bulunamadığında manuel tahsis kullanır. Son olarak, bu kodu görebilirsiniz:

go
d.heap = true

Bu defer'in yığında (heap) tahsis edildiğini gösterir. Karşılık olarak, false olduğunda, stack'te tahsis edilir. Stack'te tahsis edilen bellek dönerken otomatik olarak geri alınır ve bellek yönetim verimliliği yığındakinden daha yüksektir. Stack'te tahsis edip etmeyeceğini belirleyen faktör döngü seviyelerinin sayısıdır. Bu mantık cmd/compile/ssagen'deki escape.goDeferStmt metoduna kadar izlenebilir:

go
func (e *escape) goDeferStmt(n *ir.GoDeferStmt) {
  ...
  if n.Op() == ir.ODEFER && e.loopDepth == 1 {
    k = e.later(e.discardHole())
    n.SetEsc(ir.EscNever)
  }
    ...
}

e.loopDepth mevcut ifade için döngü seviyelerinin sayısını temsil eder. Eğer mevcut defer ifadesi bir döngüde değilse, stack'te tahsis edilecektir.

go
case ir.ODEFER:
    n := n.(*ir.GoDeferStmt)
    if s.hasOpenDefers {
      s.openDeferRecord(n.Call.(*ir.CallExpr))
    } else {
      d := callDefer
      if n.Esc() == ir.EscNever {
        d = callDeferStack
      }
      s.callResult(n.Call.(*ir.CallExpr), d)
    }

Eğer stack'te tahsis edilirse, defer yapısı doğrudan stack'te oluşturulur ve sonunda runtime.deferprocStack fonksiyonu defer yapısı oluşturmayı tamamlar.

go
if k == callDeferStack {
    // Make a defer struct d on the stack.
    if stksize != 0 {
      s.Fatalf("deferprocStack with non-zero stack size %d: %v", stksize, n)
    }

    t := deferstruct()
    ...
    // Call runtime.deferprocStack with pointer to _defer record.
    ACArgs = append(ACArgs, types.Types[types.TUINTPTR])
    aux := ssa.StaticAuxCall(ir.Syms.DeferprocStack, s.f.ABIDefault.ABIAnalyzeTypes(nil, ACArgs, ACResults))
    callArgs = append(callArgs, addr, s.mem())
    call = s.newValue0A(ssa.OpStaticLECall, aux.LateExpansionResultType(), aux)
    call.AddArgs(callArgs...)
    call.AuxInt = int64(types.PtrSize) // deferprocStack takes a *_defer arg

deferprocStack fonksiyonunun imzası şöyledir:

go
func deferprocStack(d *_defer)

Belirli oluşturma mantığı deferproc'tan çok farklı değildir. Ana fark, stack'te tahsis ederken, defer yapısı doğrudan oluşturulan struct'tan gelirken, yığında tahsis edilen defer new fonksiyonundan gelir.

Yürütme

Bir fonksiyon dönmek üzereyken veya bir panic oluştuğunda, runtime.deferreturn fonksiyonuna girer, bu fonksiyon goroutine'un bağlı listesinden defer alıp yürütmekten sorumludur.

go
func deferreturn() {
  gp := getg()
  for {
    d := gp._defer
    sp := getcallersp()
    if d.sp != sp {
      return
    }
    fn := d.fn
    d.fn = nil
    gp._defer = d.link
    freedefer(d)
    fn()
  }
}

İlk olarak, getcallersp() kullanarak mevcut fonksiyonun stack çerçevesini alır ve defer yapısındaki sp ile karşılaştırarak defer'in mevcut fonksiyona ait olup olmadığını belirler. Sonra bağlı listenin başından defer yapısını alır, gp._defer = d.link kullanarak bir sonraki defer'i yürütür, runtuime.freedefer fonksiyonu ile defer yapısını havuza geri bırakır ve son olarak fn'i çağırarak yürütür. Bu, mevcut fonksiyona ait tüm defer'ler yürütülene kadar döngüde devam eder.

Open-Coded Defer

defer kullanmanın maliyetsiz olmadığı söylenebilir. Söz diziminde kolaylık sağlasa da, doğrudan bir fonksiyon çağrısı değildir ve bir dizi işlemden geçer, bu nedenle performans yüküne neden olur. Bu nedenle, Go yetkilileri daha sonra bir optimizasyon yöntemi tasarladılar—open-coded defer. Bu defer için bir optimizasyondur, originally "open-coded" olarak adlandırılmıştır, Çince'de "开放编码" olarak çevrilir. Burada "open" genişletmek anlamına gelir, yani defer fonksiyon kodunu mevcut fonksiyon koduna genişletmek, fonksiyon satır içi yapmaya benzer. Bu optimizasyon yönteminin aşağıdaki kısıtlamaları vardır:

  1. Bir fonksiyondaki defer sayısı 8'i aşamaz
  2. defer ve return sayısının çarpımı 15'i aşamaz
  3. defer bir döngüde görünemez
  4. Derleyici optimizasyonu devre dışı bırakılmamış
  5. os.Exit() manuel çağrısı yok
  6. Parametrelerin yığından kopyalanması gerekmiyor

Bu yargı mantığı cmd/compile/ssagen.buildssa fonksiyonunun aşağıdaki bölümüne kadar izlenebilir:

go
s.hasOpenDefers = base.Flag.N == 0 && s.hasdefer && !s.curfn.OpenCodedDeferDisallowed()
if s.hasOpenDefers && len(s.curfn.Exit) > 0 {
    s.hasOpenDefers = false
}
if s.hasOpenDefers {
    for _, f := range s.curfn.Type().Results().FieldSlice() {
        if !f.Nname.(*ir.Name).OnStack() {
            s.hasOpenDefers = false
            break
        }
    }
}
if s.hasOpenDefers && s.curfn.NumReturns*s.curfn.NumDefers > 15 {
    s.hasOpenDefers = false
}

Sonra Go, mevcut fonksiyonda defer'i işaretlemek için bitmap olarak hizmet veren 8-bit tamsayı değişkeni deferBits oluşturur. Her bit bir defer'i işaretler. 8-bit tamsayı uint8 en fazla 8 defer temsil edebilir. İlgili bit 1 ise, fonksiyon dönerken ilgili open-coded optimize edilmiş defer yürütülecektir.

Golang by www.golangdev.cn edit