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

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

このコミットは、Goランタイムにおける文字列操作(string ops)とrune操作の実装をC言語からGo言語へ移行する大規模なリファクタリングを主目的としています。また、Goコンパイラディレクティブであるgo:nosplitアノテーションの導入も含まれています。これにより、GoランタイムのGo言語化をさらに進め、パフォーマンスの向上とコードベースの保守性の改善を目指しています。

コミット

commit 61dca94e107170d2ff3beb13bb9fa5ce49d8d6fd
Author: Keith Randall <khr@golang.org>
Date:   Mon Jun 16 23:03:03 2014 -0700

    runtime: implement string ops in Go
    
    Also implement go:nosplit annotation.  Not really needed
    for now, but we'll definitely need it for other conversions.
    
    benchmark                 old ns/op     new ns/op     delta
    BenchmarkRuneIterate      534           474           -11.24%
    BenchmarkRuneIterate2     535           470           -12.15%
    
    LGTM=bradfitz
    R=golang-codereviews, dave, bradfitz, minux
    CC=golang-codereviews
    https://golang.org/cl/93380044

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

https://github.com/golang/go/commit/61dca94e107170d2ff3beb13bb9fa5ce49d8d6fd

元コミット内容

このコミットの元の内容は、Goランタイムにおける文字列操作をGo言語で実装すること、そしてgo:nosplitアノテーションを導入することです。コミットメッセージには、ベンチマーク結果としてBenchmarkRuneIterateBenchmarkRuneIterate2がそれぞれ約11%と12%のパフォーマンス改善を示していることが記載されています。これは、C言語からGo言語への移行がパフォーマンス向上に寄与したことを示唆しています。

変更の背景

この変更の背景には、GoランタイムのC言語部分をGo言語で書き直すという、Go 1.3およびGo 1.4リリースにおける大規模な取り組みがあります。この移行の主な動機は以下の通りです。

  1. スタック管理の改善: GoランタイムにC言語のコードが存在すると、ゴルーチンが古い「スプリットスタック」方式を使用せざるを得ない場合があり、新しい「コピーイングスタック」方式の効率を妨げていました。これらの部分をGo言語で書き直すことで、コピーイングスタックをより効果的に利用できるようになります。
  2. コンパイラの簡素化: ランタイム内のC言語コードの量を減らすことは、Goコンパイラ自体をC言語からGo言語に移行する継続的な取り組みを簡素化することを目的としていました。
  3. Go言語の利点の活用: ランタイムをGo言語で記述することで、デバッグの容易さ、モジュール性の向上、自動リライト、単体テスト、プロファイリング、並列化のサポートなど、Go言語自体の利点を活用できます。

このコミットは、特に文字列とruneの操作に焦点を当て、これらの低レベルな処理をGo言語で再実装することで、上記の目標達成に貢献しています。go:nosplitアノテーションの導入は、将来的に他のランタイム機能のGo言語への移行を円滑に進めるための基盤作りでもあります。

前提知識の解説

Go言語における文字列とRune

  • 文字列 (string): Go言語の文字列は、不変(immutable)なバイトのスライスであり、通常はUTF-8エンコードされています。これは、文字列の内容が一度作成されると変更できないことを意味します。
  • Rune: Go言語におけるruneは、int32のエイリアスであり、Unicodeコードポイントを表します。UTF-8エンコードされた文字列では、1つのUnicode文字が1バイトから4バイトを占めることがあります。runeを使用することで、バイト単位ではなく、個々のUnicode文字として文字列を扱うことができます。for...rangeループで文字列をイテレートすると、各要素はruneとして取得されます。

GoランタイムとC言語の相互運用

Goランタイムは、Goプログラムの実行を管理する低レベルな部分であり、ガベージコレクション、スケジューリング、メモリ管理などを担当します。Go言語の初期のバージョンでは、パフォーマンスが要求される一部のランタイム機能はC言語で実装されていました。これは、C言語が低レベルなメモリ操作やシステムコールに直接アクセスできるためです。しかし、C言語とGo言語の間の境界を越える呼び出し(Cgo)にはオーバーヘッドがあり、また、C言語のコードはGoのガベージコレクタやスタック管理と統合するのが難しいという課題がありました。

