Heim  >  Artikel  >  Backend-Entwicklung  >  Erfahren Sie in einem Artikel mehr über die Wiederverwendung von Golang-Slice und -String

Erfahren Sie in einem Artikel mehr über die Wiederverwendung von Golang-Slice und -String

藏色散人
藏色散人nach vorne
2021-07-16 15:34:282341Durchsuche

Im Vergleich zu C/C++ ist eine große Verbesserung von Golang die Einführung des GC-Mechanismus, der nicht mehr erfordert, dass Benutzer den Speicher selbst verwalten, wodurch die durch das Programm verursachten Fehler erheblich reduziert werden Speicherlecks, aber gleichzeitig bringt gc auch zusätzlichen Leistungsaufwand mit sich, und manchmal führt sogar eine unsachgemäße Verwendung dazu, dass gc zu einem Leistungsengpass wird. Daher sollte beim Entwerfen von Golang-Programmen besonderes Augenmerk auf die Wiederverwendung von Objekten gelegt werden, um den Druck zu verringern gc. Slice und String sind die Grundtypen von Golang. Das Verständnis der internen Mechanismen dieser Grundtypen wird uns helfen, diese Objekte besser wiederzuverwenden. Die interne Struktur von Slice und String ist in zu finden $GOROOT Finden Sie

type StringHeader struct {
    Data uintptr
    Len  int
}

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

in /src/reflect/value.go. Sie können sehen, dass eine Zeichenfolge einen Datenzeiger und eine Länge enthält Wenn die Kapazität nicht ausreicht, wird der neue Speicher erneut angewendet, der Datenzeiger zeigt auf die neue Adresse und der ursprüngliche Adressraum wird freigegeben. Aus diesen Strukturen ist ersichtlich, dass die Zeichenfolge zugewiesen ist und Slice beinhaltet die Übergabe als Parameter und benutzerdefiniert. Die Strukturen sind die gleichen, sie sind nur flache Kopien der Datenzeiger

Slice-Wiederverwendung$GOROOT/src/reflect/value.go 里面找到

si1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
si2 := si1
si2 = append(si2, 0)
Convey("重新分配内存", func() {
    header1 := (*reflect.SliceHeader)(unsafe.Pointer(&si1))
    header2 := (*reflect.SliceHeader)(unsafe.Pointer(&si2))
    fmt.Println(header1.Data)
    fmt.Println(header2.Data)
    So(header1.Data, ShouldNotEqual, header2.Data)
})

可以看到一个 string 包含一个数据指针和一个长度,长度是不可变的

slice 包含一个数据指针、一个长度和一个容量,当容量不够时会重新申请新的内存,Data 指针将指向新的地址,原来的地址空间将被释放

从这些结构就可以看出,string 和 slice 的赋值,包括当做参数传递,和自定义的结构体一样,都仅仅是 Data 指针的浅拷贝

slice 重用

append 操作

si1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
si2 := si1[:7]
Convey("不重新分配内存", func() {
    header1 := (*reflect.SliceHeader)(unsafe.Pointer(&si1))
    header2 := (*reflect.SliceHeader)(unsafe.Pointer(&si2))
    fmt.Println(header1.Data)
    fmt.Println(header2.Data)
    So(header1.Data, ShouldEqual, header2.Data)
})

Convey("往切片里面 append 一个值", func() {
    si2 = append(si2, 10)
    Convey("改变了原 slice 的值", func() {
        header1 := (*reflect.SliceHeader)(unsafe.Pointer(&si1))
        header2 := (*reflect.SliceHeader)(unsafe.Pointer(&si2))
        fmt.Println(header1.Data)
        fmt.Println(header2.Data)
        So(header1.Data, ShouldEqual, header2.Data)
        So(si1[7], ShouldEqual, 10)
    })
})

si1 和 si2 开始都指向同一个数组,当对 si2 执行 append 操作时,由于原来的 Cap 值不够了,需要重新申请新的空间,因此 Data 值发生了变化,在 $GOROOT/src/reflect/value.go 这个文件里面还有关于新的 cap 值的策略,在 grow 这个函数里面,当 cap 小于 1024 的时候,是成倍的增长,超过的时候,每次增长 25%,而这种内存增长不仅仅数据拷贝(从旧的地址拷贝到新的地址)需要消耗额外的性能,旧地址内存的释放对 gc 也会造成额外的负担,所以如果能够知道数据的长度的情况下,尽量使用 make([]int, len, cap) 预分配内存,不知道长度的情况下,可以考虑下面的内存重用的方法

内存重用

Convey("字符串常量", func() {
    str1 := "hello world"
    str2 := "hello world"
    Convey("地址相同", func() {
        header1 := (*reflect.StringHeader)(unsafe.Pointer(&str1))
        header2 := (*reflect.StringHeader)(unsafe.Pointer(&str2))
        fmt.Println(header1.Data)
        fmt.Println(header2.Data)
        So(header1.Data, ShouldEqual, header2.Data)
    })
})

si2 是 si1 的一个切片,从第一段代码可以看到切片并不重新分配内存,si2 和 si1 的 Data 指针指向同一片地址,而第二段代码可以看出,当我们往 si2 里面 append 一个新的值的时候,我们发现仍然没有内存分配,而且这个操作使得 si1 的值也发生了改变,因为两者本就是指向同一片 Data 区域,利用这个特性,我们只需要让 si1 = si1[:0] 就可以不断地清空 si1 的内容,实现内存的复用了

PS: 你可以使用 copy(si2, si1) 实现深拷贝

string

