11

以下のような状況で memset() を利用することで、効率の面で利点があるかどうかについて興味がありました。

次のバッファ宣言を考えると...

struct More_Buffer_Info
{
    unsigned char a[10];
    unsigned char b[10];
    unsigned char c[10];
};

struct My_Buffer_Type
{
    struct More_Buffer_Info buffer_info[100];
};

struct My_Buffer_Type my_buffer[5];

unsigned char *p;
p = (unsigned char *)my_buffer;

コード行が少ないことに加えて、これを使用する利点はありますか:

memset((void *)p, 0, sizeof(my_buffer));

これについて:

for (i = 0; i < sizeof(my_buffer); i++)
{
    *p++ = 0;
}
4

6 に答える 6

28

memset()これは と の両方に適用されますmemcpy()

  1. 少ないコード:既に述べたように、コードの行数が少なくて済みます。
  2. より読みやすい:通常、短いほど読みやすくなります。(memset()そのループよりも読みやすいです)
  3. より高速になる可能性があります。より積極的なコンパイラの最適化が可能になる場合があります。(だから早いかも)
  4. ミスアライメント:場合によっては、ミスアライメント アクセスをサポートしていないプロセッサでミスアライメント データを処理しているmemset()場合memcpy()に、これが唯一のクリーンなソリューションである可能性があります。

3番目のポイントを拡張すると、memset()SIMDなどを使用してコンパイラーによって大幅に最適化できます。代わりにループを記述する場合、コンパイラは、最適化を試みる前に、最初にループの動作を「把握」する必要があります。

ここでの基本的な考え方はmemset()、同様のライブラリ関数が、ある意味でコンパイラに意図を「伝える」ということです。


コメントで @Oli が述べたように、いくつかの欠点があります。ここでそれらを拡張します。

  1. memset()それが実際にあなたが望むことをすることを確認する必要があります。標準は、さまざまなデータ型のゼロがメモリ内で必ずしもゼロであるとは言っていません。
  2. ゼロ以外のデータの場合、memset()内容は 1 バイトのみに制限されます。したがって、s のmemset()配列をintゼロ以外 (0x01010101または何か...) に設定する場合は使用できません。
  3. まれではありますが、独自のループでコンパイラのパフォーマンスを実際に上回る可能性があるいくつかのまれなケースがあります。

※私の経験から一例を挙げます。

memset()memcpy()は通常、コンパイラによる特別な処理を伴うコンパイラ組み込み関数ですが、依然として一般的な関数です。彼らは、データの配置を含むデータ型について何も言いません。

そのため、(まれではありますが) いくつかのケースでは、コンパイラはメモリ領域のアライメントを判断できず、ミスアライメントを処理するために追加のコードを生成する必要があります。一方、あなたがプログラマーである場合、アライメントが 100% 確実であれば、ループを使用する方が実際には高速になる可能性があります。

一般的な例は、SSE/AVX 組み込み関数を使用する場合です。(16/32 バイトでアラインされたfloats の配列をコピーするなど) コンパイラが 16/32 バイト アラインメントを判断できない場合、アラインされていないロード/ストアおよび/または処理コードを使用する必要があります。SSE/AVX に合わせたロード/ストア組み込み関数を使用して単純にループを作成する場合は、おそらくより適切に実行できます。

float *ptrA = ...  //  some unknown source, guaranteed to be 32-byte aligned
float *ptrB = ...  //  some unknown source, guaranteed to be 32-byte aligned
int length = ...   //  some unknown source, guaranteed to be multiple of 8

//  memcopy() - Compiler can't read comments. It doesn't know the data is 32-byte
//  aligned. So it may generate unnecessary misalignment handling code.
memcpy(ptrA, ptrB, length * sizeof(float));

//  This loop could potentially be faster because it "uses" the fact that
//  the pointers are aligned. The compiler can also further optimize this.
for (int c = 0; c < length; c += 8){
    _mm256_store_ps(ptrA + c, _mm256_load_ps(ptrB + c));
}
于 2011-12-16T00:54:06.537 に答える
8

