1. Gün. Go Dilinde Ustalaşma Yolu: Dilimler ve Verimli Kullanım İlkeleri

0:00

Dilimler Hakkında 

Go dili tarafından sağlanan dizilere dayalı soyut bir veri türüdür. Go’da, diziler gerektiren çoğu senaryo dilimlerle değiştirilebilir. Go’da, dilimler dizileri mükemmel bir şekilde değiştirir ve daha esnek ve verimli bir veri dizisi erişim arayüzü sağlar.

Slice Tam Olarak Nedir? 

Go’da dilim kavramını anlamadan önce dizileri anlamamız gerekiyor.

Dilim Nedir? 

Go’da dizi, aynı türdeki öğeleri tutan sabit uzunlukta sürekli bir dizidir . Bu nedenle, Go dizilerinin iki özelliği vardır: ve . Bu iki türün aynı olduğu diziler eşdeğerdir. Yani:element typearray length

var a [8]int
var b [8]int

Burada dizi uzunluğu a8’dir ve eleman türü int’tir, yani ile aynıdır , dolayısıyla ve’nin eşdeğer olduğunu bsöyleyebiliriz .ab

Go’da değer semantiği vardır, yani bir dizi değişkeni dizinin ilk elemanına bir işaretçi değil, tüm diziyi temsil eder (C’de yapıldığı gibi). Go’da bir diziyi geçirmek değer kopyalamayı kullanır, bu da büyük eleman türlerine veya birçok elemana sahip dizileri geçirirken önemli performans kaybına yol açar.

C’de bu tür senaryolar dizi işaretçi türleri kullanılarak işlenebilir , ancak Go’da genellikle dilimler kullanılır.

Dilimler ve Diziler Arasındaki İlişki 

Dilimler ve diziler arasındaki ilişki , dosya tanımlayıcılarının dosyalara olan ilişkisine benzer . Go’da, diziler genellikle “altta yatan” veri depolaması olarak “arka plana” çekilirken, dilimler “ön plana” geçerek altta yatan depolamaya harici bir erişim arayüzü sağlar.

Bu nedenle dilimler dizilerin “tanımlayıcıları” olarak adlandırılabilir. Dilimlerin farklı işlevler arasında geçirilebilmesinin nedeni “tanımlayıcıların” özellikleridir; temel veri depolama alanı ne kadar büyük olursa olsun, tanımlayıcının boyutu her zaman sabittir.

Dilimler Oluşturmanın Yolları ve Çalışma Zamanında Temsilleri 

Dilimlerin gösterimi Go runtime:

//$GOROOT/src/runtime/slice.go
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

Gördüğümüz gibi bir dilimin üç öğesi vardır:

  • array: Altta yatan dizinin bir öğesine işaret eden ve aynı zamanda dilimin ilk öğesi olan bir işaretçi.
  • len: dilimin uzunluğu, dilimdeki mevcut öğe sayısıdır.
  • cap: dilimin maksimum kapasitesi, cap >= len.

Çalışma zamanında, her dilim yapının bir örneğidir runtime.sliceve aşağıdaki ifadeyi kullanarak bir dilim oluşturabiliriz:

s := make([]byte, 5)
1. Gün. Go Dilinde Ustalaşma Yolu: Dilimler ve Verimli Kullanım İlkeleri

Bir dilim oluşturduğumuzda derleyicinin otomatik olarak altta yatan bir dizi oluşturduğunu görebiliriz. Belirtmediğimizde capvarsayılan olarak olur cap = len.

Mevcut bir dizi üzerinde işlem yaparak da bir dilim oluşturabiliriz, buna şunu diyoruz slicing an array:

u := [10]byte{11, 12, 13, 14, 15, 16, 17, 18, 19, 20}
s := u[3:7]
1. Gün. Go Dilinde Ustalaşma Yolu: Dilimler ve Verimli Kullanım İlkeleri

sDilimin dizi üzerinde bir işlem penceresi açtığını görebiliriz us‘nin ilk elemanı u[3]‘dir ve aracılığıyla s4 dizi elemanını görebilir ve bunlar üzerinde işlem yapabiliriz. capDilimin uzunluğu, alttaki dizinin uzunluğuna bağlıdır; ‘den u[3]sonuna kadar elemanlar için 7 depolama yuvası vardır, bu yüzden s‘nin capuzunluğu 7’dir.

Elbette, aynı dizi üzerinde işlem yapan birden fazla dilim oluşturabiliriz, ancak birden fazla dilim aynı dizinin tanımlayıcıları olduğundan, herhangi bir dilimde yapılan herhangi bir değişiklik diğer dilimlere de yansıyacaktır.

Dilimler üzerinde işlem yaparak yeni dilimler de oluşturabiliriz, bu işlem olarak bilinir reslicing. Benzer şekilde, eski dilimden oluşturulan yeni dilim aynı temel diziyi paylaşır, bu nedenle yeni dilimde yapılan değişiklikler eski dilime de yansır.

