Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 16318] ファイルの概要

このコミットは、Go言語のランタイムにおけるメモリ確保(malloc)のパフォーマンスを測定するための新しいベンチマークを追加するものです。特に、アロケーションサイズ(8バイトと16バイト)と型情報(type info)の有無がメモリ確保のパスに与える影響を評価することを目的としています。

コミット

commit 915784e11a58189524c9797ad5e1c1fc43eb632b
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Wed May 15 21:22:32 2013 +0400

    runtime: add simple malloc benchmarks
    Allocs of size 16 can bypass atomic set of the allocated bit, while allocs of size 8 can not.
    Allocs with and w/o type info hit different paths inside of malloc.
    Current results on linux/amd64:
    BenchmarkMalloc8        50000000                43.6 ns/op
    BenchmarkMalloc16       50000000                46.7 ns/op
    BenchmarkMallocTypeInfo8        50000000                61.3 ns/op
    BenchmarkMallocTypeInfo16       50000000                63.5 ns/op
    
    R=golang-dev, remyoudompheng, minux.ma, bradfitz, iant
    CC=golang-dev
    https://golang.org/cl/9090045

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/915784e11a58189524c9797ad5e1c1fc43eb632b

元コミット内容

Goランタイムにシンプルなメモリ確保ベンチマークを追加。 16バイトのアロケーションは、アロケートされたビットのアトミックな設定をバイパスできるが、8バイトのアロケーションはできない。 型情報を持つアロケーションと持たないアロケーションは、malloc内部で異なるパスを通る。 linux/amd64での現在の結果: BenchmarkMalloc8 50000000 43.6 ns/op BenchmarkMalloc16 50000000 46.7 ns/op BenchmarkMallocTypeInfo8 50000000 61.3 ns/op BenchmarkMallocTypeInfo16 50000000 63.5 ns/op

変更の背景

Go言語のランタイムは、効率的なメモリ管理とガベージコレクション(GC)のために高度に最適化されています。メモリ確保のパフォーマンスは、Goアプリケーション全体のパフォーマンスに直接影響を与えるため、その挙動を詳細に理解し、最適化することは非常に重要です。

このコミットの背景には、特定のメモリ確保シナリオにおけるパフォーマンス特性を明らかにするという目的があります。具体的には、以下の点が挙げられます。

  1. アロケーションサイズによる挙動の違いの検証: Goのメモリ管理システムは、アロケーションサイズに応じて異なるパスを使用することがあります。特に、8バイトと16バイトという特定のサイズが言及されているのは、Goの内部的なメモリ管理単位(例えば、mspanのサイズクラスやアライメント要件)に関連している可能性が高いです。コミットメッセージにある「16バイトのアロケーションは、アロケートされたビットのアトミックな設定をバイパスできるが、8バイトのアロケーションはできない」という記述は、これらのサイズがランタイムの内部的な最適化にどのように影響するかを調査するためのものです。
  2. 型情報の有無による挙動の違いの検証: Goのガベージコレクタは、オブジェクトの型情報を使用して、ポインタが含まれているかどうかを判断し、到達可能なオブジェクトを追跡します。型情報を持つオブジェクトと持たないオブジェクトでは、メモリ確保時およびGC時の処理パスが異なる場合があります。このベンチマークは、型情報の有無がメモリ確保のオーバーヘッドにどの程度影響するかを測定することを目的としています。
  3. パフォーマンス回帰の検出: 新しいベンチマークを追加することで、将来のランタイムの変更がメモリ確保のパフォーマンスに悪影響を与えないかを継続的に監視できるようになります。これは、Goのパフォーマンスを維持・向上させる上で不可欠なプラクティスです。

これらのベンチマークは、Goランタイム開発者がメモリ管理のボトルネックを特定し、さらなる最適化の機会を見つけるための貴重なデータを提供します。

前提知識の解説

このコミットを理解するためには、Go言語のメモリ管理とガベージコレクションに関する基本的な知識が必要です。

Goのメモリ管理の概要

