72

私はそれが本当に速いことを学びましたmemset(ptr, 0, nbytes)が、もっと速い方法はありますか (少なくとも x86 では)?

memset は を使用していると思いますがmov、メモリをゼロにするとき、ほとんどのコンパイラはxorより高速であるため、正しいですか? edit1: GregSが指摘したように、レジスタでのみ機能するのは間違っています。私が考えていたことは何でしょう?

また、私よりもアセンブラーに詳しい人に stdlib を見てもらうように依頼したところ、x86 では memset が 32 ビット幅のレジスターを十分に活用していないとのことでした。しかし、その時はとても疲れていたので、それを正しく理解できたかどうか自信がありません。

edit2 : この問題を再検討し、少しテストを行いました。これが私がテストしたものです:

    #include <stdio.h>
    #include <malloc.h>
    #include <string.h>
    #include <sys/time.h>

    #define TIME(body) do {                                                     \
        struct timeval t1, t2; double elapsed;                                  \
        gettimeofday(&t1, NULL);                                                \
        body                                                                    \
        gettimeofday(&t2, NULL);                                                \
        elapsed = (t2.tv_sec - t1.tv_sec) * 1000.0 + (t2.tv_usec - t1.tv_usec) / 1000.0; \
        printf("%s\n --- %f ---\n", #body, elapsed); } while(0)                 \


    #define SIZE 0x1000000

    void zero_1(void* buff, size_t size)
    {
        size_t i;
        char* foo = buff;
        for (i = 0; i < size; i++)
            foo[i] = 0;

    }

    /* I foolishly assume size_t has register width */
    void zero_sizet(void* buff, size_t size)
    {
        size_t i;
        char* bar;
        size_t* foo = buff;
        for (i = 0; i < size / sizeof(size_t); i++)
            foo[i] = 0;

        // fixes bug pointed out by tristopia
        bar = (char*)buff + size - size % sizeof(size_t);
        for (i = 0; i < size % sizeof(size_t); i++)
            bar[i] = 0;
    }

    int main()
    {
        char* buffer = malloc(SIZE);
        TIME(
            memset(buffer, 0, SIZE);
        );
        TIME(
            zero_1(buffer, SIZE);
        );
        TIME(
            zero_sizet(buffer, SIZE);
        );
        return 0;
    }

結果:

-O3 を除いて、zero_1 が最も遅いです。zero_sizet は、-O1、-O2、および -O3 でほぼ同等のパフォーマンスで最速です。memset は常に zero_sizet よりも低速でした。(-O3 の場合は 2 倍遅くなります)。興味深い点の 1 つは、-O3 で zero_1 が zero_sizet と同等に高速だったことです。ただし、逆アセンブルされた関数には、約 4 倍の命令がありました (ループの展開が原因だと思います)。また、zero_sizet をさらに最適化しようとしましたが、コンパイラは常に私よりも優れていましたが、ここで驚くことではありません。

今のところ memset が勝っていますが、以前の結果は CPU キャッシュによって歪められていました。(すべてのテストは Linux で実行されました) さらなるテストが必要です。次はアセンブラを試してみます:)

edit3:テスト コードのバグを修正しました。テスト結果は影響を受けません

edit4:逆アセンブルされた VS2010 C ランタイムを調べてmemsetいると、SSE に最適化されたゼロのルーチンがあることに気付きました。これを倒すのは難しいでしょう。

4

9 に答える 9

37

x86はかなり幅広いデバイスです。

完全に汎用的なx86ターゲットの場合、「rep movsd」を使用したアセンブリブロックは、一度に32ビットのメモリにゼロを吹き飛ばす可能性があります。この作業の大部分がDWORDに合わせられていることを確認してください。

mmxを使用するチップの場合、movqを使用するアセンブリループは一度に64ビットに達する可能性があります。

C / C ++コンパイラで、longlongまたは_m64へのポインタを使用した64ビット書き込みを使用できる場合があります。最高のパフォーマンスを得るには、ターゲットを8バイトに揃える必要があります。

sseを使用するチップの場合、movapsは高速ですが、アドレスが16バイトで整列されている場合に限り、整列されるまでmovsbを使用してから、movapsのループでクリアを完了します。

Win32には「ZeroMemory()」がありますが、それがmemsetのマクロなのか、それとも実際の「適切な」実装なのかを忘れています。

于 2010-09-07T00:25:16.800 に答える
30

memset通常、非常に高速な汎用設定/ゼロ設定コードになるように設計されています。さまざまなサイズと配置のすべてのケースを処理します。これは、作業を行うために使用できる命令の種類に影響します。使用しているシステム (および stdlib のベンダー) によっては、そのネイティブ プロパティが何であれ、そのアーキテクチャに固有のアセンブラーに基になる実装が含まれている場合があります。また、ゼロ化のケースを処理するための内部の特別なケースがある場合もあります (他の値を設定するのではなく)。

