Skip to content

slice

TIP

Bu makaleyi okumak unsafe standart kütüphanesi bilgisi gerektirir.

Slice Go dilinde en sık kullanılan veri yapısı olmalıdır, istisnasız (aslında yerleşik veri yapısı fazla yok) ve her yerde görülebilir. Temel kullanımı dil girişinde açıklanmıştır, şimdi içsel olarak nasıl göründüğüne ve nasıl çalıştığına bakalım.

Yapı

Slice uygulaması ile ilgili olarak, kaynak kod runtime/slice.go dosyasında yer almaktadır. Çalışma zamanında, slice bir struct olarak var olur, tipi runtime.slice, aşağıda gösterildiği gibi.

go
type slice struct {
  array unsafe.Pointer
  len   int
  cap   int
}

Bu struct'ın sadece üç alanı vardır:

  • array: alttaki diziye işaretçi
  • len: slice uzunluğu, dizide zaten bulunan eleman sayısını ifade eder
  • cap: slice kapasitesi, dizinin tutabileceği toplam eleman sayısını ifade eder

Yukarıdaki bilgilerden görebiliriz ki, slice'ların alttaki uygulaması hala dizilere bağlıdır. Genellikle, sadece diziye bir referans tutan ve kapasite ile uzunluğu kaydeden bir struct'tır. Bu, slice'ları geçirmeyi çok düşük maliyetli yapar, sadece veriye referansları kopyalar tüm veriyi kopyalamaz ve len ve cap kullanarak bir slice'ın uzunluğunu ve kapasitesini aldığınızda, diziyi dolaşmadan alan değerlerini almaya eşdeğerdir.

Ancak, bu da bazı o kadar belirgin olmayan sorunlar getirir. Aşağıdaki örneği düşünün:

go
package main

import "fmt"

func main() {
  s := make([]int, 0, 10)
  s = append(s, 1, 2, 3, 4, 5)
  s1 := s[:]
  s1[0] = 2
  fmt.Println(s)
}
[2 2 3 4 5]

Yukarıdaki kodda, s1 dilimleme ile yeni bir slice oluşturur, ancak o ve kaynak slice aynı alttaki diziye referans verir. s1'de veriyi değiştirmek s'in de değişmesine neden olur. Bu nedenle, slice'ları kopyalarken, copy fonksiyonu kullanılmalıdır, bu orijinalden tamamen bağımsız bir slice oluşturur. Başka bir örneğe bakalım:

go
func main() {
  s := make([]int, 0, 10)
  s = append(s, 1, 2, 3, 4, 5)
  s1 := s[:]
  s1 = append(s1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
  s1[0] = 10
  fmt.Println(s)
  fmt.Println(s1)
}
[1 2 3 4 5]
[10 2 3 4 5 1 2 3 4 5 6 7 8 9 10]

Yine dilimleme kullanarak slice'ları kopyalıyoruz, ancak bu sefer kaynak slice'ı etkilemeyecek. Başlangıçta, s1 ve s aynı diziye işaret ediyordu, ancak daha sonra s1'e çok fazla eleman eklendi, dizinin kapasitesini aştı, bu yüzden elemanları tutmak için daha büyük yeni bir dizi tahsis edildi. Bu nedenle, sonunda farklı dizilere işaret ediyorlar. Şimdi sorun olmadığını düşünüyorsunuz? Başka bir örneğe bakalım:

go
package main

import "fmt"

func main() {
  s := make([]int, 0, 10)
  appendData(s, 1, 2, 3, 4, 5, 6)
  fmt.Println(s)
}

func appendData[T comparable](s []T, data ...T) {
  s = append(s, data...)
}
[]

Açıkça elemanlar eklendi, ancak yazdırılan boş bir slice. Aslında, veri gerçekten slice'a eklendi, sadece alttaki diziye yazıldı. Go'da, fonksiyon parametreleri değer olarak geçirilir, bu nedenle parametre s aslında kaynak slice struct'ının bir kopyasıdır. append işlemi, elemanlar eklendikten sonra güncellenmiş uzunluğa sahip bir slice struct döndürür, ancak bu parametre s'e atanır, kaynak slice s'e değil, bu yüzden ikisi aslında ilişkili değildir.

Bir slice için, erişebileceği ve değiştirebileceği başlangıç pozisyonu diziye referans pozisyonuna bağlıdır, ofset struct'ta kaydedilen uzunluk tarafından belirlenir. Struct'taki işaretçi sadece başlangıca değil, dizinin ortasına da işaret edebilir, aşağıdaki şekilde gösterildiği gibi.

Tek bir alttaki diziye birçok slice referans verebilir, farklı referans pozisyonları ve aralıkları ile, yukarıdaki şekilde gösterildiği gibi. Bu durum genellikle slice'ları dilimlerken oluşur, aşağıdaki koda benzer:

go
s := make([]int, 0, 10)
s1 := s[:4]
s2 := s[4:6]
s3 := s[7:]

Dilimlerken, yeni slice'ın kapasitesi dizi uzunluğu eksi yeni slice'ın referans verdiği başlangıç pozisyonuna eşittir. Örneğin, s[4:6] ile oluşturulan yeni slice'ın kapasitesi 6 = 10 - 4'tür. Elbette, slice'ların referans verdiği aralık bitişik olmak zorunda değildir, iç içe de olabilirler. Ancak, bu önemli sorunlara neden olabilir, mevcut slice'ın verisi başka bir slice tarafından bilginiz olmadan değiştirilebilir. Örneğin, yukarıdaki şekildeki mor slice'da, eğer daha sonra eleman eklemek için append kullanılırsa, yeşil slice'ın ve mavi slice'ın verisinin üzerine yazabilir. Bu durumu önlemek için, Go dilimlerken kapasite aralığını ayarlamaya izin verir, söz dizimi aşağıdaki gibidir:

go
s4 = s[4:6:6]

Bu durumda, kapasitesi 2 ile sınırlıdır, bu nedenle eleman eklemek genişlemeyi tetikler. Genişlemeden sonra, yeni bir dizi olur, kaynak dizi ile ilişkisi yoktur, bu yüzden herhangi bir etki olmayacaktır. Slice sorunlarının sonu olduğunu mu düşünüyorsunuz? Aslında hayır. Başka bir örneğe bakalım:

go
package main

import "fmt"

func main() {
  s := make([]int, 0, 10)
  // Eklenecek eleman sayısı kapasiteden biraz büyük
  appendData(s, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
  fmt.Println(s)
}

func appendData[T comparable](s []T, data ...T) {
  s = append(s, data...)
}
[]

Kod önceki örnekle aynı değil, sadece eklenecek eleman sayısını slice kapasitesinden biraz büyük olacak şekilde giriş parametrelerini değiştirdi. Bu, eklendiğinde genişlemeyi tetikler, bu nedenle veri sadece kaynak slice s'e eklenmez, hatta işaret ettiği alttaki diziye bile yazılmaz. Bunu unsafe işaretçilerle doğrulayabiliriz, aşağıda gösterildiği gibi:

go
package main

import (
  "fmt"
  "unsafe"
)

func main() {
  s := make([]int, 0, 10)

  // Eklenecek eleman sayısı kapasiteden biraz büyük
  appendData(s, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
  fmt.Println("ori slice", unsafe.SliceData(s))
  unsafeIterator(unsafe.Pointer(unsafe.SliceData(s)), cap(s))
}

func appendData[T comparable](s []T, data ...T) {
  s = append(s, data...)
  fmt.Println("new slice", unsafe.SliceData(s))
  unsafeIterator(unsafe.Pointer(unsafe.SliceData(s)), cap(s))
}

func unsafeIterator(ptr unsafe.Pointer, offset int) {
  for ptr, i := ptr, 0; i < offset; ptr, i = unsafe.Add(ptr, unsafe.Sizeof(int(0))), i+1 {
    elem := *(*int)(ptr)
    fmt.Printf("%d, ", elem)
  }
  fmt.Println()
}
new slice 0xc0000200a0
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0,
ori slice 0xc000018190
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,

Gördüğünüz gibi, kaynak slice'ın alttaki dizisi boştur, hiç bir şey yoktur ve tüm veri yeni diziye yazılır. Ancak bu kaynak slice ile ilgili değildir, çünkü append yeni bir referans döndürse de, sadece formal parametre s'in değeri değiştirilir, bu kaynak slice s'i etkilemez. Struct olarak slice'lar gerçekten onları çok hafif yapabilir, ancak yukarıdaki sorunlar aynı şekilde göz ardı edilemez, özellikle gerçek kodda bu sorunlar genellikle çok gizlidir ve keşfedilmesi zordur.

Oluşturma

Çalışma zamanında, make fonksiyonu kullanarak slice oluşturma işi runtime.makeslice tarafından yapılır. Mantığı oldukça basittir ve fonksiyon imzası aşağıdaki gibidir:

go
func makeslice(et *_type, len, cap int) unsafe.Pointer

Üç parametre alır: eleman tipi, uzunluk, kapasite ve tamamlandıktan sonra alttaki dizinin işaretçisini döndürür. Kodu aşağıdaki gibidir:

go
func makeslice(et *_type, len, cap int) unsafe.Pointer {
    // Gerekli toplam belleği hesapla, çok büyükse taşmaya neden olur
    // mem = sizeof(et) * cap
  mem, overflow := math.MulUintptr(et.Size_, uintptr(cap))
  if overflow || mem > maxAlloc || len < 0 || len > cap {
        // mem = sizeof(et) * len
    mem, overflow := math.MulUintptr(et.Size_, uintptr(len))
    if overflow || mem > maxAlloc || len < 0 {
      panicmakeslicelen()
    }
    panicmakeslicecap()
  }

    // Her şey yolundaysa bellek tahsis et
  return mallocgc(mem, et, true)
}

Gördüğünüz gibi, mantık çok basittir, toplamda sadece iki şey yapar:

  • Gerekli belleği hesapla
  • Bellek alanı tahsis et

Koşul kontrolleri başarısız olursa, doğrudan panic yapar:

  • Bellek hesaplama taşması
  • Hesaplama sonucu maksimum tahsis edilebilir bellekten büyük
  • Uzunluk ve kapasite geçersiz

Hesaplanan bellek 32KB'tan büyükse, yığında tahsis edilecektir. Bundan sonra, alttaki dizinin işaretçisini döndürür. runtime.slice struct'ını oluşturma işi makeslice fonksiyonu tarafından yapılmaz. Aslında, struct'ı oluşturmak derleme sırasında yapılır. Runtime makeslice fonksiyonu sadece bellek tahsis etmekten sorumludur, aşağıdaki koda benzer:

go
var s runtime.slice
s.array = runtime.makeslice(type,len,cap)
s.len = len
s.cap = cap

Eğer ilgileniyorsanız, oluşturulan ara koda bakabilirsiniz, buna benzer:

go
name s.ptr[*int]: v11
name s.len[int]: v7
name s.cap[int]: v8

Eğer slice oluşturmak için dizi kullanılıyorsa, aşağıdaki gibi:

go
var arr [5]int
s := arr[:]

Bu süreç aşağıdaki koda benzer:

go
var arr [5]int
var s runtime.slice
s.array = &arr
s.len = len
s.cap = cap

Go doğrudan diziyi slice'ın alttaki dizisi olarak kullanır, bu nedenle slice'ta veriyi değiştirmek dizinin verisini de etkiler. Dizi kullanarak slice oluştururken, uzunluk high-low'a eşittir ve kapasite max-low'a eşittir, burada max varsayılan olarak dizi uzunluğudur veya dilimlerken kapasiteyi manuel olarak belirtebilirsiniz, örneğin:

go
var arr [5]int
s := arr[2:3:4]

Erişim

Slice'a erişim dizi indisleri kullanılarak yapılır:

go
elem := s[i]

Slice erişim işlemleri derleme sırasında tamamlanır, erişmek için ara kod oluşturur. Son oluşturulan kod aşağıdaki pseudocode olarak anlaşılabilir:

go
p := s.ptr
e := *(p + sizeof(elem(s)) * i)

Aslında, ilgili indekslerdeki elemanlara işaretçi işlemleriyle erişir, cmd/compile/internal/ssagen.exprCheckPtr fonksiyonunun aşağıdaki kısmına karşılık gelir:

go
case ir.OINDEX:
    n := n.(*ir.IndexExpr)
    switch {
    case n.X.Type().IsSlice():
        // İşaretçiyi ofsetle
        p := s.addr(n)
        return s.load(n.X.Type().Elem(), p)

len ve cap fonksiyonlarını kullanarak slice uzunluğuna ve kapasitesine erişirken, aynı prensip geçerlidir, ayrıca cmd/compile/internal/ssagen.exprCheckPtr fonksiyonunun bir kısmına karşılık gelir:

go
case ir.OLEN, ir.OCAP:
    n := n.(*ir.UnaryExpr)
    switch {
    case n.X.Type().IsSlice():
        op := ssa.OpSliceLen
        if n.Op() == ir.OCAP {
            op = ssa.OpSliceCap
        }
        return s.newValue1(op, types.Types[types.TINT], s.expr(n.X))

Gerçek oluşturulan kodda, slice struct'ındaki len alanına işaretçiyi hareket ettirerek erişir, aşağıdaki pseudocode olarak anlaşılabilir:

go
p := &s
len := *(p + 8)
cap := *(p + 16)

Aşağıdaki koda sahip olduğumuzu varsayalım:

go
func lenAndCap(s []int) (int, int) {
  l := len(s)
  c := cap(s)
  return l, c
}

Oluşturulan ara kodun bir aşamada büyük olasılıkla şöyle görünecektir:

go
v9 (+9) = ArgIntReg <int> {s+8} [1] : BX (l[int], s+8[int])
v10 (+10) = ArgIntReg <int> {s+16} [2] : CX (c[int], s+16[int])
v1 (?) = InitMem <mem>
v3 (11) = Copy <int> v9 : AX
v4 (11) = Copy <int> v10 : BX
v11 (+11) = MakeResult <int,int,mem> v3 v4 v1 : <>
Ret v11 (+11)
name l[int]: v9
name c[int]: v10
name s+16[int]: v10
name s+8[int]: v9

Yukarıdan görebiliriz ki, biri 8 ekler, biri 16 ekler, açıkça işaretçi ofseti ile slice alanlarına erişir.

Eğer uzunluk ve kapasite derleme sırasında çıkarılabiliyorsa, runtime'da değerleri almak için işaretçileri ofsetlemeye gerek yoktur. Örneğin, aşağıdaki durum işaretçi hareketi gerektirmez:

go
s := make([]int, 10, 20)
l := len(s)
c := cap(s)

Değişkenler l ve c değerleri doğrudan 10 ve 20 ile değiştirilir.

Yazma

Değiştirme

go
s := make([]int, 10)
s[0] = 100

İndis alt yazıları ile slice değerlerini değiştirirken, derleme sırasında, OpStore işlemi aşağıdakine benzer pseudocode oluşturur:

go
p := &s
l := *(p + 8)
if !IsInBounds(l,i) {
    panic()
}
ptr := (s.ptr + i * sizeof(elem) * i)
*ptr = val

Oluşturmanın bir aşamasında, ara kod büyük olasılıkla şöyle görünecektir:

go
v1 (?) = InitMem <mem>
v5 (8) = Arg <[]int> {s} (s[[]int])
v6 (?) = Const64 <int> [100]
v7 (?) = Const64 <int> [0]
v8 (+9) = SliceLen <int> v5
v9 (9) = IsInBounds <bool> v7 v8
v14 (?) = Const64 <int64> [0]
v12 (9) = SlicePtr <*int> v5
v15 (9) = Store <mem> {int} v12 v6 v1
v11 (9) = PanicBounds <mem> [0] v7 v7 v1
Exit v11 (9)

name s[[]int]: v5
name s[*int]:
name s+8[int]:

Gördüğünüz gibi, kod indeksin geçerli olup olmadığını kontrol etmek için slice uzunluğuna erişir ve sonunda elemanları işaretçiyi hareket ettirerek saklar.

Ekleme

append fonksiyonu kullanılarak slice'a elemanlar eklenebilir:

go
var s []int
s = append(s, 1, 2, 3)

Elemanlar eklendikten sonra, yeni bir slice struct döndürür. Genişleme oluşmadıysa, kaynak slice ile karşılaştırıldığında, sadece uzunluk güncellenir; aksi takdirde, yeni bir diziye işaret edecektir. append kullanım sorunları ile ilgili olarak, bu Yapı bölümünde ayrıntılı olarak açıklanmıştır, bu yüzden burada daha fazla açıklama yok. Aşağıda, append'in nasıl çalıştığına odaklanacağız.

Runtime'da, buna karşılık gelen runtime.appendslice gibi bir fonksiyon yoktur. Eleman ekleme işi aslında derleme sırasında yapılır. append fonksiyonu ilgili ara koda genişletilir. Yargılama kodu cmd/compile/internal/walk/assign.go walkassign fonksiyonundadır:

go
case ir.OAPPEND:
    // x = append(...)
    call := as.Y.(*ir.CallExpr)
    if call.Type().Elem().NotInHeap() {
       base.Errorf("%v can't be allocated in Go; it is incomplete (or unallocatable)", call.Type().Elem())
    }
    var r ir.Node
    switch {
    case isAppendOfMake(call):
       // x = append(y, make([]T, y)...)
       r = extendSlice(call, init)
    case call.IsDDD:
       r = appendSlice(call, init) // also works for append(slice, string).
    default:
       r = walkAppend(call, init, as)
    }

Gördüğünüz gibi, üç duruma ayrılır:

  • Birkaç eleman ekle
  • Bir slice ekle
  • Geçici olarak oluşturulmuş bir slice ekle

Aşağıda, oluşturulan kodun nasıl göründüğünü açıklayacağız, böylece append'in gerçekten nasıl çalıştığını anlayacaksınız. Kod oluşturma süreciyle ilgileniyorsanız, kendiniz öğrenebilirsiniz.

Eleman Ekleme

go
s = append(s, x, y, z)

Sadece sonlu sayıda eleman ekleniyorsa, walkAppend fonksiyonu tarafından aşağıdaki koda genişletilir:

go
// Eklenecek eleman sayısı
const argc = len(args) - 1
newLen := s.len + argc

// Genişleme gerekiyor mu
if uint(newLen) <= uint(s.cap) {
  s = s[:newLen]
} else {
  s = growslice(s.ptr, newLen, s.cap, argc, elemType)
}

s[s.len - argc] = x
s[s.len - argc + 1] = y
s[s.len - argc + 2] = z

İlk olarak eklenecek eleman sayısını hesaplar, sonra genişleme gerekip gerekmediğini belirler ve son olarak değerleri tek tek atar.

Slice Ekleme

go
s = append(s, s1...)

Doğrudan bir slice ekleniyorsa, appendSlice fonksiyonu tarafından aşağıdaki koda genişletilir:

go
newLen := s.len + s1.len
// uint olarak karşılaştır, böylece growslice taşmada panic yapabilir.
if uint(newLen) <= uint(s.cap) {
  s = s[:newLen]
} else {
  s = growslice(s.ptr, s.len, s.cap, s1.len, T)
}
memmove(&s[s.len-s1.len], &s1[0], s1.len*sizeof(T))

Öncekiyle aynı, yeni uzunluğu hesapla, genişleme gerekip gerekmediğini belirle. Farkı, Go'nun kaynak slice'tan elemanları tek tek eklememesi, doğrudan belleği kopyalamayı seçmesidir.

Geçici Slice Ekleme

go
s = append(s, make([]T, l2)...)

Geçici olarak oluşturulmuş bir slice ekleniyorsa, extendslice fonksiyonu tarafından aşağıdaki koda genişletilir:

go
if l2 >= 0 {
// Empty if block here for more meaningful node.SetLikely(true)
} else {
  panicmakeslicelen()
}
s := l1
n := len(s) + l2

if uint(n) <= uint(cap(s)) {
  s = s[:n]
} else {
  s = growslice(T, s.ptr, n, s.cap, l2, T)
}
// alttaki dizinin yeni kısmını temizle.
hp := &s[len(s)-l2]
hn := l2 * sizeof(T)
memclr(hp, hn)

Geçici olarak eklenen slice'lar için, Go geçici slice'ın uzunluğunu alır. Eğer mevcut slice'ın kapasitesi onu barındırmak için yetersizse, genişlemeyi deneyecektir. Bundan sonra, belleğin ilgili kısmını da temizler.

Genişleme

Yapı bölümünden biliyoruz ki, slice'ların altı hala bir dizidir. Diziler sabit uzunluklu veri yapılarıdır, ancak slice uzunluğu değişkendir. Dizi kapasitesi yetersiz olduğunda, slice veriyi saklamak için daha büyük bir bellek alanı talep eder, bu yeni bir dizidir, sonra eski veriyi kopyalar ve slice'ın referansı yeni diziye işaret edecektir. Bu sürece genişleme denir. Genişleme işi runtime'da runtime.growslice fonksiyonu tarafından yapılır, fonksiyon imzası aşağıdaki gibidir:

go
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice

Basit parametre açıklaması:

  • oldPtr: eski dizinin işaretçisi
  • newLen: yeni dizinin uzunluğu, newLen = oldLen + num
  • oldCap: eski slice'ın kapasitesi, eski dizinin uzunluğuna eşittir
  • et: eleman tipi

Dönüş değeri yeni bir slice döndürür, orijinal slice ile ilgisi yoktur. Tek ortakları kaydedilen verinin aynı olmasıdır.

go
var s []int
s = append(s, elems...)

append kullanarak eleman eklerken, orijinal slice'ın üzerine yazmak için dönüş değerini gerektirir. Eğer genişleme oluştuysa, döndürülen yeni bir slice'tır.

Genişleme sırasında, önce yeni uzunluk ve kapasite belirlenmelidir, aşağıdaki koda karşılık gelir:

go
oldLen := newLen - num
if newLen < 0 {
    panic(errorString("growslice: len out of range"))
}

if et.Size_ == 0 {
    return slice{unsafe.Pointer(&zerobase), newLen, newLen}
}

newcap := oldCap
// Kapasiteyi iki katına çıkar
doublecap := newcap + newcap
if newLen > doublecap {
    newcap = newLen
} else {
    const threshold = 256
    if oldCap < threshold {
        newcap = doublecap
    } else {
        for 0 < newcap && newcap < newLen {
            // newcap += 0.25 * newcap + 192
            newcap += (newcap + 3*threshold) / 4
        }
        // Sayısal taşma
        if newcap <= 0 {
            newcap = newLen
        }
    }
}

Yukarıdaki koddan, kapasitesi 256'dan az olan slice'lar için kapasite iki katına çıkar. Kapasitesi 256 veya daha büyük olan slice'lar için, en az orijinal kapasitenin 1.25 katı olacaktır. Mevcut slice küçük olduğunda, her seferinde iki katına çıkarmak sık genişlemeleri önleyebilir. Slice büyük olduğunda, genişleme oranı çok fazla bellek talep edip israf etmeyi önlemek için azalır.

Yeni uzunluk ve kapasiteyi aldıktan sonra, gerekli belleği hesapla, aşağıdaki koda karşılık gelir:

go
var overflow bool
var lenmem, newlenmem, capmem uintptr
switch {
    ...
    ...
  default:
    lenmem = uintptr(oldLen) * et.Size_
    newlenmem = uintptr(newLen) * et.Size_
    capmem, overflow = math.MulUintptr(et.Size_, uintptr(newcap))
    capmem = roundupsize(capmem)
    // Son kapasite
    newcap = int(capmem / et.Size_)
    capmem = uintptr(newcap) * et.Size_
}

if overflow || capmem > maxAlloc {
    panic(errorString("growslice: len out of range"))
}

Bellek hesaplama formülü mem = cap * sizeof(et). Bellek hizalaması kolaylığı için, hesaplanan bellek işlem sırasında 2'nin tam sayı kuvvetine yuvarlanır ve yeni kapasite tekrar hesaplanır. Eğer yeni kapasite hesaplama sırasında taşmaya neden olacak kadar büyükse veya yeni bellek maksimum tahsis edilebilir belleği aşarsa, panic yapar.

go
var p unsafe.Pointer
// Bellek tahsis et
p = mallocgc(capmem, nil, false)
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)

memmove(p, oldPtr, lenmem)
return slice{p, newLen, newcap}

Gerekli sonuçları hesapladıktan sonra, belirtilen boyutta bellek tahsis eder, sonra newLen'den newCap'e belleği temizler, eski dizi verisini yeni slice'a kopyalar ve son olarak slice struct'ını oluşturur.

Kopyalama

go
src := make([]int, 10)
dst := make([]int, 20)
copy(dst, src)

copy fonksiyonunu kullanarak slice'ları kopyalarken, cmd/compile/internal/walk.walkcopy tarafından oluşturulan kod nasıl kopyalanacağını belirler. Eğer runtime'da çağrılırsa, runtime.slicecopy fonksiyonunu kullanır, bu slice'ları kopyalamaktan sorumludur. Fonksiyon imzası aşağıdaki gibidir:

go
func slicecopy(toPtr unsafe.Pointer, toLen int, fromPtr unsafe.Pointer, fromLen int, width uintptr) int

Kaynak ve hedef slice'ların işaretçilerini ve uzunluklarını ve kopyalanacak uzunluğu width alır. Bu fonksiyonun mantığı çok basittir, aşağıda gösterildiği gibi:

go
func slicecopy(toPtr unsafe.Pointer, toLen int, fromPtr unsafe.Pointer, fromLen int, width uintptr) int {
  if fromLen == 0 || toLen == 0 {
    return 0
  }

  n := fromLen
  if toLen < n {
    n = toLen
  }

  if width == 0 {
    return n
  }

  // Kopyalanacak bayt sayısını hesapla
  size := uintptr(n) * width

  if size == 1 {
    *(*byte)(toPtr) = *(*byte)(fromPtr)
  } else {
    memmove(toPtr, fromPtr, size)
  }
  return n
}

width değeri iki slice'ın minimum uzunluğuna bağlıdır. Gördüğünüz gibi, slice'ları kopyalarken, elemanları tek tek dolaşarak kopyalamaz, doğrudan tüm alttaki dizinin belleğini kopyalar. Slice büyük olduğunda, belleği kopyalamanın performans etkisi az değildir.

Eğer runtime'da çağrılmazsa, aşağıdaki gibi koda genişletilir:

go
n := len(a)
if n > len(b) {
  n = len(b)
}
if a.ptr != b.ptr {
  memmove(a.ptr, b.ptr, n*sizeof(elem(a)))
}

Her iki yöntem de aynı şekilde çalışır, belleği kopyalayarak slice'ları kopyalar. memmove fonksiyonu assembly ile uygulanmıştır. Eğer ilgileniyorsanız, runtime/memmove_amd64.s dosyasında ayrıntıları inceleyebilirsiniz.

Temizleme

go
package main

func main() {
  s := make([]int, 0, 10)
  s = append(s, 1, 2, 3, 4, 5)
  clear(s)
}

go1.21 sürümünde, yeni bir yerleşik fonksiyon clear eklendi, bu bir slice'ın içeriğini temizlemek için kullanılabilir, veya daha doğrusu, tüm elemanları sıfır değerlere ayarlar. clear fonksiyonu bir slice üzerinde çalıştığında, derleyici bunu derleme sırasında cmd/compile/internal/walk.arrayClear fonksiyonu ile aşağıdaki forma genişletir:

go
if len(s) != 0 {
  hp = &s[0]
  hn = len(s)*sizeof(elem(s))
    if elem(s).hasPointer() {
        memclrHasPointers(hp, hn)
    }else {
        memclrNoHeapPointers(hp, hn)
    }
}

İlk olarak, slice uzunluğunun 0 olup olmadığını kontrol eder, sonra temizlenecek bayt sayısını hesaplar ve ardından elemanın işaretçi olup olmadığına bağlı olarak iki durumu işler. Ancak sonunda, memclrNoHeapPointers fonksiyonunu kullanacaktır, imzası aşağıdaki gibidir:

go
func memclrNoHeapPointers(ptr unsafe.Pointer, n uintptr)

İki parametre alır: biri başlangıç adresinin işaretçisidir ve diğeri ofsettir, yani temizlenecek bayt sayısıdır. Başlangıç bellek adresi, slice'ın tuttuğu referansın adresidir ve ofset n = sizeof(et) * len. Bu fonksiyon assembly ile uygulanmıştır. Eğer ilgileniyorsanız, runtime/memclr_amd64.s dosyasında ayrıntıları kontrol edebilirsiniz.

Bahsetmeye değer ki, eğer kaynak kod diziyi temizlemek için yinelemeyi kullanmaya çalışırsa, örneğin:

go
for i := range s {
  s[i] = ZERO_val
}

clear fonksiyonundan önce, bu genellikle slice'ların nasıl temizlendiğiydi. Derleme sırasında, bu kod şimdi cmd/compile/internal/walk.arrayRangeClear fonksiyonu tarafından aşağıdaki forma optimize edilir:

go
for i, v := range s {
    if len(s) != 0 {
        hp = &s[0]
        hn = len(s)*sizeof(elem(s))
        if elem(s).hasPointer() {
            memclrHasPointers(hp, hn)
        }else {
            memclrNoHeapPointers(hp, hn)
        }
        // Döngüyü durdur
        i = len(s) - 1
    }
}

Mantık yukarıdakiyle tamamen aynıdır, ekstra bir satır i = len(s)-1, amacı bellek temizlendikten sonra döngüyü durdurmaktır.

Yineleme

go
for i, e := range s {
  fmt.Println(i, e)
}

for range kullanarak bir slice'ı yinelerken, cmd/compile/internal/walk/range.go içindeki walkRange fonksiyonu tarafından aşağıdaki forma genişletilir:

go
// struct'ı kopyala
hs := s
// alttaki dizi işaretçisini al
hu = uintptr(unsafe.Pointer(hs.ptr))
v1 := 0
v2 := zero
for i := 0; i < hs.len; i++ {
    hp = (*T)(unsafe.Pointer(hu))
    v1, v2 = i, *hp
    ... döngü gövdesi ...
    hu = uintptr(unsafe.Pointer(hp)) + elemsize
}

Gördüğünüz gibi, for range uygulaması hala elemanları işaretçiyi hareket ettirerek yineler. Yineleme sırasında slice'ın güncellenmesini önlemek için, önceden struct'ın bir kopyası hs yapılır. Yineleme bittikten sonra işaretçinin sınır dışı belleğe işaret etmesini önlemek için, hu adresi saklamak için uintptr tipini kullanır ve sadece elemanlara erişmek gerektiğinde unsafe.Pointer'a dönüştürür.

for range'daki e olan v2 değişkeni, tüm yineleme sürecinde tek bir değişkendir. Sadece üzerine yazılacak, yeniden oluşturulmayacak. Bu nokta, Go geliştiricilerini on yıldır rahatsız eden döngü değişkeni sorununu tetikledi. go1.21 sürümüne kadar, yetkililer sonunda bunu çözmeye karar verdi. Gelecek sürüm güncellemelerinde, v2'nin oluşturma yöntemi şöyle olabilir:

go
v2 := *hp

Ara kod oluşturma süreci burada atlanmıştır, çünkü bu slice ile ilgili bilgiye ait değildir. Eğer ilgileniyorsanız, kendiniz öğrenebilirsiniz.

Golang by www.golangdev.cn edit