Bu nedenle, dilimler parametre olarak geçirildiğinde, geçirilen şey ‘dir runtime.slice, bu nedenle altta yatan dizi ne kadar büyük olursa olsun, dilimlerin getirdiği performans kaybı çok küçük ve sabittir, hatta ihmal edilebilir düzeydedir. Bu, dilimlerin genellikle işlevlerde diziler yerine kullanılmasının bir nedenidir. Başka bir neden de dilimlerin işaretçi dizilerinden daha güçlü işlevsellik sağlayabilmesidir, örneğin dizin erişimi, sınır taşması denetimleri ve dinamik yeniden boyutlandırma.

Slices’ın Gelişmiş Özellikleri 

Dinamik Yeniden Boyutlandırma 

Dilimler sıfır değerli kullanılabilirlik kavramını karşılar çünkü dilimler gelişmiş bir özelliğe sahiptir: dynamic resizing. Sıfır değerli bir dilim bile önceden tanımlanmış işlev aracılığıyla eleman atama işlemlerini gerçekleştirebilir append.

var k []int
k = append(k, 1)

ksıfır değerine başlatılır, bu nedenle kkarşılık gelen bir altta yatan diziye bağlı değildir. Ancak, appendişlemden sonra, kaçıkça kendi altta yatan dizisine bağlıdır.

Şimdi, her birinden sonra gelen lenve ifadesini bir dizi pratik işlemle göstereceğim :capappend

var s []int
s = append(s, 1)
fmt.Println(len(s), cap(s)) // 1 1
s = append(s, 2)
fmt.Println(len(s), cap(s)) // 2 2
s = append(s, 3)
fmt.Println(len(s), cap(s)) // 3 4
s = append(s, 4)
fmt.Println(len(s), cap(s)) // 4 4
s = append(s, 5)
fmt.Println(len(s), cap(s)) // 5 8

lenDeğerinin doğrusal olarak büyüdüğü, ancak düzensiz olarak büyüdüğü görülebilir cap. Bu diyagram, appendişlemin dilimlerin dinamik olarak yeniden boyutlandırılmasına nasıl izin verdiğini görsel olarak gösterir:

1. Gün. Go Dilinde Ustalaşma Yolu: Dilimler ve Verimli Kullanım İlkeleri
  1. sBaşlangıçta sıfır değerinde olduğunu ve bu noktada shenüz altta yatan bir diziye bağlanmadığını görebiliriz .
  2. Daha sonra append, dilime 11 numaralı bir eleman eklenir s, bu eleman u11 uzunluğunda bir temel dizi tahsis eder ve sonra s‘nin iç noktasını array‘ye işaret eder u1, bu nedenle bu noktada len = 1, cap = 1.
  3. Daha sonra, append12’yi dilime eklemek için tekrar çağrıldığında s, dilimin 12’yi depolayacak alanı olmadığı açıktır. Bu nedenle, yeniden boyutlandırılması gerekir. u22 (* 2) uzunluğunda yeni bir temel dizi oluşturulur u1 array lengthve tüm öğeler u1‘e kopyalanır u2, son olarak ‘ arraye işaret edilir u2ve ayarlanır len = 2, cap = 2.
  4. append‘e 13 numaralı bir öğeyi eklemek için tekrar çağrıldığında , sdilim alanı yine yetersiz kalır ve başka bir yeniden boyutlandırma gerekir. Böylece, u34 ( * 2) uzunluğunda yeni bir temel dizi oluşturulur u2 array lengthve tüm öğeler u2‘e kopyalanır u3, sonra ‘ arraye işaret edilir u3ve len = 3, cap = 4ayarlanır.
  5. Tekrar çağrıldığında append, 14. eleman ‘e s, dilime eklenir cap = 4ve yeterli alan vardır. Bu nedenle, 14 bir sonraki eleman pozisyonuna, yani u3‘in sonuna yerleştirilir ve s‘in iç değeri len1 artırılarak 4 olur.
  6. Son olarak, append‘e 15 eklemek için tekrar çağrıldığında s, dilim len = 4, cap = 4ve alan yine yetersiz kalır, başka bir yeniden boyutlandırma gerekir. Böylece, u48 ( * 2) uzunluğunda yeni bir temel dizi oluşturulur u3 array lengthve tüm öğeler u3‘e kopyalanır u4, sonra ‘ arraye işaret edilir u4ve len = 5, cap = 8ayarlanır.

Dilimin altta yatan dizi alanı yetersiz olduğunda, dilimin ihtiyaç halinde otomatik olarak yeni bir altta yatan diziye tahsis edileceği gözlemlenebilir append. Yeni dizi uzunluğu belirli bir algoritmaya göre genişletilecektir ( growsliceişlevdeki işlevi inceleyin $GOROOT/src/runtime/slice.go). Yeni dizi oluşturulduktan sonra, eski dizideki tüm veriler yeni diziye kopyalanır ve ardından arrayyeni diziyi işaret ederek yeni diziyi dilimin altta yatan dizisi yaparken, eski dizi tarafından geri alınacaktır gc.

