Skip to content

gc

Çöp toplamanın görevi artık kullanılmayan nesnelerin belleğini serbest bırakmak ve diğer nesneler için alan boşaltmaktır. Bu basit açıklama son derece karmaşık bir şekilde uygulanır. Çöp toplamanın on yıllara yayılan bir gelişim geçmişi vardır. 1960'ların başında, Lisp dili ilk olarak çöp toplama mekanizmalarını benimsedi. Aşina olduğumuz Python ve Objective-C, GC mekanizmaları için öncelikle referans sayımı kullanır, Java ve C# ise nesil toplama kullanır.

Günümüzde, çöp toplama algoritmaları genel olarak şu türlere ayrılabilir:

  • Referans Sayımı: Her nesne kaç kez referans edildiğini takip eder. Sayı 0'a ulaştığında, geri dönüştürülür.
  • İşaretle-Temizle: Aktif nesneler işaretlenir ve işaretlenmemiş nesneler toplanır.
  • Kopyalama Algoritması: Aktif nesneler yeni belleğe kopyalanır ve eski bellekteki tüm nesneler alanı boşaltmak için toplanır.
  • İşaretle-Sıkıştır: İşaretle-temizle'nin bir yükseltmesidir, aktif nesneleri yığının başına taşır, yönetimi kolaylaştırır.

Uygulama açısından, şu şekilde kategorize edilebilirler:

  • Global Toplama: Tek seferde tüm çöpleri toplar
  • Nesil Toplama: Nesneler ömürlerine göre nesillere ayrılır, her biri için farklı toplama algoritmaları kullanılır
  • Artımlı Toplama: Her seferinde sadece kısmi çöp toplama yapar

TIP

Go'nun çöp toplayıcı tarihçesi hakkında daha fazla bilgi edinmek için The Journey of Go's Garbage Collector adresini ziyaret edin.

Go ilk yayınlandığında, çöp toplama mekanizması çok ilkeldi, sadece basit bir işaretle-temizle algoritması vardı. Çöp toplamanın neden olduğu STW (Stop The World, çöp toplama için tüm programı durdurmayı ifade eder) birkaç saniye veya daha uzun sürebilirdi. Bu sorunu fark eden Go ekibi çöp toplama algoritmasını iyileştirmeye başladı. Go 1.0 ile Go 1.8 sürümleri arasında, birçok yaklaşım denediler:

  1. Read Barrier Concurrent Copying GC: Bu yaklaşım, read barrier genel giderindeki yüksek belirsizlik nedeniyle terk edildi.
  2. Request-Oriented Collector (ROC): Write barrier'ların her zaman etkin olmasını gerektiriyordu, yürütmeyi yavaşlatıyor ve derleme süresini artırıyordu.
  3. Nesil Toplama: Go'nun derleyicisi yeni nesneleri yığında, uzun ömürlü nesneleri yığında tahsis etme eğiliminde olduğu için, Go'da nesil toplama verimli değildi, bu yüzden çoğu yeni nesil nesne doğrudan yığın tarafından geri dönüştürülürdü.
  4. Write Barrier Olmadan Kart İşaretleme: Write barrier maliyetlerini hash dağıtma maliyetleriyle değiştirdi, donanım desteği gerektiriyordu.

Sonunda, Go ekibi write barrier'larla üç renkli eşzamanlı işaretleme kombinasyonunu seçti ve sonraki sürümlerde sürekli olarak iyileştirip optimize etti. Bu yaklaşım günümüze kadar devam ediyor. Aşağıdaki grafik seti Go 1.4'ten Go 1.9'a GC gecikmesi değişikliklerini gösteriyor.

Bu makaleyi yazarken, Go'nun en son sürümü Go 1.23'e yaklaşıyor. Bugünün Go'su için, GC performansı artık bir endişe değil. GC gecikmesi artık çoğunlukla 100 mikrosaniyenin altında, çoğu iş senaryosunun ihtiyaçlarını karşılıyor.

Genel olarak, Go'da çöp toplama şu aşamalara ayrılabilir:

  • Tarama Aşaması: Yığından ve global değişkenlerden kök nesneleri topla
  • İşaretleme Aşaması: Nesneleri renklendir
  • İşaretleme Sonlandırma Aşaması: Temizleme işlerini hallet, barrier'ları kapat
  • Temizleme Aşaması: Çöp nesne belleğini bırak ve geri dönüştür

Kavramlar

Aşağıdaki kavramlar resmi belgelerde ve makalelerde görünebilir, aşağıda kısa açıklamalar bulunmaktadır:

  • Mutator: Kullanıcı programlarını ifade eden teknik bir terim. Go'da, bu kullanıcı kodunu ifade eder.
  • Collector: Çöp toplamadan sorumlu programı ifade eder. Go'da, bu runtime'dır.
  • Finalizer: İşaretleme-temizleme işi tamamlandıktan sonra nesne belleğini geri dönüştürmek ve temizlemekten sorumlu kod.
  • Controller: runtime.gcController global değişkenini ifade eder, tipi gcControllerState'tir. Pacing algoritmasını uygular ve ne zaman çöp toplama yapılacağını ve ne kadar iş yürütüleceğini belirlemekten sorumludur.
  • Limiter: runtime.gcCPULimiter'ı ifade eder, çöp toplama sırasında aşırı CPU kullanımının kullanıcı programlarını etkilemesini önler.

Tetikleme

go
func gcStart(trigger gcTrigger)

Çöp toplama runtime.gcStart fonksiyonu tarafından başlatılır, bu sadece runtime.gcTrigger yapısını parametre olarak kabul eder, GC tetikleme nedenini, mevcut zamanı ve bunun hangi GC turu olduğunu içerir.

go
type gcTrigger struct {
    kind gcTriggerKind
    now  int64  // gcTriggerTime: mevcut zaman
    n    uint32 // gcTriggerCycle: başlatılacak döngü numarası
}

Burada gcTriggerKind'in aşağıdaki opsiyonel değerleri vardır:

go
const (
  // gcTriggerHeap, yığın boyutu denetleyici tarafından hesaplanan
  // tetikleme yığın boyutuna ulaştığında bir döngü başlatılması
  // gerektiğini belirtir.
  gcTriggerHeap gcTriggerKind = iota

  // gcTriggerTime, önceki GC döngüsünden forcegcperiod
  // nanosaniyeden fazla zaman geçtiğinde bir döngü başlatılması
  // gerektiğini belirtir.
  gcTriggerTime

  // gcTriggerCycle, henüz gcTrigger.n döngüsünü (work.cycles'a
  // göre) başlatmadıysak bir döngü başlatılması gerektiğini
  // belirtir.
  gcTriggerCycle
)

