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

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

このコミットは、Goランタイムのamd64アーキテクチャにおけるmemmove(メモリコピー)関数のパフォーマンスを向上させるものです。特に、257バイトから約2KB(2048バイト)の中間サイズのメモリコピーにおいて、REP MOVSQ命令の代わりにSSEレジスタを用いたMOV命令を使用することで、処理速度を大幅に改善しています。

コミット

commit a712e20a1d1587e115c1295df0850120cdfa3e44
Author: Rui Ueyama <ruiu@google.com>
Date:   Mon Jun 23 12:06:26 2014 -0700

    runtime: speed up amd64 memmove
    
    MOV with SSE registers seems faster than REP MOVSQ if the
    size being copied is less than about 2K. Previously we
    didn't use MOV if the memory region is larger than 256
    byte. This patch improves the performance of 257 ~ 2048
    byte non-overlapping copy by using MOV.
    
    Here is the benchmark result on Intel Xeon 3.5GHz (Nehalem).
    
    benchmark               old ns/op    new ns/op    delta
    BenchmarkMemmove16              4            4   +0.42%
    BenchmarkMemmove32              5            5   -0.20%
    BenchmarkMemmove64              6            6   -0.81%
    BenchmarkMemmove128             7            7   -0.82%
    BenchmarkMemmove256            10           10   +1.92%
    BenchmarkMemmove512            29           16  -44.90%
    BenchmarkMemmove1024           37           25  -31.55%
    BenchmarkMemmove2048           55           44  -19.46%
    BenchmarkMemmove4096           92           91   -0.76%
    
    benchmark                old MB/s     new MB/s  speedup
    BenchmarkMemmove16        3370.61      3356.88    1.00x
    BenchmarkMemmove32        6368.68      6386.99    1.00x
    BenchmarkMemmove64       10367.37     10462.62    1.01x
    BenchmarkMemmove128      17551.16     17713.48    1.01x
    BenchmarkMemmove256      24692.81     24142.99    0.98x
    BenchmarkMemmove512      17428.70     31687.72    1.82x
    BenchmarkMemmove1024     27401.82     40009.45    1.46x
    BenchmarkMemmove2048     36884.86     45766.98    1.24x
    BenchmarkMemmove4096     44295.91     44627.86    1.01x
    
    LGTM=khr
    R=golang-codereviews, gobot, khr
    CC=golang-codereviews
    https://golang.org/cl/90500043

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

https://github.com/golang/go/commit/a712e20a1d1587e115c1295df0850120cdfa3e44

元コミット内容

runtime: speed up amd64 memmove

MOV with SSE registers seems faster than REP MOVSQ if the
size being copied is less than about 2K. Previously we
didn't use MOV if the memory region is larger than 256
byte. This patch improves the performance of 257 ~ 2048
byte non-overlapping copy by using MOV.

Here is the benchmark result on Intel Xeon 3.5GHz (Nehalem).

benchmark               old ns/op    new ns/op    delta
BenchmarkMemmove16              4            4   +0.42%
BenchmarkMemmove32              5            5   -0.20%
BenchmarkMemmove64              6            6   -0.81%
BenchmarkMemmove128             7            7   -0.82%
BenchmarkMemmove256            10           10   +1.92%
BenchmarkMemmove512            29           16  -44.90%
BenchmarkMemmove1024           37           25  -31.55%
BenchmarkMemmove2048           55           44  -19.46%
BenchmarkMemmove4096           92           91   -0.76%

benchmark                old MB/s     new MB/s  speedup
BenchmarkMemmove16        3370.61      3356.88    1.00x
BenchmarkMemmove32        6368.68      6386.99    1.00x
BenchmarkMemmove64       10367.37     10462.62    1.01x
BenchmarkMemmove128      17551.16     17713.48    1.01x
BenchmarkMemmove256      24692.81     24142.99    0.98x
BenchmarkMemmove512      17428.70     31687.72    1.82x
BenchmarkMemmove1024     27401.82     40009.45    1.46x
BenchmarkMemmove2048     36884.86     45766.98    1.24x
BenchmarkMemmove4096     44295.91     44627.86    1.01x

LGTM=khr
R=golang-codereviews, gobot, khr
CC=golang-codereviews
https://golang.org/cl/90500043

変更の背景

Goランタイムのmemmove関数は、メモリブロックを効率的にコピーするために使用されます。amd64アーキテクチャでは、この操作に主に2つのアプローチがあります。

  1. REP MOVSQ命令: これは、大量のデータを効率的にコピーするためのx86命令です。MOVSQは8バイト(クアッドワード)を移動し、REPプレフィックスは指定された回数だけこの操作を繰り返します。非常に大きなデータブロックのコピーには適していますが、命令の実行開始に一定のオーバーヘッド(スタートアップコスト)があります。
  2. SSEレジスタを用いたMOV命令: SSE(Streaming SIMD Extensions)は、x86プロセッサの命令セット拡張であり、SIMD(Single Instruction, Multiple Data)操作を可能にします。これには、128ビットのXMMレジスタを使用するMOVOU(Move Unaligned Oword)のような命令が含まれ、一度に16バイトのデータを移動できます。