go:nosplitアノテーション

//go:nosplitはGoコンパイラに対するディレクティブ(指示)であり、その関数がスタック分割を行わないことを保証します。Go言語では、ゴルーチンのスタックは必要に応じて動的に拡張されます(スタック分割)。これは、関数呼び出し時にスタックに十分なスペースがあるかチェックし、足りなければより大きなスタックを割り当てて既存のスタックの内容をコピーすることで実現されます。

go:nosplitが指定された関数は、このスタックオーバーフローチェックを行いません。これは、特にランタイムのような低レベルのコードで、スタックチェックのオーバーヘッドを避けたい場合や、ゴルーチンのプリエンプション(横取り)を一時的に無効にしたい場合に有用です。しかし、このアノテーションを使用する関数は、スタックが不足した場合にプログラムがクラッシュする可能性があるため、スタック使用量を厳密に管理する必要があります。

技術的詳細

このコミットの技術的な核心は、GoランタイムにおけるUTF-8エンコードされた文字列とruneの操作を、C言語の実装からGo言語の実装へと移行した点にあります。

具体的には、以下のファイルが変更されています。

  • src/pkg/runtime/rune.cの削除とsrc/pkg/runtime/rune.goの新規追加:

    • rune.cは、UTF-8バイト列からruneをデコードするruntime·charntoruneや、runeからUTF-8バイト列をエンコードするruntime·runetocharといった、低レベルなUTF-8処理をC言語で実装していました。
    • rune.goでは、これらの機能がGo言語で再実装されています。これにより、GoランタイムのUTF-8処理がGo言語のコードベースに統合され、Goの型システムやメモリ管理の恩恵を受けることができます。
  • src/pkg/runtime/string.gocの削除とsrc/pkg/runtime/string.goの新規追加:

    • string.gocは、文字列の連結、バイトスライスと文字列の変換、文字列の比較など、Go言語の文字列操作の基盤となる多くの関数をC言語で提供していました。
    • string.goでは、これらの機能がGo言語で再実装されています。特に、文字列の連結を効率的に行うconcatstrings関数や、バイトスライス・runeスライスと文字列間の変換を行うslicebytetostringstringtoslicebytestringtosliceruneslicerunetostringといった内部関数がGo言語で記述されています。これらの関数は、Goコンパイラによって自動的に呼び出され、Go言語の文字列操作のパフォーマンスを最適化します。
  • src/pkg/runtime/string.cの新規追加:

    • 興味深いことに、string.gocが削除された一方で、string.cという新しいC言語ファイルが追加されています。このファイルには、runtime·findnull(Cスタイルの文字列のヌル終端を見つける)、runtime·gostring(C文字列をGo文字列に変換する)、runtime·catstring(文字列連結)、runtime·strcmp(文字列比較)、runtime·strstr(部分文字列検索)といった、一部の低レベルな文字列操作関数がC言語で含まれています。これは、GoランタイムがC言語のコードと相互運用する必要がある特定のシナリオ(例えば、Cgoを介したCライブラリとの連携)のために、これらのC言語関数がまだ必要であることを示唆しています。ただし、Go言語で再実装された機能が優先的に使用されるようになります。
  • src/cmd/gcディレクトリ内の変更:

    • src/cmd/gc/go.hsrc/cmd/gc/go.ysrc/cmd/gc/lex.csrc/cmd/gc/pgen.csrc/cmd/gc/y.tab.cなどのコンパイラ関連ファイルが変更されています。これらの変更は、go:nosplitアノテーションをコンパイラが認識し、処理できるようにするためのものです。具体的には、関数の定義にnosplitフラグを追加し、go:nosplitディレクティブが検出された場合にこのフラグを設定するロジックが追加されています。
  • アセンブリファイルの変更:

    • src/pkg/runtime/asm_386.ssrc/pkg/runtime/asm_amd64.ssrc/pkg/runtime/asm_amd64p32.ssrc/pkg/runtime/asm_arm.sといったアセンブリファイルには、runtime·gogetcallerpcという新しい関数が追加されています。この関数は、Go言語のコードから呼び出し元のプログラムカウンタ(PC)を取得するために使用され、特にレース検出器のような低レベルなランタイム機能で利用されます。これらの関数もNOSPLITとしてマークされており、スタック分割を避けるように設計されています。
  • レース検出器関連の変更:

    • src/pkg/runtime/race.gosrc/pkg/runtime/race0.goが変更されています。race0.go!raceビルドタグが指定された場合にダミーのレース検出APIを提供する新しいファイルです。race.goでは、raceenabled定数が導入され、レース検出が有効かどうかをGoコードから判断できるようになっています。また、racereadrangepcのような関数がGo言語のコードから呼び出されるようになり、レース検出器のGo言語化も進められています。
  • src/pkg/runtime/stubs.gosrc/pkg/runtime/stubs.gocの新規追加:

    • これらのファイルは、Go言語から呼び出されるがC言語またはアセンブリで実装されているランタイムサービスのためのスタブを提供します。rawstringrawbyteslicerawrunesliceといった関数が定義されており、これらはGo言語の文字列やスライスを効率的に操作するための低レベルなメモリ割り当てやコピーを扱います。これらの関数もNOSPLITとしてマークされ、スタック管理の制約が課せられています。