Goのランタイムは、独自のメモリ管理システムを持っています。これは、OSから大きなメモリブロックを確保し、それをGoのヒープとして管理します。ヒープは、オブジェクトのサイズに応じて異なる方法で管理されます。

  • mspan: Goのメモリ管理の基本的な単位はmspanです。これは、特定のサイズのオブジェクトを格納するために予約された、連続したメモリページのセットです。Goは、様々なサイズのmspanを事前に定義しており、小さなオブジェクト(small objects)はこれらのmspanから効率的に割り当てられます。
  • mcache: 各ゴルーチン(論理的なCPU)は、ローカルなメモリキャッシュであるmcacheを持っています。これにより、ロックなしで小さなオブジェクトを高速に割り当てることができます。mcacheが枯渇すると、mcentralから新しいmspanを取得します。
  • mcentral: mcentralは、特定のサイズのmspanのリストを管理します。これは、複数のmcache間で共有され、mcachemspanを要求したり、使い終わったmspanを返したりする際に使用されます。mcentralへのアクセスはロックによって保護されます。
  • mheap: mheapは、Goのヒープ全体を管理する最上位のコンポーネントです。mheapは、OSからメモリを要求し、それをmspanに分割してmcentralに提供します。大きなオブジェクト(large objects)は、mheapから直接割り当てられます。

アロケーションサイズとアライメント

Goのメモリ管理では、アロケーションサイズが非常に重要です。特に、CPUのワードサイズ(通常は8バイト)やキャッシュラインサイズ(通常は64バイト)に合わせたアライメントがパフォーマンスに影響を与えます。

  • 8バイトと16バイト: 多くのシステムでは、ポインタやint64のようなデータ型は8バイトです。16バイトは、2つのポインタやcomplex64のようなデータ型、あるいは特定の構造体のアライメント要件を満たすために重要なサイズです。コミットメッセージで言及されている「アロケートされたビットのアトミックな設定をバイパスできる」という点は、特定のサイズのアロケーションが、メモリブロックのステータスを更新する際のアトミック操作を回避できるような最適化パスを通ることを示唆しています。これは、ロックの競合を減らし、パフォーマンスを向上させるための重要な最適化です。

型情報(Type Information)とガベージコレクション

Goのガベージコレクタは、正確なGC(Precise GC)です。これは、ヒープ上のすべてのポインタを正確に識別し、到達可能なオブジェクトのみを保持することを意味します。

  • 型情報の役割: Goのコンパイラは、各型に関するメタデータ(型情報)を生成します。このメタデータには、その型がポインタを含むかどうか、含まれる場合はどのオフセットにポインタがあるか、といった情報が含まれます。ガベージコレクタは、この型情報を使用して、ヒープ上のオブジェクトをスキャンし、ポインタをたどって到達可能なオブジェクトをマークします。
  • 型情報の有無によるアロケーションパスの違い: 型情報を持つオブジェクト(例えば、構造体やスライス、マップなど)を割り当てる場合、ランタイムはGCがそのオブジェクトを正しくスキャンできるように、追加のメタデータを関連付ける必要があります。一方、型情報を持たないオブジェクト(例えば、[]byteのような純粋なデータ)を割り当てる場合、この追加の処理は不要です。この違いが、メモリ確保のパフォーマンスに影響を与える可能性があります。

unsafe.Pointer

unsafe.Pointerは、Goの型システムをバイパスして、任意の型のポインタを表現できる特殊な型です。これは、C言語のvoid*に似ています。ベンチマークコードでは、unsafe.Pointerを使用して、割り当てられたメモリのアドレスをuintptrに変換し、それをmallocSinkに代入しています。これは、コンパイラが割り当てられたメモリを最適化によって削除しないようにするための一般的なベンチマーク手法です。unsafeパッケージは、Goの型安全性を損なう可能性があるため、通常は注意して使用されますが、低レベルのランタイムベンチマークでは不可欠なツールです。

技術的詳細

このコミットで追加されたベンチマークは、Goのtestingパッケージのベンチマーク機能を利用しています。各ベンチマーク関数は、b *testing.B引数を受け取り、b.N回ループして操作を実行します。b.Nは、ベンチマーク実行時に自動的に調整され、統計的に有意な結果が得られるように十分な回数実行されます。

ベンチマークの設計

