web-dev-qa-db-ja.com

なぜmemcpy()とmemmove()はポインタのインクリメントよりも速いのですか?

pSrcからpDestにNバイトをコピーしています。これは、単一のループで実行できます。

for (int i = 0; i < N; i++)
    *pDest++ = *pSrc++

なぜこれはmemcpyまたはmemmoveより遅いのですか?彼らはそれをスピードアップするためにどのようなトリックを使用していますか?

90
wanderer

Memcpyはバイトポインターの代わりにWordポインターを使用するため、memcpyの実装も [〜#〜] simd [〜#〜] 命令で記述されることが多く、一度に128ビットをシャッフルできます。

SIMD命令は、最大16バイト長のベクター内の各要素に対して同じ操作を実行できるアセンブリ命令です。これには、ロードおよびストア命令が含まれます。

115
onemasse

メモリコピールーチンは、次のようなポインタを介した単純なメモリコピーよりもはるかに複雑で高速です。

void simple_memory_copy(void* dst, void* src, unsigned int bytes)
{
  unsigned char* b_dst = (unsigned char*)dst;
  unsigned char* b_src = (unsigned char*)src;
  for (int i = 0; i < bytes; ++i)
    *b_dst++ = *b_src++;
}

改善

最初にできる改善点は、Wordの境界にポインターの1つを揃えることです(Wordではネイティブ整数サイズ、通常は32ビット/ 4バイトですが、新しいアーキテクチャでは64ビット/ 8バイトにすることができます)。Wordサイズの移動を使用します。/copy指示。これには、ポインターが整列するまで、バイトからバイトへのコピーを使用する必要があります。

void aligned_memory_copy(void* dst, void* src, unsigned int bytes)
{
  unsigned char* b_dst = (unsigned char*)dst;
  unsigned char* b_src = (unsigned char*)src;

  // Copy bytes to align source pointer
  while ((b_src & 0x3) != 0)
  {
    *b_dst++ = *b_src++;
    bytes--;
  }

  unsigned int* w_dst = (unsigned int*)b_dst;
  unsigned int* w_src = (unsigned int*)b_src;
  while (bytes >= 4)
  {
    *w_dst++ = *w_src++;
    bytes -= 4;
  }

  // Copy trailing bytes
  if (bytes > 0)
  {
    b_dst = (unsigned char*)w_dst;
    b_src = (unsigned char*)w_src;
    while (bytes > 0)
    {
      *b_dst++ = *b_src++;
      bytes--;
    }
  }
}

異なるアーキテクチャは、ソースポインターまたは宛先ポインターが適切に調整されているかどうかに基づいて、異なるパフォーマンスを発揮します。たとえば、XScaleプロセッサでは、ソースポインターではなく宛先ポインターを調整することでパフォーマンスが向上しました。

パフォーマンスをさらに向上させるために、いくつかのループ展開を行うことができます。これにより、より多くのプロセッサのレジスタにデータがロードされ、ロード/ストア命令がインターリーブされ、追加の命令(ループカウントなど)によってレイテンシが隠されます。ロード/ストア命令のレイテンシは大きく異なる可能性があるため、これがもたらす利点はプロセッサによってかなり異なります。

レイテンシの非表示とスループットを最大限に活用するには、ロードおよびストア命令を手動で配置する必要があるため、この段階でコードはC(またはC++)ではなくアセンブリで記述されます。

一般に、展開されたループの1回の反復で、データのキャッシュライン全体をコピーする必要があります。

これにより、プリフェッチが追加され、次の改善につながります。これらは、メモリの特定の部分をキャッシュにロードするようにプロセッサのキャッシュシステムに指示する特別な命令です。命令を発行してからキャッシュラインを埋めるまでに遅延があるため、命令は、データをコピーするときに使用できるように配置する必要があります。

これは、プリフェッチ命令を関数の開始時とメインコピーループ内に配置することを意味します。コピーループの途中でプリフェッチ命令を使用して、数回の反復時間でコピーされるデータをフェッチします。

思い出せませんが、送信元アドレスだけでなく宛先アドレスもプリフェッチすることも有益です。

要因

メモリのコピー速度に影響する主な要因は次のとおりです。

  • プロセッサ、キャッシュ、メインメモリ間の遅延。
  • プロセッサのキャッシュラインのサイズと構造。
  • プロセッサのメモリ移動/コピー命令(レイテンシ、スループット、レジスタサイズなど)。

したがって、効率的で高速なメモリ処理ルーチンを作成する場合は、作成するプロセッサとアーキテクチャについて多くのことを知る必要があります。言うまでもなく、組み込みプラットフォームで作成しているのでなければ、組み込みのメモリコピールーチンを使用する方がはるかに簡単です。

79
Daemin

memcpyは、コンピューターのアーキテクチャに応じて一度に複数バイトをコピーできます。最新のコンピューターのほとんどは、単一のプロセッサー命令で32ビット以上で動作します。

実装の一例 から:

 00026 *迅速なコピーのために、両方のポインター
 00027 *とワードが一直線に並ぶ一般的なケースを最適化し、代わりに一度にワードをコピーします
 00028 *一度に1バイト。それ以外の場合は、バイト単位でコピーします。
18
Mark Byers

以下の手法のいずれかを使用してmemcpy()を実装できます。一部はパフォーマンス向上のためにアーキテクチャーに依存し、それらはすべてコードよりもはるかに高速です。

  1. バイトではなく、32ビットワードなどの大きな単位を使用します。ここでアライメントを処理することもできます(または必要になる場合があります)。プラットフォームによっては、32ビットWordを奇数のメモリ位置に読み書きすることはできません。他のプラットフォームでは、パフォーマンスが大幅に低下します。これを修正するには、アドレスを4で割り切れる単位にする必要があります。64ビットCPUの場合は64ビットまで、または [〜#〜] simd [〜#〜] (単一命令、複数データ)命令( [〜#〜] mmx [〜#〜][〜#〜] sse [〜#〜] など)

  2. コンパイラーがCから最適化できない特殊なCPU命令を使用できます。たとえば、80386では、「rep」プレフィックス命令+「movsb」命令を使用して、カウントにNを入れることによって指定されたNバイトを移動できます。登録。優れたコンパイラーはあなたのためにこれを行いますが、優れたコンパイラーのないプラットフォームを使用しているかもしれません。この例は速度の悪いデモンストレーションになる傾向がありますが、アライメント+大きいユニット命令と組み合わせると、特定のCPUで他のほとんどすべてよりも速くなる可能性があります。

  3. ループの展開 -一部のCPUでは分岐が非常に高価になる可能性があるため、ループを展開すると分岐の数を減らすことができます。これは、SIMD命令と非常に大きなサイズのユニットと組み合わせるのにも適した手法です。

たとえば、 http://www.agner.org/optimize/#asmlib には、memcpy実装がありますが、ほとんど実装されていません(ごくわずかです)。ソースコードを読むと、上記の3つの手法すべてを実行する大量のインラインアセンブリコードになり、実行しているCPUに基づいてそれらの手法を選択します。

バッファ内のバイトを見つけるために、同様の最適化を行うこともできます。 strchr()および友人は、多くの場合、ハンドロールされた同等のものよりも速くなります。これは、 。NET および Java の場合に特に当てはまります。たとえば、.NETでは、上記の最適化手法を使用するため、組み込みのString.IndexOf()Boyer–Moore文字列検索 よりもはるかに高速です。

7
Danny Dulai

短い答え:

  • キャッシュフィル
  • 可能な場合、バイト単位ではなくワード単位の転送
  • SIMDマジック
5
moshbear

memcpyの実際の実装で実際に使用されているかどうかはわかりませんが、ここでは Duff's Device に言及する価値があると思います。

から ウィキペディア

send(to, from, count)
register short *to, *from;
register count;
{
        register n = (count + 7) / 8;
        switch(count % 8) {
        case 0:      do {     *to = *from++;
        case 7:              *to = *from++;
        case 6:              *to = *from++;
        case 5:              *to = *from++;
        case 4:              *to = *from++;
        case 3:              *to = *from++;
        case 2:              *to = *from++;
        case 1:              *to = *from++;
                } while(--n > 0);
        }
}

上記はmemcpyポインターではないことに注意してください。これはtoポインターを意図的にインクリメントしないためです。これは、わずかに異なる操作を実装します:メモリマップドレジスタへの書き込み。詳細については、Wikipediaの記事を参照してください。

4
NPE

他の人が言うように、memcpyは1バイトのチャンクより大きいコピーをします。 Wordサイズのチャンクでのコピーははるかに高速です。ただし、ほとんどの実装ではさらに一歩進んで、ループする前に複数のMOV(Word)命令を実行します。たとえば、ループあたり8ワードブロックをコピーする利点は、ループ自体のコストが高いことです。この手法は、条件分岐の数を8分の1に減らし、巨大ブロックのコピーを最適化します。

3
VoidStar

答えは素晴らしいですが、高速memcpyを自分で実装したい場合は、高速memcpyに関する興味深いブログ投稿Cでの高速memcpy があります。 =

void *memcpy(void* dest, const void* src, size_t count)
{
    char* dst8 = (char*)dest;
    char* src8 = (char*)src;

    if (count & 1) {
        dst8[0] = src8[0];
        dst8 += 1;
        src8 += 1;
    }

    count /= 2;
    while (count--) {
        dst8[0] = src8[0];
        dst8[1] = src8[1];

        dst8 += 2;
        src8 += 2;
    }
    return dest;
}

さらに、メモリアクセスを最適化することで改善できます。

2
deepmax

多くのライブラリルーチンと同様に、実行しているアーキテクチャ向けに最適化されています。他にも使用可能なさまざまな手法が投稿されています。

選択肢があれば、独自のロールを作成するのではなく、ライブラリルーチンを使用します。これは、DRYのバリエーションです。DRO(Do n't Repeat Others)と呼びます。また、ライブラリルーチンは、独自の実装よりも間違っている可能性は低いです。

メモリアクセスチェッカーが、Wordサイズの倍数ではないメモリまたは文字列バッファの範囲外読み取りについて文句を言うのを見てきました。これは、使用されている最適化の結果です。

1
BillThor