memsetとはいえ、非常に具体的でパフォーマンスが非常に重要なメモリのゼロ化を行う必要がある場合、自分でそれを行うことで特定の実装を打ち負かすことができる可能性は確かにあります。memsetおよび標準ライブラリ内のその仲間は、ワンアップマンシップ プログラミングの楽しいターゲットです。:)

于 2010-09-06T23:51:58.487 に答える
24

最近では、コンパイラがすべての作業を行う必要があります。少なくとも私が知っていることの中で、gcc はmemsetaway への呼び出しを最適化するのに非常に効率的です (ただし、アセンブラを確認することをお勧めします)。

memsetまた、必要がない場合は避けてください。

  • ヒープ メモリに calloc を使用する
  • ... = { 0 }スタック メモリに適切な初期化 ( ) を使用する

そして、本当に大きなチャンクのmmap場合は、それを使用してください。これは、システムから「無料で」ゼロの初期化メモリを取得するだけです。

于 2010-09-07T12:39:31.273 に答える
6

私が正しく覚えていれば(数年前から)、上級開発者の1人がPowerPCでbzero()を高速化する方法について話していました(仕様によると、電源投入時にほとんどすべてのメモリをゼロにする必要がありました)。(仮にあったとしても)x86にうまく変換されない可能性がありますが、調査する価値があるかもしれません。

アイデアは、データキャッシュラインをロードし、そのデータキャッシュラインをクリアしてから、クリアされたデータキャッシュラインをメモリに書き戻すことでした。

それが価値があることについては、私はそれが役立つことを願っています。

于 2010-09-07T00:21:59.937 に答える
6

特定のニーズがある場合や、コンパイラ/stdlib が不適切であることがわかっている場合を除き、memset を使用してください。これは汎用であり、一般的にまともなパフォーマンスを発揮するはずです。また、コンパイラは、 memset() を組み込みでサポートできるため、 memset() を最適化/インライン化するのが簡単になる場合があります。

たとえば、Visual C++ は多くの場合、ライブラリ関数の呼び出しと同じくらい小さいmemcpy/memset のインライン バージョンを生成するため、push/call/ret のオーバーヘッドが回避されます。また、サイズ パラメーターをコンパイル時に評価できる場合は、さらに最適化の可能性があります。

とはいえ、特定のニーズがある場合 (サイズが常にtiny *or* hugeである場合)、アセンブリ レベルにドロップダウンすることで速度を向上させることができます。たとえば、ライトスルー操作を使用して、L2 キャッシュを汚染することなくメモリの巨大なチャンクをゼロにします。

しかし、それはすべて依存します-通常のものについては、memset/memcpyに固執してください:)

于 2010-09-07T13:58:46.203 に答える
3

この素晴らしい便利なテストには、致命的な欠陥が 1 つあります。memset が最初の命令であるため、「メモリ オーバーヘッド」が発生し、非常に遅くなります。memset のタイミングを 2 番目に移動し、他の何かを 1 位に移動するか、単に memset のタイミングを 2 回変更すると、すべてのコンパイル スイッチで memset が最速になります!!!

于 2011-08-19T13:03:34.993 に答える
3

それは興味深い質問です。私が作成したこの実装は、VC++ 2012 で 32 ビット リリースをコンパイルした場合にわずかに高速化されました (ただし、ほとんど測定可能ではありません)。マルチスレッド環境で独自のクラスにこれを追加すると、おそらくパフォーマンスがさらに向上しmemset()ます。マルチスレッドのシナリオではボトルネックの問題が報告されているためです。

// MemsetSpeedTest.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <iostream>
#include "Windows.h"
#include <time.h>

#pragma comment(lib, "Winmm.lib") 
using namespace std;

/** a signed 64-bit integer value type */
#define _INT64 __int64

/** a signed 32-bit integer value type */
#define _INT32 __int32

/** a signed 16-bit integer value type */
#define _INT16 __int16

/** a signed 8-bit integer value type */
#define _INT8 __int8

/** an unsigned 64-bit integer value type */
#define _UINT64 unsigned _INT64

/** an unsigned 32-bit integer value type */
#define _UINT32 unsigned _INT32

/** an unsigned 16-bit integer value type */
#define _UINT16 unsigned _INT16

/** an unsigned 8-bit integer value type */
#define _UINT8 unsigned _INT8

/** maximum allo

wed value in an unsigned 64-bit integer value type */
    #define _UINT64_MAX 18446744073709551615ULL

#ifdef _WIN32

/** Use to init the clock */
#define TIMER_INIT LARGE_INTEGER frequency;LARGE_INTEGER t1, t2;double elapsedTime;QueryPerformanceFrequency(&frequency);

/** Use to start the performance timer */
#define TIMER_START QueryPerformanceCounter(&t1);

/** Use to stop the performance timer and output the result to the standard stream. Less verbose than \c TIMER_STOP_VERBOSE */
#define TIMER_STOP QueryPerformanceCounter(&t2);elapsedTime=(t2.QuadPart-t1.QuadPart)*1000.0/frequency.QuadPart;wcout<<elapsedTime<<L" ms."<<endl;
#else
/** Use to init the clock */
#define TIMER_INIT clock_t start;double diff;