追加されたベンチマークは以下の4種類です。

  1. BenchmarkMalloc8: 8バイトのオブジェクト(int64)を割り当てます。new(int64)は、int64型のゼロ値へのポインタを返します。int64はプリミティブ型であり、それ自体はポインタを含まないため、このアロケーションは「型情報なし」のパスに近い挙動を示すと予想されます。
  2. BenchmarkMalloc16: 16バイトのオブジェクト([2]int64)を割り当てます。new([2]int64)は、2つのint64要素を持つ配列へのポインタを返します。これもプリミティブ型の配列であり、ポインタを含まないため、「型情報なし」のパスに近い挙動を示すと予想されます。
  3. BenchmarkMallocTypeInfo8: 8バイトのオブジェクトを割り当てますが、そのオブジェクトはポインタを含む構造体です。具体的には、struct { p [8 / unsafe.Sizeof(uintptr(0))]*int }という構造体を使用しています。unsafe.Sizeof(uintptr(0))はポインタのサイズ(通常8バイト)を返すため、8 / unsafe.Sizeof(uintptr(0))は1となります。つまり、この構造体は[1]*int、すなわち1つの*intポインタを含む構造体となります。これにより、ランタイムはアロケーション時に型情報を考慮し、GCがポインタをスキャンできるように準備する必要があります。
  4. BenchmarkMallocTypeInfo16: 16バイトのオブジェクトを割り当てますが、そのオブジェクトはポインタを含む構造体です。具体的には、struct { p [16 / unsafe.Sizeof(uintptr(0))]*int }という構造体を使用しています。これは、2つの*intポインタを含む構造体となります。同様に、ランタイムはアロケーション時に型情報を考慮する必要があります。

mallocSinkの役割

各ベンチマーク関数では、割り当てられたオブジェクトのポインタをuintptrに変換し、それをグローバル変数mallocSinkにXOR演算で代入しています。

var x uintptr
for i := 0; i < b.N; i++ {
    p := new(int64) // または他の型
    x ^= uintptr(unsafe.Pointer(p))
}
mallocSink = x

このmallocSinkの目的は、コンパイラによる最適化を防ぐことです。もし割り当てられたオブジェクトがどこにも使用されない場合、コンパイラは「デッドコードエリミネーション」によってそのアロケーション自体を削除してしまう可能性があります。mallocSinkに値を代入することで、コンパイラはpが使用されていると判断し、アロケーションが実際に行われることを保証します。XOR演算を使用しているのは、単に代入するだけでなく、複数のポインタの値を累積することで、より確実に最適化を防ぐためです。

測定結果の解釈

コミットメッセージに記載されている結果は、以下の傾向を示しています。

  • BenchmarkMalloc8 vs BenchmarkMalloc16: 8バイトのアロケーション(43.6 ns/op)は、16バイトのアロケーション(46.7 ns/op)よりもわずかに高速です。これは、コミットメッセージの「16バイトのアロケーションは、アロケートされたビットのアトミックな設定をバイパスできるが、8バイトのアロケーションはできない」という説明と矛盾するように見えるかもしれません。しかし、これは特定のランタイムの最適化パスや、アライメント、キャッシュの挙動など、様々な要因が絡み合っているため、単純な比較では説明できない複雑な挙動を示している可能性があります。あるいは、この時点での最適化がまだ完全ではなかった可能性も考えられます。
  • 型情報の有無による影響: 型情報を持つアロケーション(BenchmarkMallocTypeInfo8BenchmarkMallocTypeInfo16)は、型情報を持たないアロケーション(BenchmarkMalloc8BenchmarkMalloc16)と比較して、明らかに遅いです(約15〜20 ns/opの差)。これは、型情報を処理するための追加のオーバーヘッド(例えば、GCメタデータの更新や、GCスキャンパスの準備)が存在することを示しています。
  • BenchmarkMallocTypeInfo8 vs BenchmarkMallocTypeInfo16: 型情報を持つアロケーションの場合も、8バイト(61.3 ns/op)と16バイト(63.5 ns/op)で同様に16バイトの方がわずかに遅い傾向が見られます。

これらの結果は、Goランタイムのメモリ確保が、アロケーションサイズと型情報の有無によって異なるパフォーマンス特性を持つことを明確に示しています。これは、Goアプリケーションのパフォーマンスチューニングや、ランタイムのさらなる最適化のための重要な洞察を提供します。

コアとなるコードの変更箇所

src/pkg/runtime/malloc_test.goという新しいファイルが追加されています。