Özetle, çöp toplama için üç tetikleme zamanı vardır:

  • Yeni nesneler oluştururken: runtime.mallocgc çağırarak bellek tahsis ederken, yığın belleğinin eşiğe ulaştığı tespit edilirse (genellikle önceki GC'deki boyutun iki katı, bu değer pacing algoritması tarafından da ayarlanır), çöp toplama başlatılır.

    go
    func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
        ...
      if shouldhelpgc {
        if t := (gcTrigger{kind: gcTriggerHeap}); t.test() {
          gcStart(t)
        }
      }
        ...
    }
  • Zamanlanmış zorlamalı tetikleme: Go runtime'da runtime.forcegchelper fonksiyonunu çalıştırmak için ayrı bir goroutine başlatır. Eğer çöp toplama uzun süre yapılmadıysa, zorla GC başlatır. Bu süre runtime.forcegcperiod sabiti tarafından belirlenir, bu 2 dakikadır. Ayrıca, sistem izleme goroutine'i de periyodik olarak zorlamalı GC gerekip gerekmediğini kontrol eder.

    go
    func forcegchelper() {
      for {
            ...
        gcStart(gcTrigger{kind: gcTriggerTime, now: nanotime()})
            ...
      }
    }
    go
    func sysmon() {
        ...
      for {
            ...
        // zorlamalı GC yapmamız gerekip gerekmediğini kontrol et
        if t := (gcTrigger{kind: gcTriggerTime, now: now}); t.test() && forcegc.idle.Load() {
          lock(&forcegc.lock)
          forcegc.idle.Store(false)
          var list gList
          list.push(forcegc.g)
          injectglist(&list)
          unlock(&forcegc.lock)
        }
      }
    }
  • Manuel tetikleme: runtime.GC fonksiyonu aracılığıyla, kullanıcılar manuel olarak çöp toplamayı tetikleyebilir.

    go
    func GC() {
        ...
      n := work.cycles.Load()
      gcStart(gcTrigger{kind: gcTriggerCycle, n: n + 1})
        ...
    }

TIP

Eğer ilgileniyorsanız, GC'yi tetiklemek için pacing algoritmasının tasarım felsefesini ve iyileştirmelerini açıklayan Go Gc Pacer Re-Design makalesini okuyabilirsiniz. Karmaşık içeriği nedeniyle birçok matematiksel formül içerir, ana metinde detaylandırılmamıştır.

İşaretleme

Bugün, Go'nun GC algoritması hala önce işaretle sonra temizle şeklindedir, ancak uygulaması artık eskisi kadar basit değil.

İşaretle-Temizle

En basit işaretle-temizle algoritması ile başlayalım. Bellekte, nesneler arasındaki referans ilişkileri bir grafik oluşturur. Çöp toplama bu grafik üzerinde çalışır, iki aşamaya ayrılır:

  • İşaretleme Aşaması: Kök düğümlerden (kök düğümler genellikle yığındaki değişkenler, global değişkenler ve diğer aktif nesnelerdir) başlayarak, her ulaşılabilir düğümü tek tek ziyaret edin ve aktif nesneler olarak işaretleyin, tüm ulaşılabilir düğümler ziyaret edilene kadar.
  • Temizleme Aşaması: Yığındaki tüm nesneleri ziyaret edin, işaretlenmemiş nesneleri geri dönüştürün ve bellek alanlarını bırakın veya yeniden kullanın.