コンパイラとライブラリの品質に依存します。ほとんどの場合、memset の方が優れています。

memset の利点は、多くのプラットフォームで実際にはコンパイラ組み込みであることです。つまり、コンパイラは大量のメモリを特定の値に設定する意図を「理解」し、より良いコードを生成できる可能性があります。

特に、x86 での SSE、PowerPC での AltiVec、ARM での NEON など、大きなメモリ領域を設定するために特定のハードウェア操作を使用することを意味する可能性があります。これにより、パフォーマンスが大幅に向上する可能性があります。

一方、for ループを使用すると、「このアドレスをレジスタにロードする。レジスタに数値を書き込む。アドレスに 1 を加算する。数値を書き込む」など、より具体的な処理を行うようコンパイラに指示することになります。の上。理論的には、完全にインテリジェントなコンパイラは、このループが何であるかを認識し、とにかくそれを memset に変換します。しかし、これを行う実際のコンパイラに遭遇したことはありません。

したがって、 memset は、コンパイラがサポートする特定のプラットフォームとハードウェアに対して、メモリの領域全体を設定するための最良かつ最速の方法であるために、賢明な人々によって書かれたものであるという前提があります。これは、常にではありませんが多くの場合、真実です。

于 2011-12-16T00:56:24.163 に答える
5

これを覚えておいてください

for (i = 0; i < sizeof(my_buffer); i++)
{
    p[i] = 0;
}

よりも速くすることもできます

for (i = 0; i < sizeof(my_buffer); i++)
{
    *p++ = 0;
}

すでに回答されているように、コンパイラには memset() memcpy() およびその他の文字列関数用に手動で最適化されたルーチンが含まれていることがよくあります。そして、私たちはかなり速く話しています。コードの量、命令の数、コンパイラからの高速memcpy または memset は、通常、提案したループ ソリューションよりもはるかに大きくなります。コード行が少なく、命令が少ないからといって、高速になるわけではありません。

とにかく、私のメッセージは両方を試すことです。コードを逆アセンブルし、違いを見て理解しようとし、スタック オーバーフローで質問します。次に、タイマーを使用して2つのソリューションの時間を計り、memcpy関数を何千回または何十万回も呼び出し、全体の時間を計ります(タイミングのエラーを排除するため)。7 項目または 5 項目などの短いコピーと、memset あたり数百バイトのような大きなコピーを作成し、その間にいくつかの素数を試してください。一部のシステムの一部のプロセッサでは、ループは 3 や 5 などのいくつかの項目で高速になりますが、速度は遅くなります。

ここで、パフォーマンスに関するヒントを 1 つ挙げます。お使いのコンピュータの DDR メモリは 64 ビット幅である可能性が高く、一度に 64 ビットを書き込む必要があります。おそらく ecc があり、それらのビット全体を計算して一度に 72 ビットを書き込む必要があります。必ずしも正確な数ではありませんが、ここでの考えに従ってください。RAMへのシングルバイト書き込み命令を実行する場合、ハードウェアは2つのことのいずれかを実行する必要があります。途中にキャッシュがない場合、メモリシステムは64ビット読み取りを実行し、1バイトを変更し、次にそれを書き戻します。ある種のハードウェア最適化がなければ、その 1 つのドラム行内に 8 バイトを書き込むことは 16 メモリ サイクルであり、ドラムは非常に低速です。1333MHz の数値にだまされてはいけません。