このコミット以前のmemmoveの実装では、256バイトを超えるメモリ領域のコピーにはREP MOVSQが使用されていました。しかし、ベンチマークの結果、約2KB(2048バイト)未満のサイズでは、REP MOVSQのスタートアップコストが原因で、SSEレジスタを用いたMOV命令の方が高速であることが判明しました。

この変更の背景には、特に257バイトから2048バイトという中間サイズのメモリコピーにおいて、REP MOVSQの非効率性を解消し、より高速なMOV命令に切り替えることで、Goプログラム全体のパフォーマンスを向上させるという目的があります。コミットメッセージに記載されているベンチマーク結果は、この最適化が特に512バイト、1024バイト、2048バイトのコピーで顕著な速度向上をもたらしたことを明確に示しています。

前提知識の解説

  • memmove: C言語の標準ライブラリ関数であり、Goランタイムでも内部的に使用されるメモリコピー関数です。memcpyと異なり、コピー元とコピー先のメモリ領域が重なっていても正しく動作することが保証されています。これは、重なりがある場合にコピー方向を調整することで実現されます。
  • amd64アーキテクチャ: IntelおよびAMDが開発した64ビットのx86命令セットアーキテクチャです。Go言語は、このアーキテクチャ上で非常に効率的に動作するように設計されており、パフォーマンスが重要なランタイム部分はアセンブリ言語で記述されることがあります。
  • REP MOVSQ命令: x86アセンブリ命令の一つで、MOVSQ(Move String Quadword)命令をREP(Repeat)プレフィックスと組み合わせて使用します。MOVSQRSI(ソースインデックスレジスタ)が指すアドレスからRDI(デスティネーションインデックスレジスタ)が指すアドレスへ8バイトのデータをコピーし、両方のレジスタを更新します。REPプレフィックスは、RCX(カウントレジスタ)がゼロになるまでこの操作を繰り返します。これは、大量の連続したメモリブロックをコピーする際に非常に効率的ですが、命令のセットアップと終了に一定の時間がかかります。
  • SSE (Streaming SIMD Extensions): Intelが開発したSIMD(Single Instruction, Multiple Data)命令セットの拡張です。SSEは、浮動小数点演算やメディア処理の高速化を目的として導入されましたが、メモリ操作にも利用されます。SSEは128ビットのXMMレジスタ(XMM0からXMM15)を使用し、一度に16バイトのデータを処理できます。
    • MOVOU命令: SSE命令の一つで、「Move Unaligned Oword」の略です。これは、アラインメントされていないメモリ位置から16バイト(Oword = Octa-word = 128ビット)のデータをXMMレジスタにロードしたり、XMMレジスタからメモリにストアしたりするために使用されます。アラインメントを気にせずに高速なメモリコピーを行うのに適しています。
  • Goランタイムにおけるアセンブリ言語: Go言語のランタイム(ガベージコレクタ、スケジューラ、プリミティブなメモリ操作など)のパフォーマンスが非常に重要な部分は、Goアセンブリ言語(Plan 9アセンブラの構文に基づく)で記述されています。これにより、特定のハードウェア機能(例: SSE命令)を最大限に活用し、最高のパフォーマンスを引き出すことが可能になります。

技術的詳細

このコミットの主要な技術的変更は、memmove関数がメモリコピーのサイズに応じて異なる最適化パスを選択するロジックを調整し、新たに中間サイズのコピーに特化したSSEベースのルーチンを導入した点にあります。

以前の実装では、メモリコピーのサイズが256バイトを超えると、効率的なREP MOVSQ命令に切り替えていました。しかし、REP MOVSQは命令の実行開始にオーバーヘッドがあるため、257バイトから約2048バイト(2KB)の範囲では、このオーバーヘッドがコピーの利点を上回ることがありました。

このコミットでは、以下の変更が行われました。

  1. カットオーバーサイズの変更: REP MOVSQに切り替える閾値が、従来の「約1K(1024バイト)」から「約2K(2048バイト)」に変更されました。これは、2048バイト未満のコピーに対して、より高速なSSEベースのMOV命令を使用するための準備です。
  2. 新しいコピーパスの導入: move_256through2048という新しいアセンブリルーチンが導入されました。このルーチンは、256バイトを超えるが2048バイト以下の非重複メモリコピーを処理するために設計されています。
  3. SSE MOVOU命令の活用: move_256through2048ルーチン内では、一連のMOVOU命令が使用されます。具体的には、16個のXMMレジスタ(X0からX15)を使い、それぞれ16バイトずつ、合計256バイトのデータをソースからレジスタに一括でロードし、その後、これらのレジスタの内容をデスティネーションに一括でストアします。これにより、256バイトのブロックを非常に高速にコピーできます。
  4. ループ処理: 256バイトを超えるが2048バイト以下のコピーの場合、move_256through2048ルーチンは256バイトのブロックをコピーした後、残りのバイト数に応じてループし、次の256バイトブロックを処理します。これにより、中間サイズのコピー全体を効率的に処理します。
  5. 重複領域の考慮: コミットメッセージのコメントにもあるように、move_256through2048ルーチンは、ソースとデスティネーションのメモリ領域が重複しない場合、またはコピー方向が順方向(アドレスが増加する方向)の場合にのみ使用できます。これは、データをレジスタに完全にロードしてから書き出す小さなコピー(例: move_129through256)とは異なり、部分的にデータを処理するため、重複領域の扱いに注意が必要です。