Ancak, kullanarak bir dilim oluşturursak slicing an array, dilim capüst sınıra değdiğinde ve dilim üzerinde bir işlem yaptığımızda append, dilim orijinal diziden ayrılmış olacaktır.

u := [6]int{1, 2, 3, 4, 5, 6}
s := u[2:4]
fmt.Printf("u = %v, s = %v len(s) = %d, cap(s) = %d\n", u, s, len(s), cap(s))
s = append(s, 7)
fmt.Printf("append 7: u = %v, s = %v len(s) = %d, cap(s) = %d\n", u, s, len(s), cap(s))
s = append(s, 8)
fmt.Printf("append 8: u = %v, s = %v len(s) = %d, cap(s) = %d\n", u, s, len(s), cap(s))
s = append(s, 9)
fmt.Printf("append 9: u = %v, s = %v len(s) = %d, cap(s) = %d\n", u, s, len(s), cap(s))

// output:
// u = [1 2 3 4 5 6], s = [3 4] len(s) = 2, cap(s) = 4
// append 7: u = [1 2 3 4 7 6], s = [3 4 7] len(s) = 3, cap(s) = 4
// append 8: u = [1 2 3 4 7 8], s = [3 4 7 8] len(s) = 4, cap(s) = 4
// append 9: u = [1 2 3 4 7 8], s = [3 4 7 8 9] len(s) = 5, cap(s) = 8
// append 10: u = [1 2 3 4 7 8], s = [3 4 7 8 9 10] len(s) = 6, cap(s) = 8

cap(s)Çıktı sonuçlarından, 9. öğe eklendikten sonra 8’e eşit olduğu, bunun dilimin dinamik yeniden boyutlandırılmasını tetiklediği ve bu noktada dilimin sartık dizisine bağlı olmadığı u, yani sartık bir tanımlayıcı olmadığı görülebilir u.

Dilimler Oluştururken Cap Parametresini Kullanmayı Deneyin 

İşlem, appenddilimlerin sıfır değerli kullanılabilirlik kavramını desteklemesini sağlayan ve dilimlerin kullanımını kolaylaştıran güçlü bir araçtır.

Ancak, prensipten, her yeniden boyutlandırma gerçekleştiğinde, eski temel dizideki tüm öğelerin yeni temel diziye kopyalanması gerektiğini görebiliriz. Bu adımda tüketilen kaynaklar, özellikle çok sayıda öğe olduğunda, hala önemlidir. Peki, aşırı bellek ayırma ve öğeleri kopyalama maliyetlerinden nasıl kaçınabiliriz?

Etkili bir çözüm, kod yazarken dilimin ihtiyaç duyduğu kapasiteyi bilinçli bir şekilde tahmin etmek ve dilimi oluştururken capparametreyi yerleşik fonksiyona geçirmektir.make

s := make([]T, len, cap)

Aşağıda iki durum için bir performans testi bulunmaktadır:

const sliceSize = 10000

func BenchmarkSliceInitWithoutCap(b *testing.B) {
	for n := 0; n < b.N; n++ {
		sl := make([]int, 0)
		for i := 0; i < sliceSize; i++ {
			sl = append(sl, i)
		}
	}
}

func BenchmarkSliceInitWithCap(b *testing.B) {
	for n := 0; n < b.N; n++ {
		sl := make([]int, 0, sliceSize)
		for i := 0; i < sliceSize; i++ {
			sl = append(sl, i)
		}
	}
}

Sonuçlar şöyle:

goos: windows
goarch: amd64
pkg: prometheus_for_go
cpu: AMD Ryzen 7 5800H with Radeon Graphics         
BenchmarkSliceInitWithoutCap
BenchmarkSliceInitWithoutCap-16    	   34340	     34443 ns/op
BenchmarkSliceInitWithCap
BenchmarkSliceInitWithCap-16       	  106251	     11122 ns/op
PASS

Dilimler için parametrenin kullanılmasının, sırasında caportalama bir performansa sahip olduğu görülebilir ; bu, parametrenin kullanılmamasına göre yaklaşık üç kat daha fazladır .11122 ns/opappendcap

cap Bu nedenle dilimler oluşturulurken parametrenin eklenmesi önerilir .

Video

Open-Sora 2.0 artık açık kaynaklı!

2025-3-14 21:23:59

GO

Go dilinde uygulanan SEO dostu Slug üreteci

2025-3-20 12:50:15

0 yanıt AMakale Yazarı MÜyeler
    Henüz bir tartışma yok, ne düşündüğünüzü bize bildirin
Kişisel Merkez
Sepet
Kuponlar
Bugünün Girişi
Yeni özel mesaj Özel mesaj listesi
aramak