/** Use to start the performance timer */
#define TIMER_START start=clock();

/** Use to stop the performance timer and output the result to the standard stream. Less verbose than \c TIMER_STOP_VERBOSE */
#define TIMER_STOP diff=(clock()-start)/(double)CLOCKS_PER_SEC;wcout<<fixed<<diff<<endl;
#endif    


void *MemSet(void *dest, _UINT8 c, size_t count)
{
    size_t blockIdx;
    size_t blocks = count >> 3;
    size_t bytesLeft = count - (blocks << 3);
    _UINT64 cUll = 
        c 
        | (((_UINT64)c) << 8 )
        | (((_UINT64)c) << 16 )
        | (((_UINT64)c) << 24 )
        | (((_UINT64)c) << 32 )
        | (((_UINT64)c) << 40 )
        | (((_UINT64)c) << 48 )
        | (((_UINT64)c) << 56 );

    _UINT64 *destPtr8 = (_UINT64*)dest;
    for (blockIdx = 0; blockIdx < blocks; blockIdx++) destPtr8[blockIdx] = cUll;

    if (!bytesLeft) return dest;

    blocks = bytesLeft >> 2;
    bytesLeft = bytesLeft - (blocks << 2);

    _UINT32 *destPtr4 = (_UINT32*)&destPtr8[blockIdx];
    for (blockIdx = 0; blockIdx < blocks; blockIdx++) destPtr4[blockIdx] = (_UINT32)cUll;

    if (!bytesLeft) return dest;

    blocks = bytesLeft >> 1;
    bytesLeft = bytesLeft - (blocks << 1);

    _UINT16 *destPtr2 = (_UINT16*)&destPtr4[blockIdx];
    for (blockIdx = 0; blockIdx < blocks; blockIdx++) destPtr2[blockIdx] = (_UINT16)cUll;

    if (!bytesLeft) return dest;

    _UINT8 *destPtr1 = (_UINT8*)&destPtr2[blockIdx];
    for (blockIdx = 0; blockIdx < bytesLeft; blockIdx++) destPtr1[blockIdx] = (_UINT8)cUll;

    return dest;
}

int _tmain(int argc, _TCHAR* argv[])
{
    TIMER_INIT

    const size_t n = 10000000;
    const _UINT64 m = _UINT64_MAX;
    const _UINT64 o = 1;
    char test[n];
    {
        cout << "memset()" << endl;
        TIMER_START;

        for (int i = 0; i < m ; i++)
            for (int j = 0; j < o ; j++)
                memset((void*)test, 0, n);  

        TIMER_STOP;
    }
    {
        cout << "MemSet() took:" << endl;
        TIMER_START;

        for (int i = 0; i < m ; i++)
            for (int j = 0; j < o ; j++)
                MemSet((void*)test, 0, n);

        TIMER_STOP;
    }

    cout << "Done" << endl;
    int wait;
    cin >> wait;
    return 0;
}

32 ビット システムのリリース コンパイル時の出力は次のとおりです。

memset() took:
5.569000
MemSet() took:
5.544000
Done

64 ビット システムのリリース コンパイル時の出力は次のとおりです。

memset() took:
2.781000
MemSet() took:
2.765000
Done

memset()ここでは、最も一般的な実装であると私が考えるソース コード Berkley'sを見つけることができます。

于 2013-03-08T10:09:17.533 に答える
3

memset 関数は、速度を犠牲にしても柔軟でシンプルになるように設計されています。多くの実装では、指定された値を指定されたバイト数で一度に 1 バイトずつコピーする単純な while ループです。より高速な memset (または memcpy、memmove など) が必要な場合は、ほとんどの場合、自分でコーディングすることができます。

最も簡単なカスタマイズは、宛先アドレスが 32 ビットまたは 64 ビット (チップのアーキテクチャに一致するもの) になるまでシングルバイトの「設定」操作を実行し、一度に完全な CPU レジスタのコピーを開始することです。範囲がアラインされたアドレスで終了しない場合は、最後にシングルバイトの「設定」操作をいくつか実行する必要がある場合があります。

特定の CPU によっては、ストリーミング SIMD 命令が役立つ場合もあります。これらは通常、アラインされたアドレスでより適切に機能するため、アラインされたアドレスを使用する上記の手法はここでも役立ちます。

メモリの大きなセクションをゼロにするために、範囲をセクションに分割し、各セクションを並行して処理することで、速度が向上する場合もあります (セクションの数は、コア数またはハードウェア スレッド数と同じです)。

最も重要なことは、実際に試してみない限り、これが役立つかどうかを判断する方法がないということです。少なくとも、各ケースでコンパイラが出力するものを確認してください。他のコンパイラが標準の「memset」に対して出力するものも参照してください(それらの実装は、コンパイラの実装よりも効率的かもしれません)。

于 2010-09-07T13:17:58.683 に答える