Toplama süreci sırasında, nesne grafik yapısı değiştirilemez, bu yüzden tüm program durdurulmalıdır, bu STW'dir. Sadece toplama tamamlandıktan sonra çalışmaya devam edebilir. Bu algoritmanın dezavantajı uzun zaman almasıdır, bu program yürütme verimliliğini önemli ölçüde etkiler. Bu Go'nun erken sürümlerinde kullanılan işaretleme algoritmasıydı ve dezavantajları açıktır:

  • Bellek parçalanmaları üretir (Go'nun TCMalloc tarzı bellek yönetimi nedeniyle, parçalanmanın etkisi önemli değildir)
  • İşaretleme aşaması sırasında, yığındaki tüm nesneler taranır
  • STW'ye neden olur, tüm programı durdurur ve süre kısa değildir

Üç Renkli İşaretleme

Verimliliği artırmak için, Go klasik üç renkli işaretleme algoritmasını benimsedi. Sözde üç renk siyah, gri ve beyazdır:

  • Siyah: Nesne işaretleme sırasında ziyaret edildi ve doğrudan referans verdiği tüm nesneler de ziyaret edildi, aktif nesneleri gösterir.
  • Gri: Nesne işaretleme sırasında ziyaret edildi, ancak doğrudan referans verdiği nesnelerin tamamı ziyaret edilmedi. Tümü ziyaret edildiğinde, siyaha döner, aktif nesneleri gösterir.
  • Beyaz: Nesne işaretleme sırasında hiç ziyaret edilmedi. Ziyaret edildikten sonra, griye döner, çöp nesne olabileceğini gösterir.

Üç renkli işaretleme çalışmasının başlangıcında, alanda sadece gri ve beyaz nesneler vardır. Tüm kök nesneler gridir ve diğer nesneler beyazdır, aşağıda gösterildiği gibi.

Her işaretleme turunun başlangıcında, gri nesnelerden başlayın, gri nesneleri aktif nesneleri göstermek için siyah olarak işaretleyin, sonra siyah nesneler tarafından doğrudan referans verilen tüm nesneleri gri olarak işaretleyin. Gerisi beyaz kalır. Bu noktada, alanda siyah, gri ve beyaz nesneler vardır.

Yukarıdaki adımları sürekli tekrarlayın, alanda sadece siyah ve beyaz nesneler kalana kadar. Gri nesne kümesi boş olduğunda, işaretleme tamamlandı demektir, aşağıda gösterildiği gibi.

İşaretleme tamamlandıktan sonra, temizleme aşamasında, beyaz kümedeki nesnelerin belleğini bırakın.

Değişmezlik

Üç renkli işaretleme algoritması kendisi eşzamanlı işaretleme yapamaz (işaretleme sırasında programın çalışmasını ifade eder). Eğer işaretleme sırasında nesne grafik yapısı değişirse, bu iki duruma yol açabilir:

  • Aşırı işaretleme: Bir nesne siyah olarak işaretlendikten sonra, kullanıcı programı o nesneye tüm referansları siler. O zaman beyaz bir nesne olmalı ve toplanması gerekir.
  • Eksik işaretleme: Bir nesne beyaz olarak işaretlendikten sonra, kullanıcı programdaki diğer nesneler o nesneye referans verir. O zaman siyah bir nesne olmalı ve toplanmamalıdır.

İlk durum kabul edilebilirdir çünkü toplanmamış nesneler bir sonraki toplama turunda işlenebilir. Ancak ikinci durum kabul edilemez. Kullanımda olan bir nesnenin belleğini serbest bırakmak ciddi program hatalarına neden olur, bu mutlaka önlenmelidir.

Üç renkli değişmezlik kavramı Pekka P. Pirinen'in 1998 tarihli "Barrier Techniques for Incremental Tracing" makalesinden gelmektedir. Eşzamanlı işaretleme sırasında nesne renklerinin iki değişmezini ifade eder:

  • Güçlü Üç Renkli Değişmezlik: Siyah nesneler beyaz nesnelere doğrudan referans veremez.
  • Zayıf Üç Renkli Değişmezlik: Siyah bir nesne beyaz bir nesneye doğrudan referans verdiğinde, o beyaz nesneye doğrudan veya dolaylı olarak ulaşabilen başka bir gri nesne olmalıdır, buna gri nesne tarafından korunuyor denir.

Güçlü üç renkli değişmezlik için, siyah nesne 3'in zaten ziyaret edilmiş bir nesne olduğu bilinir ve çocuk nesneleri de ziyaret edilmiş ve gri nesneler olarak işaretlenmiştir. Eğer bu noktada kullanıcı programı eşzamanlı olarak siyah nesne 3 için beyaz nesne 7'ye yeni bir referans eklerse, normalde beyaz nesne 7 gri olarak işaretlenmelidir. Ancak siyah nesne 3 zaten ziyaret edildiği için, nesne 7 ziyaret edilmeyecek, bu yüzden her zaman beyaz bir nesne olarak kalacak ve sonunda yanlışlıkla temizlenecektir.

Zayıf üç renkli değişmezlik için, bu aslında güçlü üç renkli değişmezliğe benzer. Gri nesneler beyaz nesneye doğrudan veya dolaylı olarak ulaşabildiği için, sonraki işaretleme sırasında sonunda gri bir nesne olarak işaretlenecek, böylece yanlışlıkla toplanmaktan kaçınılacaktır.

Değişmezlik aracılığıyla, işaretleme sırasında hiçbir nesnenin yanlışlıkla toplanmayacağı garanti edilebilir, böylece eşzamanlı koşullar altında işaretleme çalışmasının doğruluğu garanti edilir. Bu, üç renkli işaretleme'nin eşzamanlı çalışmasını sağlar, işaretleme-temizle algoritmasına kıyasla işaretleme verimliliğini önemli ölçüde artırır. Eşzamanlı koşullar altında üç renkli değişmezliği sağlamanın anahtarı barrier teknolojisindedir.

İşaretleme Çalışması

GC tarama aşaması sırasında, GC durumunu belirtmek için kullanılan runtime.gcphase adında bir global değişken vardır, aşağıdaki opsiyonel değerlere sahiptir:

  • _GCoff: İşaretleme çalışması başlamadı
  • _GCmark: İşaretleme çalışması başladı
  • _GCmarktermination: İşaretleme çalışması sonlanmak üzere

İşaretleme çalışması başladığında, runtime.gcphase durumu _GCmark'tır. İşaretleme çalışması runtime.gcDrain fonksiyonu tarafından yürütülür, burada runtime.gcWork parametresi izlenecek nesne işaretçilerini depolayan bir tampon havuzudur.

go
func gcDrain(gcw *gcWork, flags gcDrainFlags)

Çalışma sırasında, tampon havuzundan izlenebilir işaretçiler almaya çalışır. Eğer varsa, tarama görevini devam ettirmek için runtime.scanobject fonksiyonunu çağırır. Rolü, tampondaki nesneleri sürekli olarak taramak, onları siyah olarak işaretlemektir.

go
if work.full == 0 {
    gcw.balance()
}

b := gcw.tryGetFast()
if b == 0 {
    b = gcw.tryGet()
    if b == 0 {
        // Write barrier tamponunu boşalt
        // bu daha fazla iş oluşturabilir
        wbBufFlush()
        b = gcw.tryGet()
    }
}
if b == 0 {
    // İş alınamadı
    break
}
scanobject(b, gcw)

İşaretleme çalışması sadece P önlendiğinde veya STW oluşmak üzere olduğunda durur.

go
for !(gp.preempt && (preemptible || sched.gcwaiting.Load() || pp.runSafePointFn != 0)) {
  ...
  scanobject(b, gcw)
  ...
}

runtime.gcwork üretici/tüketici modelini kullanan bir kuyruktur. Kuyruk taranacak gri nesneleri depolamaktan sorumludur. Her işlemci P'nin yerel olarak böyle bir kuyruğu vardır, runtime.p.gcw alanına karşılık gelir.

go
func scanobject(b uintptr, gcw *gcWork) {
  ...
  for {
    var addr uintptr
    if hbits, addr = hbits.nextFast(); addr == 0 {
            if hbits, addr = hbits.next(); addr == 0 {
                break
            }
        }
    scanSize = addr - b + goarch.PtrSize
    obj := *(*uintptr)(unsafe.Pointer(addr))
    if obj != 0 && obj-b >= n {
      if obj, span, objIndex := findObject(obj, b, addr-b); obj != 0 {
        greyobject(obj, b, addr-b, span, gcw, objIndex)
      }
    }
  }
  gcw.bytesMarked += uint64(n)
  gcw.heapScanWork += int64(scanSize)
}

runitme.scanobject fonksiyonu tarama sırasında ulaşılabilir beyaz nesneleri sürekli olarak gri olarak işaretler, sonra gcw.put aracılığıyla yerel kuyruğa koyar. Aynı zamanda, gcDrain fonksiyonu gcw.tryget aracılığıyla taramayı devam ettirmek için gri nesneler almaya sürekli çalışır. İşaretleme ve tarama süreci artımlıdır ve tüm işaretleme işini bir seferde tamamlaması gerekmez. İşaretleme görevleri bazı nedenlerle önlendiğinde, kesintiye uğrar. Yeniden başlatıldığında, kuyruktaki kalan gri nesnelere göre işaretleme çalışmasını devam ettirebilirler.

Arka Plan İşaretleme

İşaretleme çalışması GC başladığında hemen yürütülmez. GC ilk tetiklendiğinde, Go mevcut toplam işlemci P sayısı kadar işaretleme görevi oluşturur. Bunlar global görev kuyruğuna eklenir ve sonra işaretleme aşamasında uyandırılana kadar uykuya girer. Runtime'da, görev dağıtımı runtime.gcBgMarkStartWorkers tarafından yapılır. İşaretleme görevi aslında runtime.gcBgMarkWorker fonksiyonunu ifade eder, burada gcBgMarkWorkerCount ve gomaxprocs sırasıyla mevcut işçi sayısını ve işlemci P sayısını temsil eden iki runtime global değişkendir.

go
func gcBgMarkStartWorkers() {
  // Arka plan işaretleme P başına G'ler tarafından yapılır. Her P'nin
  // bir arka plan GC G'si olduğundan emin olun.
  //
  // İşçi G'ler gomaxprocs azaltılırsa çıkmaz. Eğer tekrar
  // artırılırsa, eski işçileri yeniden kullanabiliriz; yeni
  // oluşturmaya gerek yok.
  for gcBgMarkWorkerCount < gomaxprocs {
    go gcBgMarkWorker()

    notetsleepg(&work.bgMarkReady, -1)
    noteclear(&work.bgMarkReady)
    // İşçinin artık bir sonraki findRunnableGCWorker'den önce
    // havuza eklendiği garanti edilir.

    gcBgMarkWorkerCount++
  }
}

İşçi başladıktan sonra, bir runtime.gcBgMarkWorkerNode yapısı oluşturur, onu global işçi havuzu runitme.gcBgMarkWorkerPool'a ekler, sonra goroutine'i uykuya koymak için runtime.gopark fonksiyonunu çağırır.

go
func gcBgMarkWorker() {
    ...
  node := new(gcBgMarkWorkerNode)
  node.gp.set(gp)
  notewakeup(&work.bgMarkReady)

  for {
    // gcController.findRunnableGCWorker tarafından
    // uyandırılana kadar uykuya git.
    gopark(func(g *g, nodep unsafe.Pointer) bool {
      node := (*gcBgMarkWorkerNode)(nodep)
      // Bu G'yi havuza bırak.
      gcBgMarkWorkerPool.push(&node.node)
      // Bu noktada, G'nin hemen yeniden zamanlanabileceği
      // ve çalışıyor olabileceği unutulmamalıdır.
      return true
    }, unsafe.Pointer(node), waitReasonGCWorkerIdle, traceBlockSystemGoroutine, 0)
    }
    ...
}

İşçileri uyandırabilecek iki durum vardır:

  • İşaretleme aşaması sırasında, zamanlama döngüsü uyuyan işçileri runtime.runtime.gcController.findRunnableGCWorker fonksiyonu aracılığıyla uyandırır.
  • İşaretleme aşaması sırasında, eğer işlemci P şu anda boş durumdaysa, zamanlama döngüsü doğrudan global işçi havuzu gcBgMarkWorkerPool'dan mevcut işçiler almaya çalışır.
go
func findRunnable() (gp *g, inheritTime, tryWakeP bool) {
top:
    // Bir GC işçisi zamanlamayı dene.
  if gcBlackenEnabled != 0 {
    gp, tnow := gcController.findRunnableGCWorker(pp, now)
    if gp != nil {
      return gp, false, true
    }
    now = tnow
  }
    ...
    // Yapacak bir şeyimiz yok.
  //
  // GC işaretleme aşamasındaysak, nesneleri güvenle tarayabilir
  // ve siyaha boyayabiliriz ve yapacak işimiz varsa, P'yi
  // bırakmak yerine boş zaman işaretleme çalıştır.
  if gcBlackenEnabled != 0 && gcMarkWorkAvailable(pp) && gcController.addIdleMarkWorker() {
    node := (*gcBgMarkWorkerNode)(gcBgMarkWorkerPool.pop())
    if node != nil {
      pp.gcMarkWorkerMode = gcMarkWorkerIdleMode
      gp := node.gp.ptr()

      trace := traceAcquire()
      casgstatus(gp, _Gwaiting, _Grunnable)
      if trace.ok() {
        trace.GoUnpark(gp, 0)
        traceRelease(trace)
      }
      return gp, false, false
    }
    gcController.removeIdleMarkWorker()
  }
    ...
}

İşlemci P'nin yapısında işaretleme görevlerinin yürütme modunu belirtmek için bir alan vardır gcMarkWorkerMode, aşağıdaki opsiyonel değerlere sahiptir:

  • gcMarkWorkerNotWorker: Mevcut işlemci P'nin işaretleme görevlerini yürütmediğini gösterir.

  • gcMarkWorkerDedicatedMode: Mevcut işlemci P'nin işaretleme görevlerini yürütmeye adandığını ve bu süre zarfında önleneceğini gösterir.

  • gcMarkWorkerFractionalMode: Mevcut işlemcinin GC kullanım standardı karşılamadığı için (yüzde 25 standarttır) işaretleme görevlerini yürüttüğünü gösterir. Bu süre zarfında yürütme önlenebilir. Varsayalım ki mevcut işlemci P sayısı 5'tir, hesaplama formülüne göre, işaretleme görevlerine adanmış bir işlemci P gerekir. Kullanım sadece yüzde 20'ye ulaştı ve kalan yüzde 5 kullanım, telafi etmek için bir FractionalMode işlemci P açmayı gerektirir. Belirli hesaplama kodu aşağıdaki gibidir:

    go
    func (c *gcControllerState) startCycle(markStartTime int64, procs int, trigger gcTrigger) {
      ...
      totalUtilizationGoal := float64(procs) * gcBackgroundUtilization
      dedicatedMarkWorkersNeeded := int64(totalUtilizationGoal + 0.5)
        if float64(dedicatedMarkWorkersNeeded) > totalUtilizationGoal {
            // Çok fazla adanmış işçi
            dedicatedMarkWorkersNeeded--
        }
        c.fractionalUtilizationGoal = (totalUtilizationGoal - float64(dedicatedMarkWorkersNeeded)) / float64(procs)
        ...
    }
  • gcMarkWorkerIdleMode: Mevcut işlemcinin boş olduğu için işaretleme görevlerini yürüttüğünü gösterir. Bu süre zarfında yürütme önlenebilir.

Go ekibi GC'nin çok fazla performans kaplamasını ve böylece kullanıcı programlarının normal çalışmasını etkilemesini istemiyor. Bu farklı modlara göre işaretleme çalışması yaparak, GC performans israfı olmadan ve kullanıcı programlarını etkilemeden tamamlanabilir. İşaretleme görevlerinin temel tahsis biriminin işlemci P olduğunu belirtmek önemlidir, bu yüzden işaretleme çalışması eşzamanlı olarak yapılır. Birden fazla işaretleme görevi ve kullanıcı programları birbirini etkilemeden eşzamanlı olarak yürütülür.

İşaretleme Yardımı

Goroutine G'nin runtime'da gcAssistBytes adında bir alanı vardır, burada GC yardım kredileri olarak anılır. GC işaretleme durumunda, bir goroutine belirli bir boyutta bellek tahsis etmeye çalıştığında, tahsis edilen bellek boyutuna eşit krediler düşülür. Eğer bu noktada krediler negatifse, goroutine kredileri geri ödemek için niceliksel miktarda GC tarama görevlerini tamamlamaya yardımcı olmalıdır. Krediler pozitif olduğunda, goroutine yardım işaretleme görevlerini tamamlamak zorunda değildir.

Kredileri düşme fonksiyonu runtime.deductAssistCredit'tir, bu runtime.mallocgc fonksiyonu bellek tahsis etmeden önce çağırılır.

go
func deductAssistCredit(size uintptr) *g {
    var assistG *g
    if gcBlackenEnabled != 0 {
       // Bu tahsisi mevcut kullanıcı G'ye yükle
       assistG = getg()
       if assistG.m.curg != nil {
          assistG = assistG.m.curg
       }
       // Tahsisi G'ye yükle. İç parçalanmayı mallocgc'nin
       // sonunda hesaba katacağız.
       assistG.gcAssistBytes -= int64(size)

       if assistG.gcAssistBytes < 0 {
          // Bu G borçlu. Tahsis etmeden önce bunu düzeltmek
          // için GC'ye yardım et. Bu önyüklemeyi devre dışı
          // bırakmadan önce olmalı.
          gcAssistAlloc(assistG)
       }
    }
    return assistG
}

Ancak, bir goroutine niceliksel miktarda yardım tarama işini tamamladığında, mevcut goroutine'ye niceliksel krediler geri öder. Yardım işaretlemeden sorumlu olan fonksiyon runtime.gcDrainN'dir.

go
func gcAssistAlloc1(gp *g, scanWork int64) {
    ...
  gcw := &getg().m.p.ptr().gcw
    // İş tamamlandı
  workDone := gcDrainN(gcw, scanWork)
  ...
  assistBytesPerWork := gcController.assistBytesPerWork.Load()
  gp.gcAssistBytes += 1 + int64(assistBytesPerWork*float64(workDone))
    ...
}

Tarama eşzamanlı olduğu için, kaydedilen işin sadece bir kısmı mevcut goroutine'dendir. Kalan iş, yardım kuyruğunun sırasına göre diğer goroutine'lere tek tek geri ödenir. Eğer hala kalan varsa, global kredilere gcController.assistBytesPerWork eklenir.

go
func gcFlushBgCredit(scanWork int64) {
    // Kuyruk boşsa, doğrudan global kredilere ekle
  if work.assistQueue.q.empty() {
    gcController.bgScanCredit.Add(scanWork)
    return
  }

  assistBytesPerWork := gcController.assistBytesPerWork.Load()
  scanBytes := int64(float64(scanWork) * assistBytesPerWork)

  lock(&work.assistQueue.lock)
  for !work.assistQueue.q.empty() && scanBytes > 0 {
    gp := work.assistQueue.q.pop()
    if scanBytes+gp.gcAssistBytes >= 0 {
      scanBytes += gp.gcAssistBytes
      gp.gcAssistBytes = 0
      ready(gp, 0, false)
    } else {
      gp.gcAssistBytes += scanBytes
      scanBytes = 0
      work.assistQueue.q.pushBack(gp)
      break
    }
  }

    // Hala kalan
  if scanBytes > 0 {
    assistWorkPerByte := gcController.assistWorkPerByte.Load()
    scanWork = int64(float64(scanBytes) * assistWorkPerByte)
    gcController.bgScanCredit.Add(scanWork)
  }
  unlock(&work.assistQueue.lock)
}

Karşılık olarak, geri ödenmesi gereken çok fazla kredi olduğunda (çok fazla bellek tahsis edildiğinde), kendi borcunun bir kısmını telafi etmek için global krediler de kullanılabilir.

go
func gcAssistAlloc(gp *g) {
  ...
  assistWorkPerByte := gcController.assistWorkPerByte.Load()
  assistBytesPerWork := gcController.assistBytesPerWork.Load()
  debtBytes := -gp.gcAssistBytes
  scanWork := int64(assistWorkPerByte * float64(debtBytes))
  if scanWork < gcOverAssistWork {
    scanWork = gcOverAssistWork
    debtBytes = int64(assistBytesPerWork * float64(scanWork))
  }

    // Global kredileri teminat olarak kullan
  bgScanCredit := gcController.bgScanCredit.Load()
  stolen := int64(0)
  if bgScanCredit > 0 {
    if bgScanCredit < scanWork {
      stolen = bgScanCredit
      gp.gcAssistBytes += 1 + int64(assistBytesPerWork*float64(stolen))
    } else {
      stolen = scanWork
      gp.gcAssistBytes += debtBytes
    }
    gcController.bgScanCredit.Add(-stolen)

    scanWork -= stolen

    if scanWork == 0 {
      return
    }
  }
    ...
}

İşaretleme yardımı yüksek yük koşulları altında bir dengeleme önlemidir. Kullanıcı programlarının bellek tahsis etme hızı işaretleme hızından çok daha yüksektir. Tahsis edilen her bayt bellek için, karşılık gelen miktarda işaretleme işi yapılır.

İşaretleme Sonlandırma

Tüm ulaşılabilir gri nesneler siyah olarak renklendirildiğinde, durum _GCmark'tan _GCmarktermination'a geçer. Bu süreç runtime.gcMarkDone fonksiyonu tarafından tamamlanır. Başlangıçta, hala yürütülecek görev olup olmadığını kontrol eder.

go
top:

  if !(gcphase == _GCmark && work.nwait == work.nproc && !gcMarkWorkAvailable(nil)) {
    return
  }

  gcMarkDoneFlushed = 0
  // Write barrier'lar tarafından kesilen tüm işaretleme işlemlerini toplu olarak yürüt
  forEachP(waitReasonGCMarkTermination, func(pp *p) {
    wbBufFlush1(pp)
    pp.gcw.dispose()
    if pp.gcw.flushedWork {
      atomic.Xadd(&gcMarkDoneFlushed, 1)
      pp.gcw.flushedWork = false
    }
  })

  if gcMarkDoneFlushed != 0 {
    goto top
  }

Yürütülecek global görevler ve yerel görevler kalmadığında, STW için runtime.stopTheWorldWithSema çağırır, sonra bazı temizleme işleri yapar.

go
// Yardımları ve arka plan işçilerini devre dışı bırak.
// Tıkanmış yardımları uyandırmadan önce bunu yapmalıyız.
atomic.Store(&gcBlackenEnabled, 0)

// CPU sınırlayıcıya GC yardımlarının artık sona ereceğini bildir
gcCPULimiter.startGCTransition(false, now)

// Tıkanmış tüm yardımları uyandır. Bunlar dünyayı yeniden
// başlattığımızda çalışacak.
gcWakeAllAssists()

// STW modunda, kullanıcı goroutine'lerini yeniden etkinleştir.
// Bunlar dünyayı yeniden başlattıktan sonra çalışmak üzere
// kuyruğa alınacak.
schedEnableUser(true)

// endCycle, tüm gcWork önbellek istatistiklerinin boşaltılmasına
// bağlıdır. Yukarıdaki sonlandırma algoritması, tahsislerden
// sonraki düzensiz barrier'a kadar bunu sağladı.
gcController.endCycle(now, int(gomaxprocs), work.userForced)

// İşaretleme sonlandırmayı gerçekleştir. Bu dünyayı yeniden
// başlatacak.
gcMarkTermination(stw)

İlk olarak, runtime.BlackenEnabled'i 0 olarak ayarlayın, işaretleme çalışmasının sona erdiğini gösterir. Sınırlayıcıya işaretleme yardımının sona erdiğini bildir, bellek barrier'larını kapat, yardım işaretleme nedeniyle uyuyan tüm goroutine'leri uyandır, sonra tüm kullanıcı goroutine'lerini yeniden uyandır. Ayrıca bu tur tarama çalışmasından çeşitli veriler toplayın, bir sonraki tarama turu için pacing algoritmasını ayarlayın. Temizleme işleri tamamlandıktan sonra, çöp nesneleri temizlemek için runtime.gcSweep fonksiyonunu çağırın, sonra program çalışmasını devam ettirmek için runtime.startTheWorldWithSema çağırın.

Barrier'lar

Bellek barrier'larının rolü nesne atama davranışını hooklamak olarak anlaşılabilir, atamadan önce belirtilen işlemleri gerçekleştirir. Bu hook kodu genellikle derleyici tarafından derleme sırasında koda eklenir. Daha önce belirtildiği gibi, eşzamanlı koşullarda üç renkli işaretleme sırasında nesne referanslarını eklemek ve silmek sorunlara neden olur. İkisi de yazma işlemleri olduğu için (silme null değer atamadır), onları kesen barrier'lar topluca write barrier'lar olarak adlandırılır. Ancak barrier mekanizmaları maliyetsiz değildir. Bellek yazma işlemlerini kesmek ek genel giderlere neden olur, bu yüzden barrier mekanizmaları sadece yığın üzerinde etkilidir. Uygulama karmaşıklığı ve performans genel giderini göz önüne alarak, yığınlar ve kayıtlar üzerinde etkili değildirler.

TIP

Barrier teknolojisinin Go'daki uygulama detayları hakkında daha fazla bilgi edinmek için Eliminate STW stack rescan adresini ziyaret edin ve orijinal İngilizce makaleyi okuyun. Bu makale bundan çok içerik referans almıştır.

Ekleme Write Barrier

Ekleme write barrier Dijkstra tarafından önerilmiştir. Güçlü üç renkli değişmezliği karşılar. Siyah bir nesne yeni bir beyaz nesne referansı eklediğinde, ekleme write barrier bu işlemi keser ve beyaz nesneyi gri olarak işaretler. Bu siyah nesnelerin beyaz nesnelere doğrudan referans vermesini önler, güçlü üç renkli değişmezliği garanti eder. Bu oldukça anlaşılması kolaydır.

Daha önce belirtildiği gibi, write barrier'lar yığınlara uygulanmaz. Eğer eşzamanlı işaretleme sırasında yığın nesnelerinin referans ilişkisi değişirse, örneğin yığındaki siyah bir nesnenin yığındaki beyaz bir nesneye referans vermesi, yığın nesnelerinin doğruluğunu garanti etmek için, işaretleme tamamlandıktan sonra yığındaki tüm nesneler tekrar gri nesneler olarak işaretlenmelidir, sonra yeniden taranır. Bu, bir işaretleme turunun yığın alanını iki kez taraması gerektiği anlamına gelir ve ikinci tarama STW gerektirir. Eğer programda aynı anda yüzlerce veya binlerce goroutine yığını varsa, bu tarama sürecinin zaman tüketimi göz ardı edilemez. Resmi istatistiklere göre, yeniden tarama zaman tüketimi yaklaşık 10-100 milisaniyedir.

Avantajlar: Tarama sırasında STW gerekmez.

Dezavantajlar: Doğruluğu garanti etmek için yığın alanının ikinci kez taranması gerekir, STW gerektirir.

Silme Write Barrier

Silme write barrier Yuasa tarafından önerilmiştir, başlangıçta anlık görüntü barrier'ı olarak da bilinir. Bu yöntem başlangıçta kök nesneleri anlık görüntü kaydetmek için STW gerektirir ve tüm kök nesneleri siyah ve tüm birinci seviye çocuk nesneleri gri olarak işaretler. Bu şekilde, geri kalan beyaz çocuk nesneler gri nesnelerin koruması altındadır. Go ekibi silme write barrier'ı doğrudan uygulamadı, ancak onu ekleme write barrier ile karıştırmayı seçti. Bu yüzden daha sonra anlaşılması kolay olması için, burada hala açıklanması gerekir. Silme write barrier'ın eşzamanlı koşullar altında doğruluğu garanti etme kuralı şudur: gri veya beyaz bir nesneden beyaz bir nesneye referans silindiğinde, beyaz nesne doğrudan gri olarak işaretlenir.

İki durumda yorumlayın:

  • Gri bir nesnenin beyaz bir nesneye referansını silme: Beyaz nesnenin aşağı akışının siyah bir nesne tarafından referans verilip verilmediği bilinmediği için, bu eylem gri nesnenin beyaz nesneyi korumasını kesebilir.
  • Beyaz bir nesnenin beyaz bir nesneye referansını silme: Beyaz nesnenin yukarı akışının gri tarafından korunup korunmadığı ve aşağı akışının siyah tarafından referans verilip verilmediği bilinmediği için, bu eylem de gri'nin beyazı korumasını kesebilir.

Her iki durumda da, silme write barrier referans verilen beyaz nesneyi gri olarak işaretler. Bu zayıf üç renkli değişmezliği karşılar. Bu konservatif bir yaklaşımdır çünkü yukarı akış ve aşağı akış durumları bilinmemektedir. Gri olarak işaretlemek, artık çöp nesne olarak görülmediği anlamına gelir. Referansı silmek nesneye erişilemez hale getirse bile (yani çöp nesne haline gelse), hala gri olarak işaretlenir. Bir sonraki tarama turunda serbest bırakılacaktır, bu nesnelerin yanlışlıkla toplanmasından kaynaklanan bellek hatalarından daha iyidir.

Avantajlar: Yığın nesnelerinin hepsi siyah olduğu için, yığın alanının ikinci kez taranmasına gerek yoktur.

Dezavantajlar: Tarama başlangıcında yığın alanındaki kök nesneleri anlık görüntü kaydetmek için STW gerektirir.

Hibrit Write Barrier

Go 1.8 sürümü yeni bir barrier mekanizması tanıttı: hibrit write barrier, ekleme write barrier ve silme write barrier karışımı, her ikisinin avantajlarını birleştirir:

  • Ekleme write barrier başlangıçta anlık görüntü için STW gerektirmez.
  • Silme write barrier yığın alanının ikinci kez taranması için STW gerektirmez.

Aşağıda resmi tarafından verilen sözde kod:

writePointer(slot, ptr):
    shade(*slot)
    if current stack is grey:
        shade(ptr)
    *slot = ptr

İçerideki bazı kavramları basitçe açıklayın: slot diğer nesnelere bir referansı temsil eden bir işaretçidir, *slot orijinal nesnedir, ptr yeni nesnedir, *slot=ptr bir atama işlemidir, nesnenin referansını değiştirmeye eşdeğerdir. Null atama referansı silmektir. shade() bir nesneyi gri olarak işaretlemek demektir. shade(*slot) orijinal nesneyi gri olarak işaretlemek demektir ve shade(ptr) yeni nesneyi gri olarak işaretlemek demektir. Aşağıda bir örnek diyagram vardır. Nesne 1'in başlangıçta nesne 2'ye referans verdiğini varsayalım, sonra kullanıcı programı nesne 1'in nesne 3'e referans vermesi için referansı değiştirir. Hibrit write barrier bu davranışı yakalar, burada *slot nesne 2'yi temsil eder ve ptr nesne 1'i temsil eder.

Resmi yukarıdaki sözde kodun etkisini tek bir cümlede özetledi:

the write barrier shades the object whose reference is being overwritten, and, if the current goroutine's stack has not yet been scanned, also shades the reference being installed.

Çevirisi, hibrit write barrier bir yazma işlemini kestiğinde, orijinal nesneyi gri olarak işaretler. Eğer mevcut goroutine'in yığını henüz taranmadıysa, yeni nesneyi de gri olarak işaretler.

İşaretleme çalışması başladığında, kök nesneleri toplamak için yığın alanı taranmalıdır. Bu zamanda, hepsi doğrudan siyah olarak işaretlenir. Bu süre zarfında, oluşturulan yeni nesneler de yığındaki tüm nesnelerin siyah nesneler olduğunu garanti etmek için siyah olarak işaretlenir. Bu yüzden sözde koddaki current stack is grey mevcut goroutine'in yığınının henüz taranmadığı anlamına gelir. Bu nedenle, goroutine yığınlarının sadece iki durumu vardır: ya hepsi siyah ya da hepsi gri. Hepsi griden hepsi siyaha geçiş sürecinde, mevcut goroutine duraklatılmalıdır. Bu yüzden hibrit write barrier altında, hala yerel STW vardır. Goroutine yığını tamamen siyah olduğunda, güçlü üç renkli değişmezlik bu noktada karşılanmalıdır. Çünkü taramadan sonra, yığındaki siyah nesneler sadece gri nesnelere referans verir. Siyah nesnelerin beyaz nesnelere doğrudan referans verdiği bir durum olmayacaktır. Bu yüzden bu noktada, ekleme write barrier gerekmez, sözde koda karşılık gelir:

if current stack is grey:
        shade(ptr)

Ancak zayıf üç renkli değişmezliği karşılamak için silme write barrier hala gereklidir, bu:

shade(*slot)

Tarama tamamlandıktan sonra, yığın alanındaki nesneler zaten tamamen siyah olduğu için, yığın alanının ikinci kez taranmasına gerek yoktur, STW zamanından tasarruf sağlar.

Bu noktada, Go 1.8 sürümünden sonra, Go büyük ölçüde çöp toplamanın temel çerçevesini kurmuştur. Çöp toplama ile ilgili sonraki sürüm optimizasyonları da hibrit write barrier temelinde inşa edilmiştir. Çoğu STW büyük ölçüde ortadan kaldırıldığından, çöp toplamanın ortalama gecikmesi mikrosaniye seviyesine indirilmiştir.

Gölgeleme Önbelleği

Daha önce bahsedilen barrier mekanizmalarında, yazma işlemlerini keserken, nesne renkleri hemen işaretlenir. Hibrit write barrier benimsendikten sonra, hem orijinal nesne hem de yeni nesne renklendirilmesi gerektiği için, iş yükü iki katına çıkar ve derleyici tarafından eklenen kod da artar. Optimize etmek için, Go 1.10 sürümünde, write barrier'lar gölgeleme yaparken nesne renklerini hemen işaretlemez. Bunun yerine, orijinal nesne ve yeni nesne bir önbellek havuzunda saklanır. Belirli bir miktar biriktirildikten sonra, toplu işaretleme yapılır. Bu daha verimlidir.

Önbelleklemeden sorumlu yapı runtime.wbBuf'dur, bu aslında boyutu 512 olan bir dizidir.

go
type wbBuf struct {
  next uintptr
  end uintptr
  buf [wbBufEntries]uintptr
}

Her P'nin yerel olarak böyle bir önbelleği vardır:

go
type p struct {
    ...
  wbBuf wbBuf
    ...
}

İşaretleme çalışması sırasında, eğer gcw kuyruğunda mevcut gri nesneler yoksa, önbellekteki nesneler yerel kuyruğa konur.

go
func gcDrain(gcw *gcWork, flags gcDrainFlags) {
  for !(gp.preempt && (preemptible || sched.gcwaiting.Load() || pp.runSafePointFn != 0)) {
    if work.full == 0 {
      gcw.balance()
    }
    b := gcw.tryGetFast()
    if b == 0 {
      b = gcw.tryGet()
      if b == 0 {
        // Write barrier önbelleğini boşalt
        wbBufFlush()
        b = gcw.tryGet()
      }
    }
    if b == 0 {
      break
    }
    scanobject(b, gcw)
  }
}

Başka bir durum, işaretleme sonlandırma oluştuğunda, her P'nin yerel wbBuf'u da kalan gri nesneler için kontrol edilir:

go
func gcMarkDone() {
    ...
  forEachP(waitReasonGCMarkTermination, func(pp *p) {

    wbBufFlush1(pp)

    pp.gcw.dispose()

  })
    ...
}

Temizleme

Çöp toplamada, en önemli kısım çöp nesneleri nasıl bulmaktır, bu tarama ve işaretleme çalışmasıdır. İşaretleme çalışması tamamlandıktan sonra, temizleme çalışması nispeten daha az karmaşıktır. Sadece işaretlenmemiş nesneleri geri dönüştürmesi ve bırakması gerekir. Bu kısmın kodu principalmente runtime/mgcsweep.go dosyasındadır. Dosyadaki yorumlara göre, Go'nun temizleme algoritmaları iki türe ayrılır.

Nesne Temizleme

Nesne temizleme çalışması işaretleme sonlandırma aşaması sırasında runtime.sweepone tarafından tamamlanır. Süreç asenkrondur. Temizleme sırasında, bellek birimlerinde işaretlenmemiş nesneleri bulmaya ve onları geri dönüştürmeye çalışır. Eğer tüm bir bellek birimi işaretlenmemişse, o zaman tüm birim geri dönüştürülür.

go
func sweepone() uintptr {
  sl := sweep.active.begin()
  npages := ^uintptr(0)
  var noMoreWork bool
  for {
    s := mheap_.nextSpanForSweep()
    if s == nil {
      noMoreWork = sweep.active.markDrained()
      break
    }
    if state := s.state.get(); state != mSpanInUse {
      continue
    }
        // Geri dönüştürücü almaya çalış
    if s, ok := sl.tryAcquire(s); ok {
      npages = s.npages
            // Temizle
      if s.sweep(false) {
        mheap_.reclaimCredit.Add(npages)
      } else {
        npages = 0
      }
      break
    }
  }
  sweep.active.end(sl)
  return npages
}

Nesne temizleme algoritması için, tüm birimi geri dönüştürmek nispeten zordur, bu yüzden ikinci bir temizleme algoritması vardır.

Birim Temizleme

Birim temizleme çalışması bellek tahsisinden önce yapılır, runtime.mheap.reclaim metodu tarafından tamamlanır. Yığında tüm nesneleri işaretlenmemiş bellek birimlerini arar, sonra tüm birimi geri dönüştürür.

go
func (h *mheap) reclaim(npage uintptr) {
  mp := acquirem()
  trace := traceAcquire()
  if trace.ok() {
    trace.GCSweepStart()
    traceRelease(trace)
  }
  arenas := h.sweepArenas
  locked := false
  for npage > 0 {
    if credit := h.reclaimCredit.Load(); credit > 0 {
      take := credit
      if take > npage {
        take = npage
      }
      if h.reclaimCredit.CompareAndSwap(credit, credit-take) {
        npage -= take
      }
      continue
    }

    idx := uintptr(h.reclaimIndex.Add(pagesPerReclaimerChunk) - pagesPerReclaimerChunk)
    if idx/pagesPerArena >= uintptr(len(arenas)) {
      h.reclaimIndex.Store(1 << 63)
      break
    }

    nfound := h.reclaimChunk(arenas, idx, pagesPerReclaimerChunk)
    if nfound <= npage {
      npage -= nfound
    } else {
      h.reclaimCredit.Add(nfound - npage)
      npage = 0
    }
  }

  trace = traceAcquire()
  if trace.ok() {
    trace.GCSweepDone()
    traceRelease(trace)
  }
  releasem(mp)
}

Bellek birimleri için, temizleme durumlarını belirtmek için kullanılan bir sweepgen alanı vardır:

  • mspan.sweepgen == mheap.sweepgen - 2: Bellek biriminin temizlenmesi gerekir.
  • mspan.sweepgen == mheap.sweepgen - 1: Bellek birimi temizleniyor.
  • mspan.sweepgen == mheap.sweepgen: Bellek birimi temizlendi ve normal şekilde kullanılabilir.
  • mspan.sweepgen == mheap.sweepgen + 1: Bellek birimi önbellekte ve temizlenmesi gerekir.
  • mspan.sweepgen == mheap.sweepgen + 3: Bellek birimi temizlendi ancak hala önbellekte.

mheap.sweepgen her GC turu ile artar ve her seferinde 2 artar.

Golang by www.golangdev.cn edit