キャッシュがある場合、最初のバイト書き込みにはドラムからのキャッシュライン読み取りが必要になります。これは、これらの64ビット読み取りの1つまたは複数であり、次の7または15またはその他のバイト書き込みはおそらく非常に高速になります。それらはキャッシュにのみ移動し、ddr には移動しません。最終的にそのキャッシュ ラインは、これらの 64 ビットまたは任意の ddr ロケーションの 1 つまたは 2 つまたは 4 つなどの遅いドラムに送信されます。したがって、書き込みのみを行っている場合でも、そのRAMをすべて読み取ってから書き込む必要があるため、必要なサイクル数が2倍になります。可能であれば、一部のプロセッサとメモリ システムでは、memset または memcpy の書き込み部分は、キャッシュ ライン全体または ddr ロケーション全体を含む単一の命令にすることができ、読み取りは不要で、即座に速度が 2 倍になります。これはすべての最適化がどのように機能するかではありませんが、うまくいけば、問題についてどのように考えるかについてのアイデアが得られます. プログラムがキャッシュ ラインでキャッシュに取り込まれることで、実行される命令の数を 2 倍または 3 倍にすることができます。その見返りとして、DDR サイクル数を半分または 4 分の 1 以上削減し、全体的に勝つことができます。

少なくとも、コンパイラの memset および memcpy ルーチンは、開始アドレスが奇数の場合はバイト操作を実行し、32 ビットで整列されていない場合は 16 ビットを実行します。次に、64 で整列されていない場合は 32 ビットで、その命令セット/システムの最適な転送サイズに達するまで続きます。アームでは、128 ビットを目指す傾向があります。したがって、フロント エンドでの最悪のケースは、1 バイト、1 ハーフワード、数ワード、そしてメイン セットまたはコピー ループに入るということです。ARM 128 ビット転送の場合、命令ごとに 128 ビットが書き込まれます。次に、バックエンドで同じ取引が整列されていない場合、数ワード、1 ハーフ ワード、1 バイトの最悪のケースです。また、ライブラリが次のようなことを行うこともわかります。バイト数が X よりも小さい場合、X は 13 程度の小さな数である場合、あなたのようなループに入ります。そのループをサポートするための命令とクロックサイクルの数がより小さく/高速であるため、いくつかのバイトをコピーするだけです。ARM およびおそらく mips やその他の優れたプロセッサの gcc ソース コードを逆アセンブルまたは検索して、私が話していることを確認してください。

于 2011-12-16T02:09:39.360 に答える
4

2 つの利点:

  1. のバージョンのmemset方が読みやすいです。これは、コード行数が少ないことに関連していますが、同じではありません。バージョンが何をするかを理解するのに、あまり考えなくてもよい。memset

    memset(my_buffer, 0, sizeof(my_buffer));
    

    indirectionpと不必要な to へのキャストの代わりに(注: C++ ではなく C で実際void *にコーディングしている場合にのみ不要- 違いがはっきりしない人もいます)。

  2. memset一度に 4 バイトまたは 8 バイトを書き込める可能性が高く、特別なキャッシュ ヒント命令を利用できる可能性があります。したがって、バイト単位のループよりも高速になる可能性があります。(注: 一部のコンパイラは、一括消去ループを認識し、メモリへのより広い書き込みまたは への呼び出しのいずれかを代用するのに十分賢いmemsetです。走行距離は異なる場合があります。サイクルを削減しようとする前に、常にパフォーマンスを測定してください。)

于 2011-12-16T00:57:16.757 に答える
1

変数pは初期化ループにのみ必要です。memset のコードは単純にする必要があります

memset( my_buffer, 0, sizeof(my_buffer));

これは、より単純でエラーが発生しにくい方法です。パラメーターの要点は、void*それが任意のポインター型を受け入れること、明示的なキャストが不要であること、および異なる型のポインターへの代入が無意味であることです。

したがってmemset()、この場合に使用する利点の 1 つは、不要な中間変数を避けることです。

もう 1 つの利点は、特定のプラットフォームの memset() がターゲット プラットフォーム用に最適化される可能性が高いことですが、ループの効率はコンパイラとコンパイラの設定に依存します。

于 2011-12-16T17:31:44.757 に答える
1

memset はコードを記述する標準的な方法を提供し、特定のプラットフォーム/コンパイラ ライブラリが最も効率的なメカニズムを決定できるようにします。データ サイズに基づいて、たとえば 32 ビットまたは 64 ビットのストアを可能な限り行う場合があります。

于 2011-12-16T00:55:41.710 に答える