Я пытаюсь измерить скорость передачи данных в памяти DDR3 через тест. Согласно спецификации процессора. максимальная теоретическая ширина полосы составляет 51,2 ГБ/с. Это должна быть комбинированная пропускная способность четырех каналов, что означает 12,8 ГБ/канал. Однако это теоретический предел, и мне любопытно, как еще больше увеличить практический предел в этом посте. В описанном ниже тестовом сценарии я достигаю скорость передачи данных с пропускной способностью ~ 14 ГБ/с, которая, по моему мнению, может быть близким приближением, когда вы убиваете большую часть увеличения кэша CPU L1, L2 и L3.
Обновление 20/3 2014: Это предположение об убийстве кешей L1-L3 ошибочно. Предварительная выборка harware контроллера памяти будет анализировать шаблон доступа к данным и, поскольку он будет последовательным, он будет иметь легкую задачу предварительной выборки данных в кэши процессора.
Конкретные вопросы следуют внизу, но главным образом меня интересует а) проверка допущений, ведущих к этому результату, и б) если есть лучший способ измерения полосы памяти в .NET.
Я построил тест на С# на .NET как стартер. Хотя .NET не идеален с точки зрения распределения памяти, я думаю, что это выполнимо для этого теста (пожалуйста, дайте мне знать, если вы не согласны и почему). Тест состоит в том, чтобы выделить массив int64 и заполнить его целыми числами. Этот массив должен иметь данные, выровненные в памяти. Затем я просто зацикливаю этот массив, используя столько потоков, что у меня есть ядра на машине, и прочитайте значение int64 из массива и установите его в локальное публичное поле в тестовом классе. Поскольку поле результата является общедоступным, я должен избегать компиляции, оптимизирующей прочь материал в цикле. Более того, и это может быть слабым предположением, я думаю, что результат остается в регистре и не записывается в память до тех пор, пока он не будет написан снова. Между каждым чтением элемента в массиве я использую переменную Step offset 10, 100 и 1000 в массиве, чтобы не было возможности получить много ссылок в одном блоке кэша (64 байт).
Чтение Int64 из массива должно означать чтение в 8 байт, а затем чтение фактического значения еще 8 байт. Поскольку данные извлекаются из памяти в 64-байтной строке кэша, каждый считываемый в массиве должен соответствовать 64-байтовому чтению из ОЗУ каждый раз в цикле, учитывая, что данные чтения не расположены в каких-либо кэшах ЦП.
Вот как я инициализирую массив данных:
_longArray = new long[Config.NbrOfCores][];
for (int threadId = 0; threadId < Config.NbrOfCores; threadId++)
{
_longArray[threadId] = new long[Config.NmbrOfRequests];
for (int i = 0; i < Config.NmbrOfRequests; i++)
_longArray[threadId][i] = i;
}
И вот настоящий тест:
GC.Collect();
timer.Start();
Parallel.For(0, Config.NbrOfCores, threadId =>
{
var intArrayPerThread = _longArray[threadId];
for (int redo = 0; redo < Config.NbrOfRedos; redo++)
for (long i = 0; i < Config.NmbrOfRequests; i += Config.Step)
_result = intArrayPerThread[i];
});
timer.Stop();
Поскольку сводка данных очень важна для результата, я тоже даю эту информацию (может быть пропущен, если вы доверяете мне...)
var timetakenInSec = timer.ElapsedMilliseconds / (double)1000;
long totalNbrOfRequest = Config.NmbrOfRequests / Config.Step * Config.NbrOfCores*Config.NbrOfRedos;
var throughput_ReqPerSec = totalNbrOfRequest / timetakenInSec;
var throughput_BytesPerSec = throughput_ReqPerSec * byteSizePerRequest;
var timeTakenPerRequestInNanos = Math.Round(1e6 * timer.ElapsedMilliseconds / totalNbrOfRequest, 1);
var resultMReqPerSec = Math.Round(throughput_ReqPerSec/1e6, 1);
var resultGBPerSec = Math.Round(throughput_BytesPerSec/1073741824, 1);
var resultTimeTakenInSec = Math.Round(timetakenInSec, 1);
Пренебрегая предоставлением фактического кода рендеринга вывода, я получаю следующий результат:
Step 10: Throughput: 570,3 MReq/s and 34 GB/s (64B), Timetaken/request: 1,8 ns/req, Total TimeTaken: 12624 msec, Total Requests: 7 200 000 000
Step 100: Throughput: 462,0 MReq/s and 27,5 GB/s (64B), Timetaken/request: 2,2 ns/req, Total TimeTaken: 15586 msec, Total Requests: 7 200 000 000
Step 1000: Throughput: 236,6 MReq/s and 14,1 GB/s (64B), Timetaken/request: 4,2 ns/req, Total TimeTaken: 30430 msec, Total Requests: 7 200 000 000
Используя 12 потоков вместо 6 (поскольку процессор имеет гиперпоточность), я получаю практически такую же пропускную способность (как я полагаю, думаю): 32,9/30,2/15,5 ГБ/с.
Как видно, пропускная способность падает по мере увеличения шага, который я считаю нормальным. Частично я думаю, что это связано с тем, что кеш-память L3 объемом 12 МБ заставляет пропустить прокси-серверы, и отчасти это может быть механизм предварительной выборки контроллеров памяти, который также не работает, когда показания читаются так далеко друг от друга. Я также считаю, что результат на этапе 1000 является самым близким к реальной практической скорости памяти, так как он должен убить большинство кэшей процессора и "надеюсь" убить механизм предварительной выборки. Более того, я предполагаю, что большая часть накладных расходов в этом цикле - операция выборки памяти, а не что-то еще.
аппаратное обеспечение для этого теста: Intel Core I7-3930 (характеристики: CPU breif, более подробно, и действительно подробная спецификация) с использованием 32 ГБ памяти DDR3-1600.
Открытые вопросы
-
Правильно ли я в предположениях, сделанных выше?
-
Есть ли способ увеличить использование пропускной способности памяти?. Например, сделав это на C/С++ и распределив выделение памяти больше на куче, позволяя всем четырем каналам памяти.
-
Есть ли лучший способ измерения передачи данных в памяти?
Значительная обязанность для ввода. Я знаю, что это сложная область под капотом...
Весь код здесь доступен для загрузки https://github.com/Toby999/ThroughputTest. Не стесняйтесь обращаться ко мне по адресу электронной почты пересылки tobytemporary [at] gmail.com.