--- /dev/null
+++ b/src/pkg/runtime/malloc_test.go
@@ -0,0 +1,52 @@
+// Copyright 2013 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package runtime_test
+
+import (
+	"testing"
+	"unsafe"
+)
+
+var mallocSink uintptr
+
+func BenchmarkMalloc8(b *testing.B) {
+	var x uintptr
+	for i := 0; i < b.N; i++ {
+		p := new(int64)
+		x ^= uintptr(unsafe.Pointer(p))
+	}
+	mallocSink = x
+}
+
+func BenchmarkMalloc16(b *testing.B) {
+	var x uintptr
+	for i := 0; i < b.N; i++ {
+		p := new([2]int64)
+		x ^= uintptr(unsafe.Pointer(p))
+	}
+	mallocSink = x
+}
+
+func BenchmarkMallocTypeInfo8(b *testing.B) {
+	var x uintptr
+	for i := 0; i < b.N; i++ {
+		p := new(struct {
+			p [8 / unsafe.Sizeof(uintptr(0))]*int
+		})
+		x ^= uintptr(unsafe.Pointer(p))
+	}
+	mallocSink = x
+}
+
+func BenchmarkMallocTypeInfo16(b *testing.B) {
+	var x uintptr
+	for i := 0; i < b.N; i++ {
+		p := new(struct {
+			p [16 / unsafe.Sizeof(uintptr(0))]*int
+		})
+		x ^= uintptr(unsafe.Pointer(p))
+	}
+	mallocSink = x
+}

コアとなるコードの解説

追加されたmalloc_test.goファイルは、Goのランタイムパッケージのテストスイートの一部として機能します。package runtime_testという宣言は、このファイルがruntimeパッケージの外部テストであることを示しており、runtimeパッケージの内部実装に直接アクセスするのではなく、公開されたAPIを通じてテストを行うことを意味します。

import

  • "testing": Goの標準テストパッケージ。ベンチマーク機能を提供します。
  • "unsafe": unsafe.Pointerunsafe.Sizeofなどの機能を提供し、型安全性をバイパスしてメモリを直接操作することを可能にします。ランタイムの低レベルな挙動をテストする際に使用されます。

var mallocSink uintptr

このグローバル変数は、前述の通り、コンパイラによる最適化を防ぎ、割り当てられたメモリが実際に使用されることを保証するために使用されます。

ベンチマーク関数

各ベンチマーク関数は、Benchmarkプレフィックスを持ち、*testing.B型の引数を受け取ります。

  1. BenchmarkMalloc8(b *testing.B):

    • p := new(int64): 8バイトのint64型の新しいインスタンスをヒープに割り当てます。
    • x ^= uintptr(unsafe.Pointer(p)): 割り当てられたオブジェクトのポインタをuintptrに変換し、xにXOR演算で累積します。これにより、アロケーションが最適化で削除されるのを防ぎます。
  2. BenchmarkMalloc16(b *testing.B):

    • p := new([2]int64): 16バイトの[2]int64(2つのint64要素を持つ配列)型の新しいインスタンスをヒープに割り当てます。
  3. BenchmarkMallocTypeInfo8(b *testing.B):

    • p := new(struct { p [8 / unsafe.Sizeof(uintptr(0))]*int }):
      • unsafe.Sizeof(uintptr(0)): システムのポインタサイズ(通常8バイト)を返します。
      • 8 / unsafe.Sizeof(uintptr(0)): この式は1に評価されます。
      • したがって、new(struct { p [1]*int })となり、これは1つの*intポインタを含む8バイトの構造体をヒープに割り当てます。この構造体はポインタを含むため、ランタイムはGCのために型情報を処理する必要があります。
  4. BenchmarkMallocTypeInfo16(b *testing.B):

    • p := new(struct { p [16 / unsafe.Sizeof(uintptr(0))]*int }):
      • 16 / unsafe.Sizeof(uintptr(0)): この式は2に評価されます。
      • したがって、new(struct { p [2]*int })となり、これは2つの*intポインタを含む16バイトの構造体をヒープに割り当てます。同様に、ランタイムはGCのために型情報を処理します。

これらのベンチマークは、Goのメモリ確保の内部的な挙動を詳細に分析するための基盤を提供し、ランタイムのパフォーマンス特性を理解する上で不可欠なツールとなります。

関連リンク

  • Go言語の公式ドキュメント: https://golang.org/doc/
  • Goのメモリ管理に関するブログ記事やドキュメント(一般的な情報源)

参考にした情報源リンク

  • Go言語のソースコード(特にsrc/runtimeディレクトリ)
  • Goのtestingパッケージのドキュメント
  • Goのunsafeパッケージのドキュメント
  • Goのメモリ管理とガベージコレクションに関する技術記事や論文(一般的な知識として)
  • コミットメッセージ内のGo CLリンク: https://golang.org/cl/9090045