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

[インデックス 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: 変更前後の性能差を示す指標です。deltans/opの改善率(減少率)を示し、speedupMB/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: 検索対象のバイトcR2レジスタにロードします。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: もしR0R1が等しい場合(つまり、スライスの終端に達した場合)、_notfoundラベルに分岐します。これは検索対象のバイトが見つからなかったことを意味します。
  • MOVBU.P 1(R0), R3: R0が指すメモリ位置から1バイトを読み込み、R3レジスタに格納します。.Pサフィックスは、読み込み後にR0を1バイト進める(ポストインクリメント)ことを意味します。これにより、ポインタが自動的に次のバイトに移動し、ループ内で明示的なポインタ加算が不要になります。
  • CMP R2, R3: 読み込んだバイトR3と検索対象のバイトR2を比較します。
  • B.NE _loop: もしR2R3が等しくない場合(つまり、バイトが一致しない場合)、_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: 戻り値として-1R0にロードします。
  • MOVW R0, index+16(FP): R0の値を戻り値indexに格納します。
  • RET: 関数から戻ります。

このアセンブリ実装は、Goで書かれたポータブルバージョンと比較して、以下のような点でパフォーマンスを向上させています。

  1. 直接的なレジスタ操作: Goのコンパイラが生成するコードよりも、より直接的にARMプロセッサのレジスタを操作し、メモリへのアクセス回数を減らしています。
  2. 効率的なループ: MOVBU.Pのようなポストインクリメント命令を使用することで、ループ内の命令数を削減し、パイプライン処理の効率を高めています。
  3. 条件分岐の最適化: 比較と分岐命令を組み合わせることで、検索ロジックを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の機能を実現しています。

  1. 引数のロード:

    • MOVW base+0(FP), R0: 検索対象のバイトスライスsの先頭アドレスをR0レジスタにロードします。
    • MOVW len+4(FP), R1: バイトスライスsの長さをR1レジスタにロードします。
    • MOVBU c+12(FP), R2: 検索するバイトcR2レジスタにロードします。MOVBUはバイトを読み込み、上位ビットをゼロで埋めてワード(32ビット)として扱います。
    • MOVW R0, R4: スライスの開始アドレス(R0の初期値)をR4に保存します。これは、最終的なインデックスを計算する際に必要になります。
    • ADD R0, R1: R0(現在のポインタ)にR1(長さ)を加算し、結果をR0に格納します。これにより、R0はスライスの終端の次のアドレスを指すようになります。これはループの終了条件として機能します。
  2. 検索ループ (_loop):

    • _loop:: ループの開始点を示すラベル。
    • CMP R0, R1: 現在のポインタR0とスライスの終端アドレスR1を比較します。
    • B.EQ _notfound: もしR0R1と等しい場合、つまりスライスの終端に到達した場合は、_notfoundラベルにジャンプします(検索対象が見つからなかった場合)。
    • MOVBU.P 1(R0), R3: R0が指すメモリ位置から1バイトを読み込み、R3レジスタに格納します。.Pサフィックスは「ポストインクリメント」を意味し、読み込み後にR0レジスタの値を1バイト分自動的に増加させます。これにより、次のループイテレーションで自動的に次のバイトを指すようになります。
    • CMP R2, R3: 読み込んだバイトR3と検索対象のバイトR2を比較します。
    • B.NE _loop: もしR2R3が等しくない場合(バイトが一致しない場合)、_loopラベルにジャンプして次のバイトの検索を続行します。
  3. バイトが見つかった場合:

    • ループを抜けた場合、それはR2R3が一致したことを意味します。この時、R0は一致したバイトの次のアドレスを指しています(MOVBU.Pによるポストインクリメントのため)。
    • SUB $1, R0: R0から1を減算し、一致したバイトの正確なアドレスに戻します。
    • SUB R4, R0: R0(一致したバイトのアドレス)からR4(スライスの開始アドレス)を減算します。これにより、スライスの先頭からのオフセット、つまりインデックスが計算されます。
    • MOVW R0, index+16(FP): 計算されたインデックスを関数の戻り値indexに格納します。
    • RET: 関数から戻ります。
  4. バイトが見つからなかった場合 (_notfound):

    • _notfound:: 検索対象のバイトが見つからずにスライスの終端に達した場合のラベル。
    • MOVW $-1, R0: 戻り値として-1R0レジスタにロードします。
    • MOVW R0, index+16(FP): -1を戻り値indexに格納します。
    • RET: 関数から戻ります。

このアセンブリコードは、ARMプロセッサの命令セットを直接利用することで、バイトスライス内のバイト検索を非常に効率的に行っています。特に、MOVBU.Pのような命令は、Goのポータブルコードでは複数の命令に分解される処理を単一の命令で実行できるため、命令フェッチや実行サイクルのオーバーヘッドを削減し、大幅なパフォーマンス向上に寄与しています。

関連リンク

参考にした情報源リンク

[インデックス 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: 変更前後の性能差を示す指標です。deltans/opの改善率(減少率)を示し、speedupMB/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: 検索対象のバイトcR2レジスタにロードします。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: もしR0R1が等しい場合(つまり、スライスの終端に達した場合)、_notfoundラベルに分岐します。これは検索対象のバイトが見つからなかったことを意味します。
  • MOVBU.P 1(R0), R3: R0が指すメモリ位置から1バイトを読み込み、R3レジスタに格納します。.Pサフィックスは、読み込み後にR0を1バイト進める(ポストインクリメント)ことを意味します。これにより、ポインタが自動的に次のバイトに移動し、ループ内で明示的なポインタ加算が不要になります。
  • CMP R2, R3: 読み込んだバイトR3と検索対象のバイトR2を比較します。
  • B.NE _loop: もしR2R3が等しくない場合(つまり、バイトが一致しない場合)、_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: 戻り値として-1R0にロードします。
  • MOVW R0, index+16(FP): R0の値を戻り値indexに格納します。
  • RET: 関数から戻ります。

このアセンブリ実装は、Goで書かれたポータブルバージョンと比較して、以下のような点でパフォーマンスを向上させています。

  1. 直接的なレジスタ操作: Goのコンパイラが生成するコードよりも、より直接的にARMプロセッサのレジスタを操作し、メモリへのアクセス回数を減らしています。
  2. 効率的なループ: MOVBU.Pのようなポストインクリメント命令を使用することで、ループ内の命令数を削減し、パイプライン処理の効率を高めています。
  3. 条件分岐の最適化: 比較と分岐命令を組み合わせることで、検索ロジックを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の機能を実現しています。

  1. 引数のロード:

    • MOVW base+0(FP), R0: 検索対象のバイトスライスsの先頭アドレスをR0レジスタにロードします。
    • MOVW len+4(FP), R1: バイトスライスsの長さをR1レジスタにロードします。
    • MOVBU c+12(FP), R2: 検索するバイトcR2レジスタにロードします。MOVBUはバイトを読み込み、上位ビットをゼロで埋めてワード(32ビット)として扱います。
    • MOVW R0, R4: スライスの開始アドレス(R0の初期値)をR4に保存します。これは、最終的なインデックスを計算する際に必要になります。
    • ADD R0, R1: R0(現在のポインタ)にR1(長さ)を加算し、結果をR0に格納します。これにより、R0はスライスの終端の次のアドレスを指すようになります。これはループの終了条件として機能します。
  2. 検索ループ (_loop):

    • _loop:: ループの開始点を示すラベル。
    • CMP R0, R1: 現在のポインタR0とスライスの終端アドレスR1を比較します。
    • B.EQ _notfound: もしR0R1と等しい場合、つまりスライスの終端に到達した場合は、_notfoundラベルにジャンプします(検索対象が見つからなかった場合)。
    • MOVBU.P 1(R0), R3: R0が指すメモリ位置から1バイトを読み込み、R3レジスタに格納します。.Pサフィックスは「ポストインクリメント」を意味し、読み込み後にR0レジスタの値を1バイト分自動的に増加させます。これにより、次のループイテレーションで自動的に次のバイトを指すようになります。
    • CMP R2, R3: 読み込んだバイトR3と検索対象のバイトR2を比較します。
    • B.NE _loop: もしR2R3が等しくない場合(バイトが一致しない場合)、_loopラベルにジャンプして次のバイトの検索を続行します。
  3. バイトが見つかった場合:

    • ループを抜けた場合、それはR2R3が一致したことを意味します。この時、R0は一致したバイトの次のアドレスを指しています(MOVBU.Pによるポストインクリメントのため)。
    • SUB $1, R0: R0から1を減算し、一致したバイトの正確なアドレスに戻します。
    • SUB R4, R0: R0(一致したバイトのアドレス)からR4(スライスの開始アドレス)を減算します。これにより、スライスの先頭からのオフセット、つまりインデックスが計算されます。
    • MOVW R0, index+16(FP): 計算されたインデックスを関数の戻り値indexに格納します。
    • RET: 関数から戻ります。
  4. バイトが見つからなかった場合 (_notfound):

    • _notfound:: 検索対象のバイトが見つからずにスライスの終端に達した場合のラベル。
    • MOVW $-1, R0: 戻り値として-1R0レジスタにロードします。
    • MOVW R0, index+16(FP): -1を戻り値indexに格納します。
    • RET: 関数から戻ります。

このアセンブリコードは、ARMプロセッサの命令セットを直接利用することで、バイトスライス内のバイト検索を非常に効率的に行っています。特に、MOVBU.Pのような命令は、Goのポータブルコードでは複数の命令に分解される処理を単一の命令で実行できるため、命令フェッチや実行サイクルのオーバーヘッドを削減し、大幅なパフォーマンス向上に寄与しています。

関連リンク

参考にした情報源リンク