13

次のコードのパフォーマンスについてサポートが必要です。これは、動的に割り当てられた任意のサイズの2つの配列に対してmemcpyを実行します。

int main()
{
  double *a, *b;
  unsigned n = 10000000, i;
  a = malloc(n*sizeof(double));
  b = malloc(n*sizeof(double));
  for(i=0; i<n; i++) {
    a[i] = 1.0;
    /* b[i] = 0.0; */
  }

  tic();
  bzero(b, n*sizeof(double));
  toc("bzero1");

  tic();
  bzero(b, n*sizeof(double));
  toc("bzero2");

  tic();
  memcpy(b, a, n*sizeof(double));
  toc("memcpy");
}

tic/tocは実行時間を測定します。

私のコンピューターでは、memcpy(Linux、gccバージョン4.4.6)に0.035秒かかります。ここで、宛先配列bを初期化する行のコメントを解除すると、コードは3倍速くなります(!)-0.011秒。

memcpyの代わりにループを使用した場合にも同様の動作が見られます。使用する前にメモリを「初期化」するだけで十分なので、通常はこれを気にしません。ただし、単純なメモリコピーを実行し、可能な限り高速に実行する必要があります。データを初期化するには、たとえばメモリに0を書き込む必要がありますが、これは不要で時間がかかります。そして、使用可能なすべてのメモリ帯域幅でメモリコピーを実行したいと思います。

この問題の解決策はありますか?それとも、Linuxが動的メモリを処理する方法(ある種の怠惰なページ割り当て?)に接続されており、回避できませんか?他のシステムではどうですか?

編集: gcc4.6でも同じ結果が得られます。-O3を使用してコンパイルしました。

編集: コメントありがとうございます。メモリマッピングには時間がかかることを理解しています。実際のメモリアクセスよりもはるかに長い時間がかかることを受け入れるのに苦労していると思います。コードが変更され、後続の2つのbzero呼び出しを使用した配列bの初期化のベンチマークが含まれるようになりました。タイミングが表示されます

bzero1 0.273981
bzero2 0.056803
memcpy 0.117934

明らかに、最初のbzero呼び出しは、ゼロをメモリにストリーミングするだけではありませ。つまり、メモリマッピングとメモリゼロ化です。一方、2番目のbzero呼び出しは、memcpyを実行するのに必要な時間の半分を要します。これは、正確に予想どおりです。書き込みのみの時間と読み取りおよび書き込み時間です。OSのセキュリティ上の理由から、2番目のbzero呼び出しのオーバーヘッドが存在する必要があることを理解しています。残りはどうですか?どういうわけかそれを減らすことはできませんか?例えば、より大きなメモリページを使用しますか?異なるカーネル設定?

私はこれをUbuntu喘鳴で実行していることに言及する必要があります。

4

3 に答える 3

10

それはおそらく怠惰なページ割り当てであり、Linuxは最初のアクセス時にのみページをマッピングします。Linuxの新しいブロックのIIRC各ページは、空白ページのコピーオンライトであり、割り当ては新しいブロックを要求するのに十分な大きさです。

これを回避したい場合は、4k間隔で1バイトまたはワードを書き込むことができます。これにより、各ページ全体を書き込むよりもわずかに速く仮想アドレスがRAMにマップされる可能性があります。

ただし、(レイジーメモリマッピングを強制的に実行するための最も効率的な回避策)プラス(コピー)が、の初期化なしの(コピー)よりも大幅に高速になるとは思いませんb。したがって、操作全体ではなく、コピーだけのパフォーマンスを気にする特別な理由がない限り、それはかなり無駄です。それは「今すぐ支払うか後で支払う」であり、Linuxは後で支払い、あなたは後で時間を測定しているだけです。

于 2012-09-03T16:23:40.260 に答える
6

確かに、初期化とコピーの速度をコピーだけの速度と比較している場合は、初期化を時間指定セクションに含める必要があります。私には、実際にこれを比較する必要があるように見えます。

// Version 1
for(i=0; i<n; i++)
    a[i] = 1.0;

tic();

memcpy(b, a, n*sizeof(double));

toc();

これに:

// Version 2
for(i=0; i<n; i++)
    a[i] = 1.0;

tic();

for(i=0; i<n; i++)
    b[i] = 0.0;
memcpy(b, a, n*sizeof(double));

toc();

これにより、3倍の速度向上が大幅に低下すると思います。

編集: Steve Jessopが提案したように、ページごとに1つのエントリのみに触れるという3番目の戦略をテストすることもできます。

// Version 3
for(i=0; i<n; i++)
    a[i] = 1.0;

tic();

for(i=0; i<n; i+=DOUBLES_PER_PAGE)
    b[i] = 0.0;
memcpy(b, a, n*sizeof(double));

toc();
于 2012-09-03T16:30:54.010 に答える
5

最初のbzeroは、(1)レイジーページの割り当てと(2)カーネルによるレイジーページのゼロ初期化のために、より長く実行されます。セキュリティ上の理由から2番目の理由は避けられませんが、遅延ページの割り当ては、より大きな(「巨大な」)ページを使用することで最適化される場合があります。

Linuxで巨大なページを使用するには少なくとも2つの方法があります。難しい方法はhugetlbfsです。簡単な方法は、透明な巨大なページです。

システム上のプロセスのリストを検索khugepagedします。mallocそのようなプロセスが存在する場合、透明な巨大なページがサポートされています。これに変更すると、アプリケーションでそれらを使用できます。

posix_memalign((void **)&b, 2*1024*1024, n*sizeof(double));
madvise((void *)b, n*sizeof(double), MADV_HUGEPAGE);
于 2012-09-04T10:26:51.743 に答える