Convey("相同字符串的不同子串", func() {
    str1 := "hello world"[:6]
    str2 := "hello world"[:5]
    Convey("地址相同", func() {
        header1 := (*reflect.StringHeader)(unsafe.Pointer(&str1))
        header2 := (*reflect.StringHeader)(unsafe.Pointer(&str2))
        fmt.Println(header1.Data, str1)
        fmt.Println(header2.Data, str2)
        So(str1, ShouldNotEqual, str2)
        So(header1.Data, ShouldEqual, header2.Data)
    })
})

这个例子比较简单,字符串常量使用的是同一片地址区域

Convey("不同字符串的相同子串", func() {
    str1 := "hello world"[:5]
    str2 := "hello golang"[:5]
    Convey("地址不同", func() {
        header1 := (*reflect.StringHeader)(unsafe.Pointer(&str1))
        header2 := (*reflect.StringHeader)(unsafe.Pointer(&str2))
        fmt.Println(header1.Data, str1)
        fmt.Println(header2.Data, str2)
        So(str1, ShouldEqual, str2)
        So(header1.Data, ShouldNotEqual, header2.Data)
    })
})

相同字符串的不同子串,不会额外申请新的内存,但是要注意的是这里的相同字符串,指的是 str1.Data == str2.Data && str1.Len == str2.Len,而不是 str1 == str2,下面这个例子可以说明 str1 == str2 但是其 Data 并不相同

rrreee

实际上对于字符串,你只需要记住一点,字符串是不可变的,任何字符串的操作都不会申请额外的内存(对于仅内部数据指针而言),我曾自作聪明地设计了一个 cache 去存储字符串,以减少重复字符串所占用的空间,事实上,除非这个字符串本身就是由 []byte 创建而来,否则,这个字符串本身就是另一个字符串的子串(比如通过 strings.Split

Anhängeoperation

rrreeesi1 und si2 zeigen beide auf dasselbe Array bei Zuerst wird der Anhängevorgang für si2 ausgeführt, da der ursprüngliche Cap-Wert nicht ausreicht und erneut neuer Speicherplatz beantragt werden muss, sodass sich der Datenwert in der Datei $GOROOT/src/ geändert hat. reflektieren/value.go, es gibt auch eine Strategie für den neuen Obergrenzenwert grow Wenn die Obergrenze kleiner als 1024 ist, wächst sie exponentiell. Es wird jedes Mal um 25 % wachsen. Und dieses Speicherwachstum wird nicht nur durch das Kopieren von Daten (Kopieren von der alten Adresse auf die neue Adresse) zusätzliche Leistung verbrauchen, sondern auch durch die Freigabe des alten Adressspeichers eine zusätzliche Belastung für gc entstehen. Wenn Sie also die Länge der Daten kennen, versuchen Sie, make([]int, len, cap) zu verwenden. Speicher vorab zuweisen. Wenn die Länge nicht bekannt ist, können Sie die folgende Speicherwiederverwendung in Betracht ziehen Methode

Speicherwiederverwendung

rrreeesi2 ist ein Slice von si1, wie Sie am ersten Teil des Codes sehen können. Der Speicher wird dem Slice nicht neu zugewiesen. Die Datenzeiger von si2 und si1 zeigen auf dasselbe Wie aus dem zweiten Teil des Codes ersichtlich ist, stellen wir beim Anhängen eines neuen Werts an si2 fest, dass noch keine Speicherzuweisung vorhanden ist, und dieser Vorgang führt dazu, dass sich auch der Wert von si1 ändert, da beide auf zeigen Mit dieser Funktion müssen wir nur si1 = si1[:0] festlegen, um den Inhalt von si1 kontinuierlich zu löschen. Die Wiederverwendung des Speichers wird erreichtPS: Sie können copy(si2, si1) zum Implementieren einer tiefen Kopie
🎜string🎜rrreee🎜Dieses Beispiel ist relativ einfach, die String-Konstante wird für verschiedene Teilzeichenfolgen derselben Zeichenfolge im selben Adressbereich verwendet 🎜rrreee🎜 wird nicht angewendet neuer Speicher, aber es sollte beachtet werden, dass sich die gleiche Zeichenfolge hier auf str1.Data == str2.Data && str1 .Len == str2.Len und nicht auf str1 == str2. Das folgende Beispiel kann das veranschaulichen <code>str1 == str2, aber seine Daten sind nicht die gleichen🎜rrreee 🎜Tatsächlich müssen Sie sich bei Zeichenfolgen nur eines merken: Zeichenfolgen sind unveränderlich, und Jede Zeichenfolgenoperation erfordert keinen zusätzlichen Speicher (nur für interne Datenzeiger). Ich habe einmal einen Cache zum Speichern von Zeichenfolgen entwickelt, um den durch doppelte Zeichenfolgen belegten Speicherplatz zu reduzieren. Es sei denn, die Zeichenfolge selbst wird aus [] erstellt. Byte, andernfalls ist die Zeichenfolge selbst ein anderes Zeichen. Teilzeichenfolgen von Zeichenfolgen (z. B. Zeichenfolgen, die über strings.Split erhalten werden) beanspruchen keinen zusätzlichen Speicherplatz, daher ist dies einfach unnötig. 🎜🎜Weitere technische Artikel zum Thema Golang finden Sie in der 🎜🎜Golang🎜🎜Tutorial-Kolumne! 🎜🎜

Das obige ist der detaillierte Inhalt vonErfahren Sie in einem Artikel mehr über die Wiederverwendung von Golang-Slice und -String. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:segmentfault.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen