私はCでいくつかの配列の例をいじっていました。メモリの概念、配置、およびキャッシュについてもっと理解したいと思います。特にヒープ上の大きな配列では。時々私は大きな画像(非常に大きな画像)を扱うので、それが私にとって重要ですが、私が今求めていることには関係ありません。
次の例を書きました。
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
#include <GLFW/glfw3.h>
#define ARRAY_NUM 1000000 * 1000 // GIG
int main(int argc, char *argv[]) {
if(!glfwInit()) {
exit(EXIT_FAILURE);
}
uint64_t *array = malloc(sizeof(uint64_t) * ARRAY_NUM);
double time_start = glfwGetTime();
for(uint64_t i = 0; i < ARRAY_NUM; ++i) {
array[i] = 0xff;
}
double time_end = glfwGetTime();
free(array);
glfwTerminate();
double performance = ((ARRAY_NUM * sizeof(uint64_t))/1000000) / (time_end - time_start);
printf("Done in %f\n", (time_end - time_start));
printf("Performance: %f MB/s\n", performance);
exit(EXIT_SUCCESS);
}
1ギグが割り当てられていると、次のようになります。
Done in 36.126213
Performance: 221.445854 MB/s
私が得る300メガ:
Done in 7.564931
Performance: 317.253391 MB/s
200メガを取得します:
Done in 1.279391
Performance: 1250.594728 MB/s
私が得る100メガ:
Done in 0.355763
Performance: 2248.685313 MB/s
私が得る10メガ:
Done in 0.036575
Performance: 2187.315761 MB/s
そして私が得る1メガ:
Done in 0.004050
Performance: 1975.160383 MB/s
もちろん、タイミングは実行間で最大5〜10%異なります。
さて、ここで私が不思議に思っているのは、ヒープ上で配列をトラバースするために得ることができる一貫した最大スループットを得るために何ができるか、そして何をすべきかです-特に大きなものです。何かにより、最高の2+ GB/sの数でさえ、ここではトップの数ではないことがわかります。興味深いのは、1メガの結果と10と100の比較です。キャッシュは、特定の時間後にのみ高温になるようです。
私はC99(gcc5)を使用していますが、私の理解では、mallocとuint64_tを使用すると、メモリを整列させる必要があります。そうでなければ、たぶんそれへの確実な方法はありますか?もちろん、それが原因である場合はもちろんです。たぶん私はキャッシュラインを間違ってヒットしている、またはOSのVM不要なページをヒットしている?大規模な配列内の線形アクセスの最適化をどこから始めればよいでしょうか?
詳細:
-pedantic -std=c99 -Wall -Werror -Wextra -Wno-unused -O9
(そうです、私は3つ使用しますが、それ以外は3つ使用しますが、あなたは決して知りません)。使いやすいです。私はそれなしでそれをすることができた、それは問題ではない。編集:
エリックが私に移植したいくつかの考えの後、私は別のテストを行いました。プリフェッチと、できればキャッシュにも関連するものをいくつか示します。
#include <stdio.h>
#include <stdlib.h>
#include <string.h> // memcpy
#include <inttypes.h>
#include <GLFW/glfw3.h>
#define ARRAY_NUM 1000000 * 125 // GIG
int main(int argc, char *argv[]) {
double performance = 0.0;
if(!glfwInit()) {
exit(EXIT_FAILURE);
}
uint64_t *array = malloc(sizeof(uint64_t) * ARRAY_NUM);
uint64_t *array_copy = malloc(sizeof(uint64_t) * ARRAY_NUM);
double time_start = glfwGetTime();
for(uint64_t i = 0; i < ARRAY_NUM; ++i) {
array[i] = 0xff;
}
double time_end = glfwGetTime();
performance = ((ARRAY_NUM * sizeof(uint64_t))/1000000) / (time_end - time_start);
printf("Init done in %f - size of array: %lu MBs (x2)\n", (time_end - time_start), (ARRAY_NUM*sizeof(uint64_t)/1000000));
printf("Performance: %f MB/s\n\n", performance);
performance = 0;
time_start = glfwGetTime();
for(uint64_t i = 0; i < ARRAY_NUM; ++i) {
array_copy[i] = array[i];
}
time_end = glfwGetTime();
performance = ((ARRAY_NUM * sizeof(uint64_t))/1000000) / (time_end - time_start);
printf("Copying (linear) done in %f\n", (time_end - time_start));
printf("Performance: %f MB/s\n\n", performance);
performance = 0;
time_start = glfwGetTime();
for(uint64_t i = 0; i < ARRAY_NUM; i=i+8) {
array_copy[i] = array[i];
if(i < (ARRAY_NUM-8)) {
array_copy[i+1] = array[i+1];
array_copy[i+2] = array[i+2];
array_copy[i+3] = array[i+3];
array_copy[i+4] = array[i+4];
array_copy[i+5] = array[i+5];
array_copy[i+6] = array[i+6];
array_copy[i+7] = array[i+7];
}
}
time_end = glfwGetTime();
performance = ((ARRAY_NUM * sizeof(uint64_t))/1000000) / (time_end - time_start);
printf("Copying (stride 8) done in %f\n", (time_end - time_start));
printf("Performance: %f MB/s\n\n", performance);
const int imax = 100;
double performance_average = 0.0;
for(int j = 0; j < imax; ++j) {
uint64_t tmp = 0;
performance = 0;
time_start = glfwGetTime();
for(uint64_t i = 0; i < ARRAY_NUM; i=i+8) {
tmp = array[i];
if(i < (ARRAY_NUM-8)) {
tmp = array[i+1];
tmp = array[i+2];
tmp = array[i+3];
tmp = array[i+4];
tmp = array[i+5];
tmp = array[i+6];
tmp = array[i+7];
}
}
time_end = glfwGetTime();
performance = ((ARRAY_NUM * sizeof(uint64_t))/1000000) / (time_end - time_start);
performance_average += performance;
printf("[%d/%d] Performance stride 8: %f MB/s\r", j+1, imax, performance);
fflush(stdout);
}
performance_average = performance_average / imax;
printf("\nAverage: %f MB/s\n\n", performance_average);
performance_average = 0.0;
for(int j = 0; j < imax; ++j) {
uint64_t tmp = 0;
performance = 0;
time_start = glfwGetTime();
for(uint64_t i = 0; i < ARRAY_NUM; ++i) {
tmp = array[i];
}
time_end = glfwGetTime();
performance = ((ARRAY_NUM * sizeof(uint64_t))/1000000) / (time_end - time_start);
performance_average += performance;
printf("[%d/%d] Performance dumb: %f MB/s\r", j+1, imax, performance);
fflush(stdout);
}
performance_average = performance_average / imax;
printf("\nAverage: %f MB/s\n\n", performance_average);
performance = 0;
time_start = glfwGetTime();
memcpy(array_copy, array, ARRAY_NUM*sizeof(uint64_t));
time_end = glfwGetTime();
performance = ((ARRAY_NUM * sizeof(uint64_t))/1000000) / (time_end - time_start);
printf("Copying (memcpy) done in %f\n", (time_end - time_start));
printf("Performance: %f MB/s\n", performance);
free(array);
free(array_copy);
glfwTerminate();
exit(EXIT_SUCCESS);
}
Glfwのタイマーを使用したくない場合は、タイマーをタイマーに置き換えてください。 -pedantic -std=c99 -Wall -Werror -Wextra -Wno-unused -O0
を使用してコンパイルしましたが、-fprefetch-loop-arrays
は効果がないようなので、省略しました。
これで、プリフェッチの効果を確認できます。次に出力例を示します。
Init done in 0.784799 - size of array: 1000 MBs (x2)
Performance: 1274.211087 MB/s
Copying (linear) done in 2.086404
Performance: 479.293545 MB/s
Copying (stride 8) done in 0.313592
Performance: 3188.856625 MB/s
[100/100] Performance stride 8: 6458.897164 MB/s
Average: 6393.163000 MB/s
[100/100] Performance dumb: 2597.816831 MB/s
Average: 2530.225830 MB/s
Copying (memcpy) done in 0.202581
Performance: 4936.303056 MB/s
ループごとに8つの値(現時点では任意の値)を読み取ると、帯域幅が2倍以上になることがわかります。驚いたのは、コピーが6倍以上増加したことです。参照としてmemcpyを含めましたが、さらに高速です。面白いプレー。何を探すべきか、そして配列および場合によっては重要なペイロード(この場合はuint64_tを選択しました)の最適な読み取り/書き込みにどのようにアプローチするかについて、もっと知りたいと思います。
edit2:
ストライドが40(これ以上、それ以下でもない-320バイト)の場合、約7800 MB /秒の読み取りを取得しました。これは最大値の10600(1333MhZ DDR3)に近い値です。
テストは基本的にメモリを介して直接ループするため、キャッシュはおおよそ次のように機能します。
キャッシュにないメモリに触れると、メインメモリに到達するまで、最も近いキャッシュL1からキャッシュラインロードがトリガーされ、L2およびL3キャッシュを介してカスケードされます。メインメモリーからL3キャッシュラインサイズのデータチャンクがフェッチされ、L3がいっぱいになります。次に、L2を埋めるためにL3からのL2キャッシュラインサイズのデータのチャンクがフェッチされ、L2からL1が埋められます。
これでプログラムは、ミスを引き起こしたメモリ位置の処理を再開できます。ループの次の数回の反復では、L1キャッシュからのデータを処理します。これは、キャッシュラインのサイズが、各反復で処理する8バイトより大きいためです。少数の反復の後、キャッシュラインの終わりに達し、これにより別のキャッシュミスがトリガーされます。 L2またはL3キャッシュのラインサイズがL1キャッシュのラインサイズと同じかそれよりも大きいかどうかに応じて、再びメインメモリに到達する場合とそうでない場合があります。
1回の反復で要求されるより多くのデータがハードウェアによってフェッチされるため、ループは通常、キャッシュラインサイズによるキャッシュの恩恵を受けるだけです。
一部のプロセッサ(Itanium)には、ソフトウェアがハードウェアにキャッシュラインのロードを事前に実行するように通知するメカニズムがあります。これにより、メインメモリの負荷の一部をカバーできます。
ループはそれが触れるメモリを再訪問することはないので、キャッシュからそれ以上の利益を得ることはありません。さらに実験したい場合は、次のことを検討してください。
一定のステップ量でステップする外部ループと、ステップ範囲のデータを繰り返し使用するいくつかの内部ループを作成する:したがって、固定ステップ量と最初の内部が単純な繰り返し(例:100回の反復)で反復する、3重にネストされたループ)、ステッピング量に従って配列の範囲にアクセスする最も内側のループ。基本的に、配列内の各メモリ位置に100回アクセスしますが、実行ごとにステッピングサイズを変えることにより、これらの位置にアクセスする方法の順序とグループを変更します。これにより、キャッシュが適切に機能している場合と機能していない場合との間に大幅なばらつき(桁)が示されます。
小さなステップサイズでキャッシュは適切に機能し、小さな範囲ごとに100回の反復を行うと、キャッシュの再利用によって大きなメリットが得られます。大きなステップサイズでは、パフォーマンスは劇的に低下し、直線で観察しているものに近くなります。記憶を通して。
(もちろん、これは、コンパイラーがコードをあまり乱用しないことを前提としています。場合によっては、過度に単純なベンチマークが最適化されることがあります...)