全体として、このコミットはGoランタイムのGo言語化という大きな目標に向けた重要な一歩であり、特に文字列とruneの処理において、C言語からGo言語への移行を推進しています。これにより、Go言語のコードベースの一貫性が高まり、将来的な最適化や機能追加が容易になります。

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

このコミットのコアとなる変更は、主に以下のファイルの追加と削除、および既存ファイルへのgo:nosplit関連の変更です。

  • 削除されたファイル:

    • src/pkg/runtime/rune.c: C言語で実装されていたrune操作のコード。
    • src/pkg/runtime/string.goc: C言語で実装されていた文字列操作のコード。
  • 新規追加されたファイル:

    • src/pkg/runtime/rune.go: Go言語で再実装されたrune操作のコード。
    • src/pkg/runtime/string.go: Go言語で再実装された文字列操作のコード。
    • src/pkg/runtime/string.c: 一部の低レベルな文字列操作のために残されたC言語のコード。
    • src/pkg/runtime/stubs.go: Go言語からC/アセンブリ実装のランタイムサービスを呼び出すためのスタブ。
    • src/pkg/runtime/stubs.goc: stubs.goで宣言された関数の一部をC言語で実装。
    • src/pkg/runtime/race0.go: レース検出器が無効な場合のダミー実装。
  • 変更された主要なファイル:

    • src/cmd/gc/go.h, src/cmd/gc/go.y, src/cmd/gc/lex.c, src/cmd/gc/pgen.c, src/cmd/gc/y.tab.c: go:nosplitアノテーションのサポートを追加するためのコンパイラ関連の変更。
    • src/pkg/runtime/asm_*.s (386, amd64, amd64p32, arm): runtime·gogetcallerpc関数の追加。
    • src/pkg/runtime/string_test.go: 新しいGo言語実装のベンチマークテストの追加。

コアとなるコードの解説

src/pkg/runtime/rune.go (新規)

このファイルは、C言語で書かれていたUTF-8のデコード/エンコードロジックをGo言語で再実装しています。

// Copyright 2002 by Lucent Technologies.
// Portions Copyright 2009 The Go Authors. All rights reserved.
// ... (license header)

package runtime

const (
	// ... (UTF-8関連の定数定義)
)

// charntorune converts a UTF-8 encoded byte slice to a rune and its width.
// This is the Go equivalent of the C function runtime·charntorune.
func charntorune(s string) (rune, int) {
	// ... (UTF-8デコードロジック)
}

// runetochar converts a rune to its UTF-8 encoded byte representation.
// This is the Go equivalent of the C function runtime·runetochar.
func runetochar(str []byte, r rune) int {
	// ... (UTF-8エンコードロジック)
}

charntoruneは、UTF-8バイト列の先頭から1つのruneをデコードし、そのruneとバイト幅を返します。runetocharは、runeをUTF-8バイト列にエンコードし、書き込まれたバイト数を返します。これらの関数は、Go言語の文字列とruneの内部表現を扱う上で基盤となります。

