c/c と比較した場合、golang の大きな改善点は gc メカニズムの導入であり、ユーザーがメモリを管理する必要がなくなりました。それ自体により、メモリ リークによってプログラムによってもたらされるバグが大幅に軽減されますが、同時に gc は追加のパフォーマンス オーバーヘッドももたらし、場合によっては不適切な使用により gc がパフォーマンスのボトルネックになることさえあります。 gc への負担を軽減するためにオブジェクトの再利用に注意を払う必要があります。スライスと文字列は golang の基本的なタイプです。これらの基本的なタイプの内部メカニズムを理解すると、これらのオブジェクトをより適切に再利用できるようになります。
スライスと文字列の内部構造
スライスと文字列の内部構造string 構造は $GOROOT/src/reflect/value.go
type StringHeader struct { Data uintptr Len int } type SliceHeader struct { Data uintptr Len int Cap int }
にあります。文字列にはデータ ポインターと長さが含まれており、長さは不変であることがわかります
スライスにはデータ ポインタ、長さ、容量が含まれています。容量が十分でない場合は、新しいメモリが再適用されます。データ ポインタは新しいアドレスを指し、元のアドレス空間は解放されます。
これらの構造から、文字列とスライスの割り当て (パラメーターとして渡すことを含む) は、カスタム構造のようなデータ ポインターの単なる浅いコピーであることがわかります。
スライスの再利用
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) })
si1 と si2 は両方とも最初は同じ配列を指します。si2 で追加操作が実行されると、元の Cap 値が十分ではないため、新しいスペースを再適用する必要があります$GOROOT /src/reflect/value.go
内 このファイルには、新しい上限値の戦略も含まれています 関数 grow
では、上限が小さくなったとき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) }) })
si2 は si1 のスライスです。コードの最初の部分から、スライスがメモリを再割り当てしないことがわかります。 si2 と si1 のデータ ポインターは同じスライス アドレスを指しますが、コードの 2 番目の部分は si2 に新しい値を追加すると、まだメモリ割り当てがないことがわかり、この操作により値が割り当てられます。両方とも同じデータ領域を指しているため、si1 の値も変更されます。この機能を使用すると、si1 = si1[:0]
にするだけで、si1 の内容を継続的にクリアしてメモリの再利用を実現できます
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) }) })
実際には文字列の場合、一つだけ覚えておいてください。文字列は不変です。文字列操作は追加のメモリには適用されません (内部データ ポインタのみ)。私はかつて、文字列を格納するキャッシュを巧みに設計しました。実際、文字列自体が []byte
から作成されない限り、そうでない場合、文字列自体は別の文字列の部分文字列になります ( を通じて取得された文字列など) strings.Split
) は追加のスペースには適用されません。
golang 関連の技術記事をさらに詳しく知りたい場合は、golang チュートリアル列をご覧ください。