[インデックス 18847] ファイルの概要
このコミットは、GoランタイムがPlan 9オペレーティングシステム上でmemmove
およびmemclr
関数をどのように扱うかを変更するものです。具体的には、Plan 9環境では最適化されていないバージョンのこれらの関数を使用するように修正されています。
コミット
commit 8303a13bb8e46a11c64080f4da1b6c9ed11ac5a2
Author: Anthony Martin <ality@pbrane.org>
Date: Wed Mar 12 18:12:25 2014 -0700
runtime: use unoptimized memmove and memclr on Plan 9
On Plan 9, the kernel disallows the use of floating point
instructions while handling a note. Previously, we worked
around this by using a simple loop in place of memmove.
When I added that work-around, I verified that all paths
from the note handler didn't end up calling memmove. Now
that memclr is using SSE instructions, the same process
will have to be done again.
Instead of doing that, however, this CL just punts and
uses unoptimized functions everywhere on Plan 9.
LGTM=rsc
R=rsc, 0intro
CC=golang-codereviews
https://golang.org/cl/73830044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8303a13bb8e46a11c64080f4da1b6c9ed11ac5a2
元コミット内容
このコミットは、GoランタイムがPlan 9上でmemmove
とmemclr
の最適化されていないバージョンを使用するように変更します。
Plan 9では、カーネルが「ノート」(Unixにおけるシグナルに類似した非同期イベント通知メカニズム)を処理している間、浮動小数点命令の使用を許可しません。以前は、この制約を回避するためにmemmove
の代わりに単純なループを使用するワークアラウンドが適用されていました。
このワークアラウンドが導入された際、ノートハンドラからのすべてのパスがmemmove
を呼び出さないことが確認されていました。しかし、memclr
がSSE命令(浮動小数点演算に関連するSIMD命令セット)を使用するようになったため、同様の検証プロセスを再度行う必要が生じました。
このコミットでは、その複雑な検証を再度行う代わりに、Plan 9上では常に最適化されていないバージョンのmemmove
とmemclr
を使用するという、より単純なアプローチを採用しています。
変更の背景
この変更の背景には、Goランタイムの低レベルなメモリ操作関数(memmove
とmemclr
)が、特定のアーキテクチャやOS環境下で予期せぬ問題を引き起こす可能性があったという事情があります。
特にPlan 9オペレーティングシステムは、その設計思想から来る独自のカーネル動作を持っています。コミットメッセージにあるように、Plan 9カーネルは「ノート」(note)と呼ばれる非同期イベントを処理している間、浮動小数点命令の実行を厳しく制限します。これは、ノートハンドラが非常に限定されたコンテキストで動作し、予測可能な動作を保証するためのセキュリティ的・安定性的な制約であると考えられます。
Goランタイムのmemmove
やmemclr
といった関数は、パフォーマンス向上のためにアセンブリ言語で実装され、しばしばSSE(Streaming SIMD Extensions)のようなCPUの高度な命令セットを利用します。SSE命令は、浮動小数点演算だけでなく、整数演算やメモリ操作にも利用されますが、その実装によっては浮動小数点ユニット(FPU)を内部的に使用したり、FPUの状態に影響を与えたりする可能性があります。
以前はmemmove
がこの問題に直面し、Plan 9のノートハンドラから呼び出されないようにパスが検証されていました。しかし、memclr
もSSE命令を使用するように変更されたことで、同様の問題がmemclr
にも発生する可能性が出てきました。
この問題に対する解決策として、以下の2つの選択肢が考えられます。
memclr
についても、ノートハンドラから呼び出される可能性のあるすべてのパスを詳細に分析し、SSE命令を使用しない安全なパスを確保する。- Plan 9上では、
memmove
とmemclr
の両方について、SSE命令などの高度な最適化を使用しない、より基本的な(しかし安全な)実装を常に使用する。
コミットの作者は、前者のアプローチが複雑で手間がかかるため、後者のアプローチを選択しました。これにより、Plan 9環境におけるランタイムの安定性と予測可能性を確保しつつ、開発の複雑さを軽減しています。
前提知識の解説
このコミットを理解するためには、以下の前提知識が必要です。
-
Goランタイム (Go Runtime): Goプログラムは、Goランタイムと呼ばれる軽量な実行環境上で動作します。ランタイムは、ガベージコレクション、スケジューリング、メモリ管理、システムコールインターフェースなど、プログラムの実行に必要な低レベルな機能を提供します。これらの機能の一部は、パフォーマンスのためにアセンブリ言語で書かれており、OSやアーキテクチャに特化した実装を持つことがあります。
-
memmove
とmemclr
:memmove
: メモリブロックをコピーする関数です。C言語のmemmove
と同様に、コピー元とコピー先のメモリ領域が重なっていても正しく動作することが保証されています。Goランタイムでは、ガベージコレクタによるオブジェクトの移動や、スライス操作などで頻繁に使用されます。memclr
: メモリブロックをゼロで埋める(クリアする)関数です。Goランタイムでは、新しいメモリの割り当て時や、オブジェクトの解放時などに使用され、セキュリティやガベージコレクションの効率化に寄与します。 これらの関数は、非常に頻繁に呼び出されるため、可能な限り高速に動作するように最適化されています。
-
アセンブリ言語 (Assembly Language): Goランタイムのパフォーマンスクリティカルな部分は、Go言語ではなくアセンブリ言語で記述されています。これにより、特定のCPUアーキテクチャの命令セットを直接利用して、最大限の性能を引き出すことができます。このコミットで変更されているファイルも
.s
拡張子を持つアセンブリ言語のソースファイルです。 -
Plan 9オペレーティングシステム: Plan 9は、ベル研究所で開発された分散オペレーティングシステムです。Unixの後継として設計され、すべてのリソースをファイルとして表現する「Everything is a file」という哲学や、名前空間の概念が特徴です。Go言語は、Plan 9の開発者であるRob PikeやKen Thompsonが関わっているため、Plan 9へのサポートが比較的初期から存在します。 Plan 9カーネルの「ノート」メカニズムは、Unixのシグナルに似ていますが、より柔軟で強力な非同期イベント通知を提供します。このノートハンドラが実行されるコンテキストには、特定の制約(例: 浮動小数点命令の禁止)が課されることがあります。
-
浮動小数点命令とSSE命令:
- 浮動小数点命令: 実数を扱うためのCPU命令です。科学技術計算やグラフィックス処理などで広く使われます。
- SSE (Streaming SIMD Extensions): Intelが開発したSIMD(Single Instruction, Multiple Data)命令セットの一種です。単一の命令で複数のデータを同時に処理できるため、メディア処理、科学技術計算、そしてメモリ操作の高速化に利用されます。SSE命令は、浮動小数点演算だけでなく、整数演算にも使用されますが、FPU(Floating Point Unit)と密接に関連しているため、FPUの状態や動作に影響を与える可能性があります。
-
ビルドタグ (
+build
): Go言語のソースファイルでは、ファイルの先頭に+build
ディレクティブを記述することで、特定のビルド条件(OS、アーキテクチャ、カスタムタグなど)に基づいてファイルをコンパイルに含めるか除外するかを制御できます。例えば、+build plan9
はPlan 9環境でのみコンパイルされ、+build !plan9
はPlan 9以外の環境でコンパイルされます。
技術的詳細
このコミットの技術的詳細は、GoランタイムがPlan 9環境でmemmove
とmemclr
をどのように実装するかを変更することに集約されます。
-
既存の最適化された実装の除外:
src/pkg/runtime/memclr_386.s
、src/pkg/runtime/memclr_amd64.s
、src/pkg/runtime/memmove_386.s
、src/pkg/runtime/memmove_amd64.s
といった既存の汎用的なアセンブリ実装ファイルに、// +build !plan9
というビルドタグが追加されています。 これは、これらのファイルがPlan 9以外のシステム(Linux, Windows, macOSなど)でのみコンパイルされることを意味します。これにより、Plan 9環境ではこれらの最適化された(SSE命令などを使用する可能性のある)実装が使用されなくなります。 -
Plan 9専用の非最適化実装の導入: 新たに以下の4つのアセンブリファイルが追加されています。
-
src/pkg/runtime/memclr_plan9_386.s
-
src/pkg/runtime/memclr_plan9_amd64.s
-
src/pkg/runtime/memmove_plan9_386.s
-
src/pkg/runtime/memmove_plan9_amd64.s
これらのファイルは、それぞれ386(x86)およびAMD64(x86-64)アーキテクチャ向けのPlan 9専用の実装を提供します。これらの実装は、SSE命令のような高度な最適化を使用せず、より基本的なCPU命令(例:MOVSL
,STOSL
などの繰り返し文字列命令や、バイト/ワード/ダブルワード単位の単純なムーヴ命令)とループ構造を用いてmemmove
およびmemclr
の機能を実現しています。 -
memclr_plan9_386.s
/memclr_plan9_amd64.s
: これらのファイルは、指定されたメモリ領域をゼロで埋めるためのアセンブリコードを含んでいます。サイズに応じて、バイト、ワード、ダブルワード、クアッドワード(AMD64の場合)単位でゼロを書き込む処理を分岐させ、残りの部分をREP STOSL
(386)またはREP STOSQ
(AMD64)命令で効率的に処理します。XORL AX, AX
やXORQ AX, AX
でレジスタをゼロクリアし、そのゼロをメモリに書き込んでいます。 -
memmove_plan9_386.s
/memmove_plan9_amd64.s
: これらのファイルは、メモリブロックをコピーするためのアセンブリコードを含んでいます。- 小サイズ最適化:
memclr
と同様に、非常に小さいサイズ(1バイトから16バイト程度)のコピーは、MOVB
,MOVW
,MOVL
,MOVQ
などの命令を直接使用して効率的に処理されます。これは、REP
命令の起動コストを避けるためです。 - 順方向/逆方向コピー:
memmove
の重要な特性である、コピー元とコピー先が重なっている場合の正しい動作を保証するために、コピー方向を決定します。- コピー元アドレスがコピー先アドレスより小さい場合(
SI < DI
)、順方向(forward
ラベル)にコピーします。 - コピー元とコピー先が重なっていて、コピー元アドレスがコピー先アドレスより大きい場合(
SI > DI
かつオーバーラップがある場合)、逆方向(back
ラベル)にコピーします。これは、コピー元データが上書きされる前に読み取られるようにするためです。
- コピー元アドレスがコピー先アドレスより小さい場合(
REP MOVSL
/REP MOVSQ
: 順方向および逆方向のコピーでは、REP MOVSL
(386)またはREP MOVSQ
(AMD64)命令が使用されます。これらの命令は、CX
レジスタで指定された回数だけ、SI
からDI
へデータをコピーする効率的な命令です。逆方向コピーの場合は、STD
命令で方向フラグを設定し、DI
とSI
をコピーブロックの終端に設定してから逆方向にコピーします。コピー完了後にはCLD
命令で方向フラグをクリアします。
- 小サイズ最適化:
-
これらの変更により、Plan 9環境では、ノートハンドラが浮動小数点命令の使用を禁止している状況でも、Goランタイムのメモリ操作が安全かつ安定して実行されるようになります。パフォーマンスは汎用的な最適化されたバージョンに劣る可能性がありますが、Plan 9の特定の制約下での堅牢性が優先されています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、以下のファイル群の追加と既存ファイルへのビルドタグの追加です。
-
既存の
memclr
およびmemmove
アセンブリファイルへのビルドタグ追加:src/pkg/runtime/memclr_386.s
src/pkg/runtime/memclr_amd64.s
src/pkg/runtime/memmove_386.s
src/pkg/runtime/memmove_amd64.s
これらのファイルの先頭に、以下の行が追加されています。
--- a/src/pkg/runtime/memclr_386.s +++ b/src/pkg/runtime/memclr_386.s @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// +build !plan9 + #include "../../cmd/ld/textflag.h" // void runtime·memclr(void*, uintptr)
(他のファイルも同様)
-
Plan 9専用の
memclr
アセンブリファイルの新規追加:src/pkg/runtime/memclr_plan9_386.s
src/pkg/runtime/memclr_plan9_amd64.s
これらのファイルは、Plan 9環境でのみ使用されるruntime·memclr
関数の実装を含んでいます。
-
Plan 9専用の
memmove
アセンブリファイルの新規追加:src/pkg/runtime/memmove_plan9_386.s
src/pkg/runtime/memmove_plan9_amd64.s
これらのファイルは、Plan 9環境でのみ使用されるruntime·memmove
関数の実装を含んでいます。
これらの新規追加されたファイルは、それぞれ数十行から百数十行のアセンブリコードで構成されており、Plan 9の制約に合わせたメモリ操作ロジックが記述されています。
コアとなるコードの解説
新規追加されたPlan 9専用のアセンブリファイル(例: src/pkg/runtime/memclr_plan9_386.s
、src/pkg/runtime/memmove_plan9_386.s
など)は、以下の特徴を持つコードを含んでいます。
memclr_plan9_386.s
(386アーキテクチャ向け memclr
)
// void runtime·memclr(void*, uintptr)
TEXT runtime·memclr(SB), NOSPLIT, $0-8
MOVL ptr+0(FP), DI // 第一引数 (void* ptr) を DI レジスタにロード
MOVL n+4(FP), BX // 第二引数 (uintptr n) を BX レジスタにロード (サイズ)
XORL AX, AX // AX レジスタをゼロクリア (メモリを埋める値)
clr_tail:
TESTL BX, BX // サイズがゼロかチェック
JEQ clr_0 // ゼロなら終了
CMPL BX, $2 // サイズが2以下かチェック
JBE clr_1or2 // 2以下なら専用処理へ
CMPL BX, $4 // サイズが4以下かチェック
JBE clr_3or4 // 4以下なら専用処理へ
CMPL BX, $8 // サイズが8以下かチェック
JBE clr_5through8 // 8以下なら専用処理へ
CMPL BX, $16 // サイズが16以下かチェック
JBE clr_9through16 // 16以下なら専用処理へ
MOVL BX, CX // サイズを CX レジスタにコピー
SHRL $2, CX // CX を 4 で割る (DWORD 単位の繰り返し回数)
REP // REP プレフィックス: CX 回繰り返す
STOSL // DI が指すメモリに AX (ゼロ) を書き込み、DI を 4 進める
ANDL $3, BX // 残りのバイト数 (0-3) を BX に格納
JNE clr_tail // 残りがあれば再度処理
RET // 終了
// 小さいサイズの処理 (1-16バイト)
clr_1or2:
MOVB AX, (DI)
MOVB AX, -1(DI)(BX*1)
clr_0:
RET
clr_3or4:
MOVW AX, (DI)
MOVW AX, -2(DI)(BX*1)
RET
clr_5through8:
MOVL AX, (DI)
MOVL AX, -4(DI)(BX*1)
RET
clr_9through16:
MOVL AX, (DI)
MOVL AX, 4(DI)
MOVL AX, -8(DI)(BX*1)
MOVL AX, -4(DI)(BX*1)
RET
このコードは、memclr
関数を実装しています。まず、引数からポインタDI
とサイズBX
を取得し、AX
をゼロに設定します。サイズが小さい場合は、直接バイト、ワード、ダブルワード単位でゼロを書き込みます。サイズが大きい場合は、REP STOSL
命令(32ビット単位でゼロを書き込む)を使用して効率的にメモリをクリアし、残りのバイトを個別に処理します。この実装はSSE命令を使用せず、基本的なx86命令のみで構成されています。
memmove_plan9_386.s
(386アーキテクチャ向け memmove
)
// void runtime·memmove(void*, void*, uintptr)
TEXT runtime·memmove(SB), NOSPLIT, $0-12
MOVL to+0(FP), DI // 第一引数 (void* to) を DI レジスタにロード
MOVL fr+4(FP), SI // 第二引数 (void* fr) を SI レジスタにロード
MOVL n+8(FP), BX // 第三引数 (uintptr n) を BX レジスタにロード (サイズ)
// REP instructions have a high startup cost, so we handle small sizes
// with some straightline code. The REP MOVSL instruction is really fast
// for large sizes. The cutover is approximately 1K.
tail:
TESTL BX, BX
JEQ move_0
CMPL BX, $2
JBE move_1or2
CMPL BX, $4
JBE move_3or4
CMPL BX, $8
JBE move_5through8
CMPL BX, $16
JBE move_9through16
/*
* check and set for backwards
*/
CMPL SI, DI // コピー元とコピー先のアドレスを比較
JLS back // SI < DI なら back へ (順方向コピーの可能性)
/*
* forward copy loop
*/
forward:
MOVL BX, CX
SHRL $2, CX // DWORD 単位の繰り返し回数
ANDL $3, BX // 残りのバイト数
REP; MOVSL // SI から DI へ DWORD 単位でコピー
JMP tail // 残りがあれば再度処理
/*
* check overlap
*/
back:
MOVL SI, CX
ADDL BX, CX // CX = SI + BX (コピー元の終端アドレス)
CMPL CX, DI // コピー元の終端がコピー先の開始より小さいかチェック
JLS forward // 小さければオーバーラップなし、順方向コピーへ
/*
* whole thing backwards has
* adjusted addresses
*/
ADDL BX, DI // DI = DI + BX (コピー先の終端)
ADDL BX, SI // SI = SI + BX (コピー元の終端)
STD // 方向フラグを設定 (逆方向コピー)
/*
* copy
*/
MOVL BX, CX
SHRL $2, CX // DWORD 単位の繰り返し回数
ANDL $3, BX // 残りのバイト数
SUBL $4, DI // DI を 4 減らす (終端から開始)
SUBL $4, SI // SI を 4 減らす (終端から開始)
REP; MOVSL // SI から DI へ DWORD 単位で逆方向コピー
CLD // 方向フラグをクリア (順方向に戻す)
ADDL $4, DI
ADDL $4, SI
SUBL BX, DI
SUBL BX, SI
JMP tail
// 小さいサイズの処理 (1-16バイト)
move_1or2:
MOVB (SI), AX
MOVB -1(SI)(BX*1), CX
MOVB AX, (DI)
MOVB CX, -1(DI)(BX*1)
move_0:
RET
move_3or4:
MOVW (SI), AX
MOVW -2(SI)(BX*1), CX
MOVW AX, (DI)
MOVW CX, -2(DI)(BX*1)
RET
move_5through8:
MOVL (SI), AX
MOVL -4(SI)(BX*1), CX
MOVL AX, (DI)
MOVL CX, -4(DI)(BX*1)
RET
move_9through16:
MOVL (SI), AX
MOVL 4(SI), CX
MOVL -8(SI)(BX*1), DX
MOVL -4(SI)(BX*1), BP
MOVL AX, (DI)
MOVL CX, 4(DI)
MOVL DX, -8(DI)(BX*1)
MOVL BP, -4(DI)(BX*1)
RET
このコードは、memmove
関数を実装しています。引数からコピー先DI
、コピー元SI
、サイズBX
を取得します。サイズが小さい場合は、直接バイト、ワード、ダブルワード単位でコピーします。
重要なのは、コピー元とコピー先のアドレスを比較し、オーバーラップがある場合にコピー方向を決定するロジックです。
- コピー元がコピー先より前にある場合(
SI < DI
)、順方向にコピーするとコピー元が上書きされる可能性があるため、逆方向コピー(back
ラベル)を検討します。ただし、オーバーラップがない場合は順方向コピー(forward
ラベル)を行います。 - コピー元がコピー先より後にある場合(
SI > DI
)、順方向コピー(forward
ラベル)で問題ありません。 順方向コピーではREP MOVSL
を、逆方向コピーではSTD
命令で方向フラグを設定し、ポインタを終端に移動させてからREP MOVSL
で逆方向にコピーします。コピー完了後にはCLD
命令で方向フラグをクリアします。この実装もSSE命令を使用せず、基本的なx86命令のみで構成されています。
amd64
版のファイルも同様のロジックですが、レジスタが64ビットになり、MOVSQ
(クアッドワード単位のコピー)が使用される点が異なります。
これらの変更により、Plan 9環境におけるGoランタイムのメモリ操作は、カーネルの制約に準拠し、安定した動作が保証されます。
関連リンク
- Go CL 73830044: https://golang.org/cl/73830044
- GitHub Commit: https://github.com/golang/go/commit/8303a13bb8e46a11c64080f4da1b6c9ed11ac5a2
参考にした情報源リンク
- Go言語のソースコード (特に
src/pkg/runtime
ディレクトリ内のアセンブリファイル) - Plan 9オペレーティングシステムのドキュメント (ノートハンドラとカーネルの制約に関する情報)
- x86/x86-64アセンブリ言語の命令セットリファレンス (MOVS, STOS, REP, STD, CLDなど)
- SSE命令セットの概要 (浮動小数点演算との関連性)
- Inferno OSの
libkern/memmove-386.s
(Goのmemmove_plan9
実装の派生元) - Go言語のビルドタグに関するドキュメント