src/pkg/runtime/string.go (新規)

このファイルは、Go言語の文字列操作の多くの低レベルな部分をGo言語で実装しています。

// Copyright 2014 The Go Authors. All rights reserved.
// ... (license header)

package runtime

import (
	"unsafe"
)

// concatstrings concatenates a slice of strings.
// This is used by the compiler for string concatenation operations.
func concatstrings(a []string) string {
	// ... (文字列連結ロジック)
}

//go:nosplit
func concatstring2(a [2]string) string {
	return concatstrings(a[:])
}
// ... (concatstring3, concatstring4, concatstring5 も同様に定義)

// slicebytetostring converts a byte slice to a string.
// This is an internal runtime function.
func slicebytetostring(b []byte) string {
	// ... (バイトスライスから文字列への変換ロジック)
}

// slicebytetostringtmp is an optimized version of slicebytetostring
// that avoids copying if the string form will be discarded quickly.
// This is an internal compiler optimization.
func slicebytetostringtmp(b []byte) string {
	// ... (最適化されたバイトスライスから文字列への変換ロジック)
}

// stringtoslicebyte converts a string to a byte slice.
// This is an internal runtime function.
func stringtoslicebyte(s string) []byte {
	// ... (文字列からバイトスライスへの変換ロジック)
}

// stringtoslicerune converts a string to a rune slice.
// This is an internal runtime function.
func stringtoslicerune(s string) []rune {
	// ... (文字列からruneスライスへの変換ロジック)
}

// slicerunetostring converts a rune slice to a string.
// This is an internal runtime function.
func slicerunetostring(a []rune) string {
	// ... (runeスライスから文字列への変換ロジック)
}

// cstringToGo converts a C-style null-terminated string (uintptr) to a Go string.
func cstringToGo(str uintptr) (s string) {
	// ... (C文字列からGo文字列への変換ロジック)
}

// intstring converts an int64 to a string (representing a single rune).
func intstring(v int64) string {
	// ... (int64から文字列への変換ロジック)
}

// stringiter returns the index of the next rune after the rune that starts at s[k].
func stringiter(s string, k int) int {
	// ... (文字列イテレーションロジック)
}

// stringiter2 returns the rune that starts at s[k] and the index where the next rune starts.
func stringiter2(s string, k int) (int, rune) {
	// ... (文字列イテレーションロジック)
}

このファイルは、Go言語の文字列操作の効率的な実装を提供します。特に、concatstringsとその特殊化されたバージョン(concatstring2など)は、Goコンパイラが+演算子による文字列連結を最適化するために使用します。slicebytetostringなどの変換関数は、Goの不変な文字列と可変なスライス間の変換を効率的に行います。stringiterstringiter2は、文字列をrune単位で効率的にイテレートするための内部ヘルパー関数です。

src/cmd/gc/lex.c (変更)

このファイルは、Goコンパイラの字句解析器の一部であり、go:nosplitアノテーションを認識するように変更されています。

// ... (既存のコード)

if(strcmp(lexbuf, "go:nosplit") == 0) {
	nosplit = 1;
	goto out;
}

// ... (既存のコード)

この変更により、コンパイラはソースコード内の//go:nosplitコメントを検出し、それに対応するフラグを設定できるようになります。

src/pkg/runtime/asm_amd64.s (変更例)

アセンブリファイルには、runtime·gogetcallerpcという新しい関数が追加されています。

TEXT runtime·gogetcallerpc(SB),NOSPLIT,$0-8
	MOVQ	x+0(FP),AX		// addr of first arg
	MOVQ	-8(AX),AX		// get calling pc
	MOVQ	AX,r+4(FP)
	RET

このアセンブリ関数は、Go言語のコードから呼び出し元のプログラムカウンタ(PC)を取得するために使用されます。NOSPLITディレクティブが指定されており、この関数がスタック分割を行わないことを保証しています。これは、レース検出器のような低レベルなランタイム機能で、正確なPC情報を取得するために重要です。

関連リンク

参考にした情報源リンク