Как увеличить скорость передачи данных в памяти DDR3?

Я пытаюсь измерить скорость передачи данных в памяти 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.

Ответ 1

Уменьшение пропускной способности при увеличении шага, скорее всего, связано с тем, что предварительная выборка памяти больше не работает, если вы не линейно перемещаетесь по памяти.

Что вы можете сделать, чтобы улучшить скорость:

  • Испытательная скорость будет искусственно привязана самим циклом, занимающим циклы процессора. Как показывает Рой, более высокая скорость может быть достигнута путем развертывания цикла.
  • Вам следует избавиться от проверки границ (с помощью "unchecked" )
  • Вместо использования Parallel.For используйте Thread.Start и запишите каждый поток, который вы запускаете на отдельном ядре (используя здесь код: Установить зависимость потока потоков в Microsoft.Net)
  • Убедитесь, что все потоки запускаются в одно и то же время, поэтому вы не измеряете отступников (вы можете сделать это, вращаясь по адресу памяти, который вы Interlock.Exchange) добавили к новому значению, когда все потоки работают и вращаются)
  • На машине NUMA (например, 2 Socket Modern Xeon) вам может потребоваться дополнительные шаги для выделения памяти на NUMA node, в которой будет работать поток. Для этого вам потребуется PInvoke VirtualAllocExNuma
  • Говоря о распределении памяти, использование больших страниц должно обеспечить еще один импульс

Хотя .NET не самая простая структура для использования для такого типа тестирования, возможно уговорить ее делать то, что вы хотите.

Ответ 2

Отчетные операционные результаты (128 МБ) для моего теста bus8thread64.exe на i7 3820 с максимальной пропускной способностью памяти 51,2 ГБ/с варьируются от 15,6 с 1 потоком, 28,1 с 2 потоками до 38,7 при 8 потоках. Код:

   void inc1word(IDEF data1[], IDEF ands[], int n)
    {
       int i, j;

       for(j=0; j<passes1; j++)
       {
           for (i=0; i<wordsToTest; i=i+64)
           {
               ands[n] = ands[n] & data1[i   ] & data1[i+1 ] & data1[i+2 ] & data1[i+3 ]
                                 & data1[i+4 ] & data1[i+5 ] & data1[i+6 ] & data1[i+7 ]
                                 & data1[i+8 ] & data1[i+9 ] & data1[i+10] & data1[i+11]
                                 & data1[i+12] & data1[i+13] & data1[i+14] & data1[i+15]
                                 & data1[i+16] & data1[i+17] & data1[i+18] & data1[i+19]
                                 & data1[i+20] & data1[i+21] & data1[i+22] & data1[i+23]
                                 & data1[i+24] & data1[i+25] & data1[i+26] & data1[i+27]
                                 & data1[i+28] & data1[i+29] & data1[i+30] & data1[i+31]
                                 & data1[i+32] & data1[i+33] & data1[i+34] & data1[i+35]
                                 & data1[i+36] & data1[i+37] & data1[i+38] & data1[i+39]
                                 & data1[i+40] & data1[i+41] & data1[i+42] & data1[i+43]
                                 & data1[i+44] & data1[i+45] & data1[i+46] & data1[i+47]
                                 & data1[i+48] & data1[i+49] & data1[i+50] & data1[i+51]
                                 & data1[i+52] & data1[i+53] & data1[i+54] & data1[i+55]
                                 & data1[i+56] & data1[i+57] & data1[i+58] & data1[i+59]
                                 & data1[i+60] & data1[i+61] & data1[i+62] & data1[i+63];
           }
        }
    }

Это также измеряет скорости считывания папок, где max DTR на основе этого составляет 46,9 ГБ/с. Исходный код и исходный код:

http://www.roylongbottom.org.uk/quadcore.zip

Для результатов с интересными скоростями с использованием кеша L3 находятся:

http://www.roylongbottom.org.uk/busspd2k%20results.htm#anchor8Thread

Ответ 3

C/С++ даст более точный показатель производительности памяти, так как .NET иногда может делать некоторые странные вещи с обработкой памяти и не даст вам точного изображения, так как он не использует встроенные скрипты компилятора или инструкции SIMD.

Там нет гарантии, что CLR даст вам все, что можно по-настоящему сравнить с вашей оперативной памятью. Я уверен, что, возможно, программное обеспечение уже написано для этого. Ах, да, PassMark что-то делает: http://www.bandwidthtest.net/memory_bandwidth.htm

Скорее всего, ваш лучший выбор для создания бенчмаркинга - это в значительной степени все, что они делают. Кроме того, хороший процессор btw, у меня тот же самый в одном из моих машин;)

ОБНОВЛЕНИЕ (2/20/2014): Я помню, что видел некоторый код в XNA Framework, который сделал некоторые сверхмощные оптимизации на С#, которые могут дать вам именно то, что вы хотите. Вы пытались использовать "небезопасный" код и указатели?