🐸
DDR4の書込みレート計測
0. 概要
C++er向けの高速化ネタで、DDR4の転送レート(書込み)を計測する話です。
最適化が至上命題という、スピード狂向けの記事です。
メモリボトルネックでお困りの方にも有用かもと思います。
結論だけ書くと下記です。
- memsetを使うと、最大転送レートの約58%しかでない
- _mm256_stream_si256を使うと、最大転送レートのほぼ100%でる
マルチスレッドかつ、大容量のメモリ書込み処理でmemsetを使ってる人は、_mm256_stream_si256を使うと処理速度が改善するかもしれません
1. DDR4の最大転送レート
最大転送レートは、下記のように求めます。
転送レート[Byte/sec] = 動作周波数[Hz] * 8[byte] * チャンネル数
例) DDR4-2400 4chの最大転送レート
76800[MB/sec] = 2400[MHz] * 8[Byte] * 4ch
DDR4-XXXXの、XXXXが動作周波数です。
8byteはDDR4のアクセス単位で固定値です。
チャンネル数は、使ってるCPUによって異なります(コンシューマ向けは2か4が多いと思います)。
2. 計測プログラム
計測プログラムの例を示します。
#include <cstdio>
#include <cstdint>
#include <cstring>
#include <numeric>
#include <vector>
#include <thread>
#include <intrin.h>
using namespace std;
/*!
* \note 以下の条件に特化してるので注意
* - 引数のbufは、32byteにアライメント
* - 引数のsizeは、128byteの倍数
*/
void *memset_nt(void *buf, int val, size_t size)
{
const size_t lenDiv32 = size >> 5;
const char v = static_cast<char>(val);
__m256i *buf256;
__m256i ymm;
ymm = _mm256_set1_epi8(v);
buf256 = reinterpret_cast<__m256i *>(buf);
for (size_t i = 0; i < lenDiv32; i += 2)
{
_mm256_stream_si256(&buf256[i + 0], ymm);
_mm256_stream_si256(&buf256[i + 1], ymm);
}
return buf;
}
int main(void)
{
using Memset = void* (*)(void*, int, size_t);
constexpr Memset MEME_SET[] =
{
memset,
memset_nt,
};
constexpr size_t WRITE_SIZE[] =
{
16ULL * 1024 * 1024, // 16MB
32ULL * 1024 * 1024, // 32MB
64ULL * 1024 * 1024, // 64MB
128ULL * 1024 * 1024, // 128MB
256ULL * 1024 * 1024, // 256MB
512ULL * 1024 * 1024, // 512MB
1024ULL * 1024 * 1024, // 1024MB
2048ULL * 1024 * 1024, // 2048MB
};
constexpr int NUM_THREAD[] =
{
1, // 1 thread
2, // 2 thread
4, // 4 thread
};
constexpr int PTN_FUNC = sizeof(MEME_SET) / sizeof(Memset);
constexpr int PTN_WRITE_SIZE = sizeof(WRITE_SIZE) / sizeof(size_t);
constexpr int PTN_THREAD_NUM = sizeof(NUM_THREAD) / sizeof(int);
constexpr int NUM_ITR = 3;
constexpr int MEMSET_VAL = 255;
constexpr int ALIGNMENT = 64;
constexpr double CPU_FREQ = 3.5 * 1024 * 1024 * 1024; // i7-7800X 3.5GHz
double aveTrRate[PTN_FUNC][PTN_THREAD_NUM][PTN_WRITE_SIZE] = {};
for (int f = 0; f < PTN_FUNC; ++f)
{
for (int t = 0; t < PTN_THREAD_NUM; ++t)
{
for (int s = 0; s < PTN_WRITE_SIZE; ++s)
{
char *buf = nullptr;
const auto wSize = WRITE_SIZE[s];
const auto numThread = NUM_THREAD[t];
vector<double> wRate(NUM_ITR);
vector<size_t> wSizePerThread(numThread);
vector<size_t> wOfstPerThread(numThread, 0);
// スレッド毎の書込みサイズ計算
for (int i = 0; i < numThread; ++i)
{
wSizePerThread[i] = wSize / numThread;
if (i == numThread - 1)
wSizePerThread[i] += wSize % numThread;
if (i > 0)
wOfstPerThread[i] = wOfstPerThread[i - 1] + wSizePerThread[i];
}
// ヒープメモリ確保
buf = (reinterpret_cast<char *>(_aligned_malloc(wSize, ALIGNMENT)));
if (!buf)
{
printf("***memory allocation err\n");
exit(1);
}
for (int i = 0; i < NUM_ITR; ++i)
{
uint64_t beg, end;
uint32_t tmp;
double rate = 0.0;
bool isErr = false;
vector<thread> trThread;
trThread.reserve(numThread);
// キャッシュメモリをフラッシュ
for (auto j = 0; j < wSize >> 6; ++j)
_mm_clflush(buf + 64 * j);
// 転送時間を計測
beg = __rdtscp(&tmp);
for (int j = 0; j < numThread; ++j)
{
trThread.emplace_back(thread(MEME_SET[f],
buf + wOfstPerThread[j],
MEMSET_VAL,
wSizePerThread[j]));
}
for (int j = 0; j < numThread; ++j)
trThread[j].join();
end = __rdtscp(&tmp);
// 転送レート(MB per second)を記録
rate = (wSize / (1024 * 1024)) / ((end - beg) / CPU_FREQ);
wRate.push_back(rate);
// 結果の検証
for (auto j = 0; j < wSize; ++j)
{
if (buf[j] != static_cast<char>(MEMSET_VAL))
isErr = true;
}
if (isErr)
printf("*** verify err\n");
}
// ヒープメモリ解放
_aligned_free(buf);
aveTrRate[f][t][s] =
accumulate(wRate.begin(), wRate.end(), 0.0) / NUM_ITR;
}
}
}
printf("%6s %8s %16s %16s\n"
, "thread"
, "size[MB]"
, "memset[MB/s]"
, "memset_nt[MB/s]");
for (int t = 0; t < PTN_THREAD_NUM; ++t)
{
for (int s = 0; s < PTN_WRITE_SIZE; ++s)
{
printf("%6d %8llu"
, NUM_THREAD[t]
, WRITE_SIZE[s] / (1024 * 1024));
for (int f = 0; f < PTN_FUNC; ++f)
printf(" %16.2f", aveTrRate[f][t][s]);
printf("\n");
}
}
}
プログラムの概要は下記です。
- 標準ライブラリのmemsetと_mm256_stream_si256を使ったmemset_nt関数を比較
- 書込みサイズは、16MB~2GBと2次キャッシュでも収まらないサイズに設定
- スレッド数は、1, 2, 4の3パタンに設定
- 上記の組合せを各3回ずつ計測
なお、プログラム中のCPU_FREQ
は、私の環境のCPUの動作周波数なので流用される方はご注意を。
3. 実行例
Window10, i7-7800X@3.5GHz(物理コア数6), DDR4-2400@4ch 16GBx8, VisualStudio2019 x64ビルドでの実行例です。
thread size[MB] memset[MB/s] memset_nt[MB/s]
1 16 11345.33 17712.22
1 32 19247.36 20267.48
1 64 21254.95 21347.63
1 128 22490.71 22361.12
1 256 22549.88 22171.41
1 512 23041.12 22871.81
1 1024 23319.27 22608.92
1 2048 23151.53 23049.48
2 16 25314.78 26894.04
2 32 31644.99 35528.00
2 64 36194.49 39132.16
2 128 39206.65 41527.68
2 256 40618.50 42692.81
2 512 41489.82 43097.01
2 1024 42161.47 43568.82
2 2048 42039.94 44200.75
4 16 27814.67 32413.75
4 32 35470.41 48683.81
4 64 38428.65 59310.47
4 128 41696.34 68718.82
4 256 42771.21 74161.52
4 512 43756.21 76315.55
4 1024 44088.75 76879.00
4 2048 44320.05 77849.70
DDR4-2400@4chの理論転送レートは76800[MB/sec]なので、以下の事がわかります。
- memsetでは、4threadで2GB書込むときに、最大で理論転送レートの約57.7%(=100 * 44320 / 76800)
- memset_ntでは、4threadで2GB書込むときに、最大で理論転送レートの101.4%(=100 * 77849 / 76800)
- memsetは、スレッド数を2->4にしても転送レートは2倍にならない
- _mm256_stream_si256は、キャッシュを汚さない書込み命令なのでマルチスレッドで書込みサイズが大きい程、転送レートの改善効果が高そう
4. まとめ
- memsetでは、4スレッド使っても最大で最大転送レートの約57.7%(=100 * 44320 / 76800)しか出ないことがわかりました
- _mm256_stream_si256を使うと、4スレッドで最大転送レートが出てることもわかりました
- 一方、シングルスレッド処理ではmemsetと_mm256_stream_si256に大きな差はなかったため、マルチスレッドでかつ大きなサイズのメモリ書込み時に、memsetを使ってる処理では高速化できる可能性がありそうです
- _mm256_stream_si256を使うには、32バイトにアラインメントされたメモリ領域でないといけませんので、非アラインメント領域や32バイト未満の端数領域には別の処理が必要です
Discussion