この最適化により、特に512バイト、1024バイト、2048バイトといったサイズのメモリコピーにおいて、ナノ秒単位で大幅な時間短縮が実現され、スループット(MB/s)も向上しています。

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

変更はsrc/pkg/runtime/memmove_amd64.sファイルに集中しています。

  1. tailラベル付近のロジック変更:
    • REP命令のスタートアップコストに関するコメントが更新され、カットオーバーサイズが「約1K」から「約2K」に変更されました。
    • tailラベルの直後に、move_129through256やそれ以下のサイズが重複領域でも動作するのに対し、move_256through2048は非重複または順方向コピーでのみ使用可能であるという重要なコメントが追加されました。
  2. forwardラベル付近のロジック変更:
    • forwardパス(順方向コピー)において、コピーサイズBX$2048以下の場合に、新しく追加されたmove_256through2048ルーチンにジャンプする条件分岐(CMPQ BX, $2048JLS move_256through2048)が追加されました。これにより、中間サイズのコピーが新しい最適化パスにルーティングされます。
  3. move_256through2048ルーチンの追加:
    • ファイルの下部に、move_256through2048という新しいアセンブリルーチンが追加されました。
    • このルーチンは、まずコピーサイズBXから256を引きます(SUBQ $256, BX)。
    • 次に、MOVOU命令を16回連続して使用し、ソースアドレスSIから16バイトずつXMMレジスタ(X0からX15)にロードします。これにより、合計256バイトがレジスタに読み込まれます。
    • その後、同様にMOVOU命令を16回連続して使用し、XMMレジスタの内容をデスティネーションアドレスDIに16バイトずつストアします。
    • コピーサイズBXがまだ256バイト以上の場合(CMPQ BX, $256JGE move_256through2048)、SIDIを256バイト進めて(LEAQ 256(SI), SILEAQ 256(DI), DI)、ルーチン自身にジャンプして次の256バイトブロックを処理します。
    • 残りのサイズが256バイト未満になった場合、JMP tailで既存の小さなコピー処理ルーチンに制御を移します。

コアとなるコードの解説

新しく追加されたmove_256through2048ルーチンは、Goランタイムのmemmoveにおける中間サイズのメモリコピーのボトルネックを解消するために設計されています。

このルーチンは、256バイトのブロックを効率的にコピーするために、SSE命令のMOVOUを最大限に活用しています。

  1. 一括ロード: MOVOU (SI), X0からMOVOU 240(SI), X15までの一連の命令は、ソースポインタSIから始まる256バイトのデータを、16個のXMMレジスタ(X0-X15)に一括でロードします。各MOVOU命令は16バイトをロードするため、16 * 16 = 256バイトが一度にレジスタセットに読み込まれます。
  2. 一括ストア: ロードが完了した後、MOVOU X0, (DI)からMOVOU X15, 240(DI)までの一連の命令は、XMMレジスタにロードされた256バイトのデータを、デスティネーションポインタDIに一括でストアします。
  3. ループ処理:
    • SUBQ $256, BX: コピーすべき残りバイト数BXから、今回処理した256バイトを引きます。
    • LEAQ 256(SI), SILEAQ 256(DI), DI: ソースとデスティネーションのポインタをそれぞれ256バイト進めます。
    • CMPQ BX, $256JGE move_256through2048: もし残りバイト数BXがまだ256バイト以上であれば、move_256through2048ルーチンの先頭にジャンプし、次の256バイトブロックのコピーを繰り返します。
  4. 残りの処理: ループが終了し、残りバイト数BXが256バイト未満になった場合、JMP tail命令によって、既存のmemmoveのロジック(tailラベル)に制御が戻されます。tailラベル以下のコードは、残りの小さなバイト数を処理するための効率的なルーチン(例: 128バイト、64バイト、32バイト、16バイト、8バイト、1バイト単位のコピー)に分岐します。

この構造により、257バイトから2048バイトの範囲のメモリコピーは、REP MOVSQのオーバーヘッドを回避し、SSE命令による高速なブロックコピーと、残りのバイトに対する既存の最適化された処理を組み合わせることで、全体として大幅なパフォーマンス向上を実現しています。

関連リンク

参考にした情報源リンク