[インデックス 12953] ファイルの概要
このコミットは、Go言語の標準ライブラリbytes
パッケージ内のIndexByte
関数に対し、ARMアーキテクチャ向けのアセンブリ言語による最適化を導入するものです。これにより、特定のバイトをバイトスライス内で検索する際のパフォーマンスが大幅に向上しました。ベンチマーク結果は、最大で約82%の実行時間短縮(ns/op)と、約5.6倍のスループット向上(MB/s)を示しています。
コミット
commit 0681b13437e36de582521d5b9f1b4664400312a9
Author: Dave Cheney <dave@cheney.net>
Date: Wed Apr 25 13:18:31 2012 +1000
bytes: add assembly version of IndexByte for ARM
benchmark old ns/op new ns/op delta
BenchmarkIndexByte32 459 126 -72.55%
BenchmarkIndexByte4K 52404 10939 -79.13%
BenchmarkIndexByte4M 54470800 11177370 -79.48%
BenchmarkIndexByte64M 1010803000 178860500 -82.31%
benchmark old MB/s new MB/s speedup
BenchmarkIndexByte32 69.58 252.63 3.63x
BenchmarkIndexByte4K 78.16 374.42 4.79x
BenchmarkIndexByte4M 77.00 375.25 4.87x
BenchmarkIndexByte64M 66.39 375.20 5.65x
R=rsc, minux.ma
CC=golang-dev
https://golang.org/cl/6106044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0681b13437e36de582521d5b9f1b4664400312a9
元コミット内容
bytes: add assembly version of IndexByte for ARM
benchmark old ns/op new ns/op delta
BenchmarkIndexByte32 459 126 -72.55%
BenchmarkIndexByte4K 52404 10939 -79.13%
BenchmarkIndexByte4M 54470800 11177370 -79.48%
BenchmarkIndexByte64M 1010803000 178860500 -82.31%
benchmark old MB/s new MB/s speedup
BenchmarkIndexByte32 69.58 252.63 3.63x
BenchmarkIndexByte4K 78.16 374.42 4.79x
BenchmarkIndexByte4M 77.00 375.25 4.87x
BenchmarkIndexByte64M 66.39 375.20 5.65x
R=rsc, minux.ma
CC=golang-dev
https://golang.org/cl/6106044
変更の背景
この変更の主な背景は、Go言語のbytes.IndexByte
関数のARMアーキテクチャ上でのパフォーマンスを劇的に改善することにあります。IndexByte
は、バイトスライス([]byte
)の中から特定のバイトが最初に現れるインデックスを検索する基本的な操作であり、文字列処理やバイナリデータの解析など、多くの場面で頻繁に利用されます。
Go言語は通常、高レベルな抽象化を提供し、開発者がアセンブリ言語を直接書く必要がないように設計されています。しかし、一部のクリティカルな関数、特にCPUのレジスタやメモリ操作に直接アクセスすることで大幅なパフォーマンス向上が見込めるような低レベルな処理においては、アセンブリ言語による最適化が採用されることがあります。
ARMプロセッサは、モバイルデバイス、組み込みシステム、そして近年ではサーバー分野でも広く利用されており、Go言語がこれらのプラットフォームで効率的に動作することは非常に重要です。既存のIndexByte
のポータブル(Goで書かれた)実装では、ARMアーキテクチャの特性を十分に活かしきれていなかったため、アセンブリ言語を用いてより効率的な検索アルゴリズムを実装することで、実行速度とスループットのボトルネックを解消することが目指されました。
コミットメッセージに示されているベンチマーク結果は、この最適化が非常に効果的であったことを明確に示しています。特に大きなデータサイズ(4MBや64MB)での改善率が顕著であり、これは大量のデータを扱うアプリケーションにおいて、この変更が大きな恩恵をもたらすことを意味します。
前提知識の解説
Go言語におけるアセンブリ
Go言語は、通常はGoで記述されたコードをコンパイルしますが、パフォーマンスが極めて重要な一部の関数や、特定のハードウェア機能にアクセスする必要がある場合には、アセンブリ言語で記述されたコード(通常は.s
拡張子のファイル)をGoのパッケージに含めることができます。Goのアセンブリは、Plan 9アセンブラの文法に基づいており、一般的なx86やARMのアセンブリとは異なる独自の記法を持ちます。Goのアセンブリは、Goのランタイムや標準ライブラリの低レベルな部分で利用され、例えばメモリ操作、コンテキストスイッチ、特定のCPU命令の利用などに用いられます。
ARMアーキテクチャ
ARM(Advanced RISC Machine)は、RISC(Reduced Instruction Set Computer)アーキテクチャに基づくプロセッサファミリーです。低消費電力と高い性能効率が特徴で、スマートフォン、タブレット、組み込みシステム、IoTデバイスなど、幅広い分野でデファクトスタンダードとなっています。近年では、Apple MシリーズチップやAWS Gravitonプロセッサなど、高性能コンピューティング分野でも採用が拡大しています。ARMプロセッサは、レジスタベースのアーキテクチャであり、命令セットが比較的シンプルで、パイプライン処理に適しています。
bytes.IndexByte
関数
Go言語の標準ライブラリbytes
パッケージに含まれるIndexByte
関数は、func IndexByte(s []byte, c byte) int
というシグネチャを持ちます。この関数は、バイトスライスs
の中から、指定されたバイトc
が最初に現れるインデックスを返します。もしc
が見つからない場合は-1
を返します。これは、C言語の標準ライブラリ関数memchr
に相当する機能を提供します。
memchr
memchr
は、C言語の標準ライブラリ関数で、void *memchr(const void *s, int c, size_t n);
というシグネチャを持ちます。これは、メモリブロックs
の最初のn
バイトの中から、指定されたバイトc
が最初に現れる位置を検索します。見つかった場合はその位置へのポインタを返し、見つからない場合はNULLを返します。bytes.IndexByte
は、Go言語でこのmemchr
と同様の機能を提供するものです。
ベンチマーク指標 (ns/op
, MB/s
)
ns/op
(nanoseconds per operation): 1回の操作にかかる平均時間(ナノ秒)。この値が小さいほど、処理が速いことを意味します。コミットのベンチマークでは、この値が大幅に減少しており、個々の検索操作が高速化されたことを示しています。MB/s
(megabytes per second): 1秒あたりに処理できるデータ量(メガバイト)。この値が大きいほど、スループットが高いことを意味します。コミットのベンチマークでは、この値が大幅に増加しており、単位時間あたりにより多くのデータを検索できるようになったことを示しています。delta
/speedup
: 変更前後の性能差を示す指標です。delta
はns/op
の改善率(減少率)を示し、speedup
はMB/s
の向上倍率を示します。
技術的詳細
このコミットでは、bytes.IndexByte
関数のARMアーキテクチャ向け実装を、Goで書かれたポータブルバージョンからアセンブリ言語バージョンに置き換えることで最適化を行っています。
Goのアセンブリコードは、Goの関数呼び出し規約に従って引数を受け取り、結果を返します。
TEXT ·IndexByte(SB),7,$0
:IndexByte
関数のアセンブリ実装の開始を宣言します。SB
はStatic Baseレジスタで、グローバルシンボルへのオフセット計算に使われます。7
はフラグ、$0
はスタックフレームサイズ(この関数ではローカル変数を必要としないため0)。MOVW base+0(FP), R0
: 関数引数s
(バイトスライスの先頭アドレス)をR0
レジスタにロードします。FP
はFrame Pointerで、関数引数やローカル変数へのアクセスに使われます。base+0(FP)
はs
の先頭アドレスを指します。MOVW len+4(FP), R1
: 関数引数s
の長さ(len
)をR1
レジスタにロードします。Goのバイトスライスは、ポインタ、長さ、容量の3つの要素で構成されます。len+4(FP)
はs
の長さの部分を指します。MOVBU c+12(FP), R2
: 検索対象のバイトc
をR2
レジスタにロードします。c+12(FP)
はc
の値を指します。MOVBU
はバイトをロードし、ゼロ拡張してワード(32ビット)に格納する命令です。MOVW R0, R4
: スライスの開始アドレス(R0
に格納されている)をR4
にコピーします。これは、後でインデックスを計算するために元の開始アドレスを保持しておくためです。ADD R0, R1
:R0
(現在のポインタ)にR1
(長さ)を加算し、結果をR0
に格納します。これによりR0
はスライスの終端アドレス(終端の1バイト先)を指すようになります。これはループの終了条件に使われます。
検索ループ (_loop
):
_loop:
: ループの開始ラベル。CMP R0, R1
: 現在のポインタR0
とスライスの終端アドレスR1
を比較します。B.EQ _notfound
: もしR0
とR1
が等しい場合(つまり、スライスの終端に達した場合)、_notfound
ラベルに分岐します。これは検索対象のバイトが見つからなかったことを意味します。MOVBU.P 1(R0), R3
:R0
が指すメモリ位置から1バイトを読み込み、R3
レジスタに格納します。.P
サフィックスは、読み込み後にR0
を1バイト進める(ポストインクリメント)ことを意味します。これにより、ポインタが自動的に次のバイトに移動し、ループ内で明示的なポインタ加算が不要になります。CMP R2, R3
: 読み込んだバイトR3
と検索対象のバイトR2
を比較します。B.NE _loop
: もしR2
とR3
が等しくない場合(つまり、バイトが一致しない場合)、_loop
ラベルに分岐し、次のバイトの検索を続行します。
バイトが見つかった場合:
SUB $1, R0
: ループを抜けた時点のR0
は、見つかったバイトの1バイト先を指しています。そのため、R0
から1を減算して、見つかったバイトの正確なアドレスに戻します。SUB R4, R0
:R0
(見つかったバイトのアドレス)からR4
(スライスの開始アドレス)を減算することで、見つかったバイトのインデックス(オフセット)を計算します。MOVW R0, index+16(FP)
: 計算されたインデックスを関数の戻り値index
に格納します。index+16(FP)
は戻り値の格納場所を指します。RET
: 関数から戻ります。
バイトが見つからなかった場合 (_notfound
):
_notfound:
: バイトが見つからなかった場合のラベル。MOVW $-1, R0
: 戻り値として-1
をR0
にロードします。MOVW R0, index+16(FP)
:R0
の値を戻り値index
に格納します。RET
: 関数から戻ります。
このアセンブリ実装は、Goで書かれたポータブルバージョンと比較して、以下のような点でパフォーマンスを向上させています。
- 直接的なレジスタ操作: Goのコンパイラが生成するコードよりも、より直接的にARMプロセッサのレジスタを操作し、メモリへのアクセス回数を減らしています。
- 効率的なループ:
MOVBU.P
のようなポストインクリメント命令を使用することで、ループ内の命令数を削減し、パイプライン処理の効率を高めています。 - 条件分岐の最適化: 比較と分岐命令を組み合わせることで、検索ロジックをCPUが効率的に実行できるようにしています。
これらの最適化により、特に大きなバイトスライスを扱う際に、CPUキャッシュの利用効率が向上し、命令の実行サイクルが削減され、結果として大幅なパフォーマンス向上が実現されました。
コアとなるコードの変更箇所
--- a/src/pkg/bytes/asm_arm.s
+++ b/src/pkg/bytes/asm_arm.s
@@ -2,10 +2,29 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// no memchr implementation on arm yet
TEXT ·IndexByte(SB),7,$0
-\tB\t·indexBytePortable(SB)\n+\tMOVW\tbase+0(FP), R0
+\tMOVW\tlen+4(FP), R1
+\tMOVBU\tc+12(FP), R2\t// byte to find
+\tMOVW\tR0, R4\t\t// store base for later
+\tADD\tR0, R1\t\t// end
+\n+_loop:
+\tCMP\tR0, R1
+\tB.EQ\t_notfound
+\tMOVBU.P\t1(R0), R3
+\tCMP\tR2, R3
+\tB.NE\t_loop
+\n+\tSUB\t$1, R0\t\t// R0 will be one beyond the position we want
+\tSUB\tR4, R0\t\t// remove base
+\tMOVW R0, index+16(FP)
+\tRET
+\n+_notfound:
+\tMOVW\t$-1, R0
+\tMOVW\tR0, index+16(FP)
+\tRET
-// no memcmp implementation on arm yet
TEXT ·Equal(SB),7,$0
\tB\t·equalPortable(SB)
コアとなるコードの解説
変更されたsrc/pkg/bytes/asm_arm.s
ファイルは、ARMアーキテクチャ向けのアセンブリコードを格納しています。このコミットの主要な変更点は、IndexByte
関数の実装を、Goで書かれたポータブルバージョン(·indexBytePortable(SB)
への分岐)から、ARMアセンブリで直接記述された高速なバージョンに置き換えたことです。
新しいアセンブリコードは、以下のステップでIndexByte
の機能を実現しています。
-
引数のロード:
MOVW base+0(FP), R0
: 検索対象のバイトスライスs
の先頭アドレスをR0
レジスタにロードします。MOVW len+4(FP), R1
: バイトスライスs
の長さをR1
レジスタにロードします。MOVBU c+12(FP), R2
: 検索するバイトc
をR2
レジスタにロードします。MOVBU
はバイトを読み込み、上位ビットをゼロで埋めてワード(32ビット)として扱います。MOVW R0, R4
: スライスの開始アドレス(R0
の初期値)をR4
に保存します。これは、最終的なインデックスを計算する際に必要になります。ADD R0, R1
:R0
(現在のポインタ)にR1
(長さ)を加算し、結果をR0
に格納します。これにより、R0
はスライスの終端の次のアドレスを指すようになります。これはループの終了条件として機能します。
-
検索ループ (
_loop
):_loop:
: ループの開始点を示すラベル。CMP R0, R1
: 現在のポインタR0
とスライスの終端アドレスR1
を比較します。B.EQ _notfound
: もしR0
がR1
と等しい場合、つまりスライスの終端に到達した場合は、_notfound
ラベルにジャンプします(検索対象が見つからなかった場合)。MOVBU.P 1(R0), R3
:R0
が指すメモリ位置から1バイトを読み込み、R3
レジスタに格納します。.P
サフィックスは「ポストインクリメント」を意味し、読み込み後にR0
レジスタの値を1バイト分自動的に増加させます。これにより、次のループイテレーションで自動的に次のバイトを指すようになります。CMP R2, R3
: 読み込んだバイトR3
と検索対象のバイトR2
を比較します。B.NE _loop
: もしR2
とR3
が等しくない場合(バイトが一致しない場合)、_loop
ラベルにジャンプして次のバイトの検索を続行します。
-
バイトが見つかった場合:
- ループを抜けた場合、それは
R2
とR3
が一致したことを意味します。この時、R0
は一致したバイトの次のアドレスを指しています(MOVBU.P
によるポストインクリメントのため)。 SUB $1, R0
:R0
から1を減算し、一致したバイトの正確なアドレスに戻します。SUB R4, R0
:R0
(一致したバイトのアドレス)からR4
(スライスの開始アドレス)を減算します。これにより、スライスの先頭からのオフセット、つまりインデックスが計算されます。MOVW R0, index+16(FP)
: 計算されたインデックスを関数の戻り値index
に格納します。RET
: 関数から戻ります。
- ループを抜けた場合、それは
-
バイトが見つからなかった場合 (
_notfound
):_notfound:
: 検索対象のバイトが見つからずにスライスの終端に達した場合のラベル。MOVW $-1, R0
: 戻り値として-1
をR0
レジスタにロードします。MOVW R0, index+16(FP)
:-1
を戻り値index
に格納します。RET
: 関数から戻ります。
このアセンブリコードは、ARMプロセッサの命令セットを直接利用することで、バイトスライス内のバイト検索を非常に効率的に行っています。特に、MOVBU.P
のような命令は、Goのポータブルコードでは複数の命令に分解される処理を単一の命令で実行できるため、命令フェッチや実行サイクルのオーバーヘッドを削減し、大幅なパフォーマンス向上に寄与しています。
関連リンク
- Go言語の
bytes
パッケージドキュメント: https://pkg.go.dev/bytes - Go言語のアセンブリについて(公式ドキュメント): https://go.dev/doc/asm
- Goの
bytes.IndexByte
のソースコード(Go言語版): https://cs.opensource.google/go/go/+/refs/tags/go1.22.4:src/bytes/bytes.go;l=100 (時期によって実装は異なる可能性がありますが、一般的なGo実装の例として)
参考にした情報源リンク
- Go Assembly Language: https://go.dev/doc/asm
- ARM Architecture Reference Manual (ARM ARM): ARM命令セットの詳細なリファレンス。
- Go
bytes
package source code: https://github.com/golang/go/tree/master/src/bytes memchr
C standard library function: https://en.cppreference.com/w/c/string/byte/memchr- Go benchmark documentation: https://go.dev/doc/articles/go_benchmarking
- Dave Cheney's blog (for general Go performance insights): https://dave.cheney.net/ (具体的な記事は特定していませんが、Goのパフォーマンスに関する彼の貢献は多岐にわたります)
- Go CL 6106044: https://golang.org/cl/6106044 (コミットメッセージに記載されているChange Listへのリンク)
[インデックス 12953] ファイルの概要
このコミットは、Go言語の標準ライブラリbytes
パッケージ内のIndexByte
関数に対し、ARMアーキテクチャ向けのアセンブリ言語による最適化を導入するものです。これにより、特定のバイトをバイトスライス内で検索する際のパフォーマンスが大幅に向上しました。ベンチマーク結果は、最大で約82%の実行時間短縮(ns/op)と、約5.6倍のスループット向上(MB/s)を示しています。
コミット
commit 0681b13437e36de582521d5b9f1b4664400312a9
Author: Dave Cheney <dave@cheney.net>
Date: Wed Apr 25 13:18:31 2012 +1000
bytes: add assembly version of IndexByte for ARM
benchmark old ns/op new ns/op delta
BenchmarkIndexByte32 459 126 -72.55%
BenchmarkIndexByte4K 52404 10939 -79.13%
BenchmarkIndexByte4M 54470800 11177370 -79.48%
BenchmarkIndexByte64M 1010803000 178860500 -82.31%
benchmark old MB/s new MB/s speedup
BenchmarkIndexByte32 69.58 252.63 3.63x
BenchmarkIndexByte4K 78.16 374.42 4.79x
BenchmarkIndexByte4M 77.00 375.25 4.87x
BenchmarkIndexByte64M 66.39 375.20 5.65x
R=rsc, minux.ma
CC=golang-dev
https://golang.org/cl/6106044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0681b13437e36de582521d5b9f1b4664400312a9
元コミット内容
bytes: add assembly version of IndexByte for ARM
benchmark old ns/op new ns/op delta
BenchmarkIndexByte32 459 126 -72.55%
BenchmarkIndexByte4K 52404 10939 -79.13%
BenchmarkIndexByte4M 54470800 11177370 -79.48%
BenchmarkIndexByte64M 1010803000 178860500 -82.31%
benchmark old MB/s new MB/s speedup
BenchmarkIndexByte32 69.58 252.63 3.63x
BenchmarkIndexByte4K 78.16 374.42 4.79x
BenchmarkIndexByte4M 77.00 375.25 4.87x
BenchmarkIndexByte64M 66.39 375.20 5.65x
R=rsc, minux.ma
CC=golang-dev
https://golang.org/cl/6106044
変更の背景
この変更の主な背景は、Go言語のbytes.IndexByte
関数のARMアーキテクチャ上でのパフォーマンスを劇的に改善することにあります。IndexByte
は、バイトスライス([]byte
)の中から特定のバイトが最初に現れるインデックスを検索する基本的な操作であり、文字列処理やバイナリデータの解析など、多くの場面で頻繁に利用されます。
Go言語は通常、高レベルな抽象化を提供し、開発者がアセンブリ言語を直接書く必要がないように設計されています。しかし、一部のクリティカルな関数、特にCPUのレジスタやメモリ操作に直接アクセスすることで大幅なパフォーマンス向上が見込めるような低レベルな処理においては、アセンブリ言語による最適化が採用されることがあります。
ARMプロセッサは、モバイルデバイス、組み込みシステム、そして近年ではサーバー分野でも広く利用されており、Go言語がこれらのプラットフォームで効率的に動作することは非常に重要です。既存のIndexByte
のポータブル(Goで書かれた)実装では、ARMアーキテクチャの特性を十分に活かしきれていなかったため、アセンブリ言語を用いてより効率的な検索アルゴリズムを実装することで、実行速度とスループットのボトルネックを解消することが目指されました。
コミットメッセージに示されているベンチマーク結果は、この最適化が非常に効果的であったことを明確に示しています。特に大きなデータサイズ(4MBや64MB)での改善率が顕著であり、これは大量のデータを扱うアプリケーションにおいて、この変更が大きな恩恵をもたらすことを意味します。
前提知識の解説
Go言語におけるアセンブリ
Go言語は、通常はGoで記述されたコードをコンパイルしますが、パフォーマンスが極めて重要な一部の関数や、特定のハードウェア機能にアクセスする必要がある場合には、アセンブリ言語で記述されたコード(通常は.s
拡張子のファイル)をGoのパッケージに含めることができます。Goのアセンブリは、Plan 9アセンブラの文法に基づいており、一般的なx86やARMのアセンブリとは異なる独自の記法を持ちます。Goのアセンブリは、Goのランタイムや標準ライブラリの低レベルな部分で利用され、例えばメモリ操作、コンテキストスイッチ、特定のCPU命令の利用などに用いられます。
ARMアーキテクチャ
ARM(Advanced RISC Machine)は、RISC(Reduced Instruction Set Computer)アーキテクチャに基づくプロセッサファミリーです。低消費電力と高い性能効率が特徴で、スマートフォン、タブレット、組み込みシステム、IoTデバイスなど、幅広い分野でデファクトスタンダードとなっています。近年では、Apple MシリーズチップやAWS Gravitonプロセッサなど、高性能コンピューティング分野でも採用が拡大しています。ARMプロセッサは、レジスタベースのアーキテクチャであり、命令セットが比較的シンプルで、パイプライン処理に適しています。
bytes.IndexByte
関数
Go言語の標準ライブラリbytes
パッケージに含まれるIndexByte
関数は、func IndexByte(s []byte, c byte) int
というシグネチャを持ちます。この関数は、バイトスライスs
の中から、指定されたバイトc
が最初に現れるインデックスを返します。もしc
が見つからない場合は-1
を返します。これは、C言語の標準ライブラリ関数memchr
に相当する機能を提供します。
memchr
memchr
は、C言語の標準ライブラリ関数で、void *memchr(const void *s, int c, size_t n);
というシグネチャを持ちます。これは、メモリブロックs
の最初のn
バイトの中から、指定されたバイトc
が最初に現れる位置を検索します。見つかった場合はその位置へのポインタを返し、見つからない場合はNULLを返します。bytes.IndexByte
は、Go言語でこのmemchr
と同様の機能を提供するものです。
ベンチマーク指標 (ns/op
, MB/s
)
ns/op
(nanoseconds per operation): 1回の操作にかかる平均時間(ナノ秒)。この値が小さいほど、処理が速いことを意味します。コミットのベンチマークでは、この値が大幅に減少しており、個々の検索操作が高速化されたことを示しています。MB/s
(megabytes per second): 1秒あたりに処理できるデータ量(メガバイト)。この値が大きいほど、スループットが高いことを意味します。コミットのベンチマークでは、この値が大幅に増加しており、単位時間あたりにより多くのデータを検索できるようになったことを示しています。delta
/speedup
: 変更前後の性能差を示す指標です。delta
はns/op
の改善率(減少率)を示し、speedup
はMB/s
の向上倍率を示します。
技術的詳細
このコミットでは、bytes.IndexByte
関数のARMアーキテクチャ向け実装を、Goで書かれたポータブルバージョンからアセンブリ言語バージョンに置き換えることで最適化を行っています。
Goのアセンブリコードは、Goの関数呼び出し規約に従って引数を受け取り、結果を返します。
TEXT ·IndexByte(SB),7,$0
:IndexByte
関数のアセンブリ実装の開始を宣言します。SB
はStatic Baseレジスタで、グローバルシンボルへのオフセット計算に使われます。7
はフラグ、$0
はスタックフレームサイズ(この関数ではローカル変数を必要としないため0)。MOVW base+0(FP), R0
: 関数引数s
(バイトスライスの先頭アドレス)をR0
レジスタにロードします。FP
はFrame Pointerで、関数引数やローカル変数へのアクセスに使われます。base+0(FP)
はs
の先頭アドレスを指します。MOVW len+4(FP), R1
: 関数引数s
の長さ(len
)をR1
レジスタにロードします。Goのバイトスライスは、ポインタ、長さ、容量の3つの要素で構成されます。len+4(FP)
はs
の長さの部分を指します。MOVBU c+12(FP), R2
: 検索対象のバイトc
をR2
レジスタにロードします。c+12(FP)
はc
の値を指します。MOVBU
はバイトをロードし、ゼロ拡張してワード(32ビット)に格納する命令です。MOVW R0, R4
: スライスの開始アドレス(R0
に格納されている)をR4
にコピーします。これは、後でインデックスを計算するために元の開始アドレスを保持しておくためです。ADD R0, R1
:R0
(現在のポインタ)にR1
(長さ)を加算し、結果をR0
に格納します。これによりR0
はスライスの終端アドレス(終端の1バイト先)を指すようになります。これはループの終了条件に使われます。
検索ループ (_loop
):
_loop:
: ループの開始ラベル。CMP R0, R1
: 現在のポインタR0
とスライスの終端アドレスR1
を比較します。B.EQ _notfound
: もしR0
とR1
が等しい場合(つまり、スライスの終端に達した場合)、_notfound
ラベルに分岐します。これは検索対象のバイトが見つからなかったことを意味します。MOVBU.P 1(R0), R3
:R0
が指すメモリ位置から1バイトを読み込み、R3
レジスタに格納します。.P
サフィックスは、読み込み後にR0
を1バイト進める(ポストインクリメント)ことを意味します。これにより、ポインタが自動的に次のバイトに移動し、ループ内で明示的なポインタ加算が不要になります。CMP R2, R3
: 読み込んだバイトR3
と検索対象のバイトR2
を比較します。B.NE _loop
: もしR2
とR3
が等しくない場合(つまり、バイトが一致しない場合)、_loop
ラベルに分岐し、次のバイトの検索を続行します。
バイトが見つかった場合:
SUB $1, R0
: ループを抜けた時点のR0
は、見つかったバイトの1バイト先を指しています。そのため、R0
から1を減算して、見つかったバイトの正確なアドレスに戻します。SUB R4, R0
:R0
(見つかったバイトのアドレス)からR4
(スライスの開始アドレス)を減算することで、見つかったバイトのインデックス(オフセット)を計算します。MOVW R0, index+16(FP)
: 計算されたインデックスを関数の戻り値index
に格納します。index+16(FP)
は戻り値の格納場所を指します。RET
: 関数から戻ります。
バイトが見つからなかった場合 (_notfound
):
_notfound:
: バイトが見つからなかった場合のラベル。MOVW $-1, R0
: 戻り値として-1
をR0
にロードします。MOVW R0, index+16(FP)
:R0
の値を戻り値index
に格納します。RET
: 関数から戻ります。
このアセンブリ実装は、Goで書かれたポータブルバージョンと比較して、以下のような点でパフォーマンスを向上させています。
- 直接的なレジスタ操作: Goのコンパイラが生成するコードよりも、より直接的にARMプロセッサのレジスタを操作し、メモリへのアクセス回数を減らしています。
- 効率的なループ:
MOVBU.P
のようなポストインクリメント命令を使用することで、ループ内の命令数を削減し、パイプライン処理の効率を高めています。 - 条件分岐の最適化: 比較と分岐命令を組み合わせることで、検索ロジックをCPUが効率的に実行できるようにしています。
これらの最適化により、特に大きなバイトスライスを扱う際に、CPUキャッシュの利用効率が向上し、命令の実行サイクルが削減され、結果として大幅なパフォーマンス向上が実現されました。
コアとなるコードの変更箇所
--- a/src/pkg/bytes/asm_arm.s
+++ b/src/pkg/bytes/asm_arm.s
@@ -2,10 +2,29 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// no memchr implementation on arm yet
TEXT ·IndexByte(SB),7,$0
-\tB\t·indexBytePortable(SB)\n+\tMOVW\tbase+0(FP), R0
+\tMOVW\tlen+4(FP), R1
+\tMOVBU\tc+12(FP), R2\t// byte to find
+\tMOVW\tR0, R4\t\t// store base for later
+\tADD\tR0, R1\t\t// end
+\n+_loop:
+\tCMP\tR0, R1
+\tB.EQ\t_notfound
+\tMOVBU.P\t1(R0), R3
+\tCMP\tR2, R3
+\tB.NE\t_loop
+\n+\tSUB\t$1, R0\t\t// R0 will be one beyond the position we want
+\tSUB\tR4, R0\t\t// remove base
+\tMOVW R0, index+16(FP)
+\tRET
+\n+_notfound:
+\tMOVW\t$-1, R0
+\tMOVW\tR0, index+16(FP)
+\tRET
-// no memcmp implementation on arm yet
TEXT ·Equal(SB),7,$0
\tB\t·equalPortable(SB)
コアとなるコードの解説
変更されたsrc/pkg/bytes/asm_arm.s
ファイルは、ARMアーキテクチャ向けのアセンブリコードを格納しています。このコミットの主要な変更点は、IndexByte
関数の実装を、Goで書かれたポータブルバージョン(·indexBytePortable(SB)
への分岐)から、ARMアセンブリで直接記述された高速なバージョンに置き換えたことです。
新しいアセンブリコードは、以下のステップでIndexByte
の機能を実現しています。
-
引数のロード:
MOVW base+0(FP), R0
: 検索対象のバイトスライスs
の先頭アドレスをR0
レジスタにロードします。MOVW len+4(FP), R1
: バイトスライスs
の長さをR1
レジスタにロードします。MOVBU c+12(FP), R2
: 検索するバイトc
をR2
レジスタにロードします。MOVBU
はバイトを読み込み、上位ビットをゼロで埋めてワード(32ビット)として扱います。MOVW R0, R4
: スライスの開始アドレス(R0
の初期値)をR4
に保存します。これは、最終的なインデックスを計算する際に必要になります。ADD R0, R1
:R0
(現在のポインタ)にR1
(長さ)を加算し、結果をR0
に格納します。これにより、R0
はスライスの終端の次のアドレスを指すようになります。これはループの終了条件として機能します。
-
検索ループ (
_loop
):_loop:
: ループの開始点を示すラベル。CMP R0, R1
: 現在のポインタR0
とスライスの終端アドレスR1
を比較します。B.EQ _notfound
: もしR0
がR1
と等しい場合、つまりスライスの終端に到達した場合は、_notfound
ラベルにジャンプします(検索対象が見つからなかった場合)。MOVBU.P 1(R0), R3
:R0
が指すメモリ位置から1バイトを読み込み、R3
レジスタに格納します。.P
サフィックスは「ポストインクリメント」を意味し、読み込み後にR0
レジスタの値を1バイト分自動的に増加させます。これにより、次のループイテレーションで自動的に次のバイトを指すようになります。CMP R2, R3
: 読み込んだバイトR3
と検索対象のバイトR2
を比較します。B.NE _loop
: もしR2
とR3
が等しくない場合(バイトが一致しない場合)、_loop
ラベルにジャンプして次のバイトの検索を続行します。
-
バイトが見つかった場合:
- ループを抜けた場合、それは
R2
とR3
が一致したことを意味します。この時、R0
は一致したバイトの次のアドレスを指しています(MOVBU.P
によるポストインクリメントのため)。 SUB $1, R0
:R0
から1を減算し、一致したバイトの正確なアドレスに戻します。SUB R4, R0
:R0
(一致したバイトのアドレス)からR4
(スライスの開始アドレス)を減算します。これにより、スライスの先頭からのオフセット、つまりインデックスが計算されます。MOVW R0, index+16(FP)
: 計算されたインデックスを関数の戻り値index
に格納します。RET
: 関数から戻ります。
- ループを抜けた場合、それは
-
バイトが見つからなかった場合 (
_notfound
):_notfound:
: 検索対象のバイトが見つからずにスライスの終端に達した場合のラベル。MOVW $-1, R0
: 戻り値として-1
をR0
レジスタにロードします。MOVW R0, index+16(FP)
:-1
を戻り値index
に格納します。RET
: 関数から戻ります。
このアセンブリコードは、ARMプロセッサの命令セットを直接利用することで、バイトスライス内のバイト検索を非常に効率的に行っています。特に、MOVBU.P
のような命令は、Goのポータブルコードでは複数の命令に分解される処理を単一の命令で実行できるため、命令フェッチや実行サイクルのオーバーヘッドを削減し、大幅なパフォーマンス向上に寄与しています。
関連リンク
- Go言語の
bytes
パッケージドキュメント: https://pkg.go.dev/bytes - Go言語のアセンブリについて(公式ドキュメント): https://go.dev/doc/asm
- Goの
bytes.IndexByte
のソースコード(Go言語版): https://cs.opensource.google/go/go/+/refs/tags/go1.22.4:src/bytes/bytes.go;l=100 (時期によって実装は異なる可能性がありますが、一般的なGo実装の例として)
参考にした情報源リンク
- Go Assembly Language: https://go.dev/doc/asm
- ARM Architecture Reference Manual (ARM ARM): ARM命令セットの詳細なリファレンス。
- Go
bytes
package source code: https://github.com/golang/go/tree/master/src/bytes memchr
C standard library function: https://en.cppreference.com/w/c/string/byte/memchr- Go benchmark documentation: https://go.dev/doc/articles/go_benchmarking
- Dave Cheney's blog (for general Go performance insights): https://dave.cheney.net/ (具体的な記事は特定していませんが、Goのパフォーマンスに関する彼の貢献は多岐にわたります)
- Go CL 6106044: https://golang.org/cl/6106044 (コミットメッセージに記載されているChange Listへのリンク)