golang sliceとstring再利用

10161 ワード

c/c++に比べて、golangの大きな改善はgcメカニズムを導入し、ユーザー自身がメモリを管理する必要がなくなり、メモリ漏洩によるプログラムのバグを大幅に減少させることであるが、同時にgcも追加の性能オーバーヘッドをもたらし、使用が不適切であるため、gcが性能ボトルネックになることもあるため、golangプログラム設計の際、gcの圧力を低減するために、オブジェクトの再利用に特に注意しなければならない.一方、sliceとstringはgolangの基本タイプであり、これらの基本タイプの内部メカニズムを理解することで、これらのオブジェクトをよりよく再利用するのに役立ちます.
sliceとstring内部構造
sliceとstringの内部構造は$GOROOT/src/reflect/value.goの中で見つけることができます
type StringHeader struct {
    Data uintptr
    Len  int
}
type SliceHeader struct {    Data uintptr    Len  int    Cap  int
}

stringにはデータポインタと長さが含まれており、長さは可変ではありません.
sliceは1つのデータポインタ、1つの長さと1つの容量を含んで、容量が足りない時新しいメモリを再申請して、Dataポインタは新しいアドレスを指して、元のアドレス空間は解放されます
これらの構造からstringとsliceの付与値は,パラメータとして伝達することを含め,カスタム構造体と同様にDataポインタの浅いコピーにすぎないことがわかる.
slice再利用
append操作
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)
})

Si 1とsi 2は最初は同じ配列を指していたが、si 2に対してappend操作を実行すると、元のCap値が足りず、新しい空間を再申請する必要があるため、Data値が変化し、$GOROOT/src/reflect/value.goというファイルには新しいcap値に関する戦略があり、growという関数ではcapが1024未満の場合、倍増した.超えると25%ずつ増加するが、このようなメモリの増加は、データコピー(古いアドレスから新しいアドレスにコピー)だけでなく、余分な性能を消費する必要があり、古いアドレスメモリの解放はgcにも余分な負担をもたらすため、データの長さが分かる場合は、できるだけmake([]int, len, cap)を使用してメモリを割り当て、長さが分からない場合は、次のメモリ再利用方法を考慮できます.
メモリ再利用
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)
    })
})

Si 2はsi 1のスライスであり、第1のセグメントコードからスライスがメモリを再割り当てしていないことがわかり、si 2とsi 1のDataポインタが同じアドレスを指していることがわかり、第2のセグメントコードから、si 2の中にappendの新しい値を入力すると、メモリ割り当てが残っていないことがわかり、この操作によってsi 1の値も変化したことがわかります.両者は本来同じデータ領域を指すため,この特性を利用してsi1 = si1[:0]でsi 1のコンテンツを絶えずクリアし,メモリの多重化を実現することができる.
PS:copy(si2, si1)を使って深いコピーを実現できます
string
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)
    })
})

この例は比較的簡単で,文字列定数は同じアドレス領域を用いる.
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)
    })
})

同じ文字列の異なるサブストリングは、新しいメモリを追加的に申請することはありませんが、ここで同じ文字列は、str1.Data == str2.Data && str1.Len == str2.Lenではなくstr1 == str2を指します.次の例では、str1 == str2を説明しますが、データは同じではありません.
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)
    })
})

実際には文字列については、文字列は可変であり、任意の文字列の操作で追加のメモリを申請することはありません(内部データポインタのみの場合)、私は文字列を格納するためにcacheを賢く設計して文字列を保存し、重複文字列の占有スペースを減らしました.実際には、この文字列自体が[]byteによって作成されていない限り、この文字列自体は別の文字列のサブストリング(例えばstrings.Splitによって取得された文字列)であり、もともと余分なスペースを申請することはありません.
リファレンスリンク
  • Go Slices: usage and internals:https://blog.golang.org/go-slices-usage-and-internals
  • テストコードリンク:https://github.com/hatlonely/hellogolang/blob/master/internal/buildin/reuse_test.go

  • テキストリンク:http://hatlonely.com/2018/03/17/golang-slice-%E5%92%8C-string-%E9%87%8D%E7%94%A8/