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

Я пытаюсь измерить скорость передачи данных памяти DDR3 с помощью теста. Согласно спецификации процессора. максимальная теоретическая пропускная способность – 51,2 ГБ/с. Это должна быть объединенная пропускная способность четырех каналов, то есть 12,8 ГБ/канал. Однако это теоретический предел, и мне любопытно, как еще больше увеличить практический предел в этом посте. В описанном ниже тестовом сценарии я достиг скорости передачи данных ~14 ГБ/с, что, как я полагаю, может быть близким приближением, когда подавляется большая часть прироста пропускной способности кэшей ЦП L1, L2 и L3.

Обновление от 20 марта 2014 г. Предположение об уничтожении кешей L1–L3 неверно. Аппаратная предварительная выборка контроллера памяти будет анализировать схему доступа к данным, и, поскольку она является последовательной, у нее будет простая задача предварительной выборки данных в кэши ЦП.

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

Для начала я создал тест на С# на .NET. Хотя .NET не идеален с точки зрения распределения памяти, я думаю, что это выполнимо для этого теста (пожалуйста, дайте мне знать, если вы не согласны и почему). Тест состоит в том, чтобы выделить массив int64 и заполнить его целыми числами. Этот массив должен иметь данные, выровненные в памяти. Затем я просто зацикливаю этот массив, используя столько потоков, сколько у меня есть ядер на машине, и читаю значение int64 из массива и устанавливаю его в локальное общедоступное поле в тестовом классе. Поскольку поле результата является общедоступным, мне следует избегать оптимизации компилятором вещей в цикле. Кроме того, и это может быть слабым предположением, я думаю, что результат остается в регистре и не записывается в память до тех пор, пока он не будет перезаписан снова. Между каждым чтением элемента в массиве я использую переменное смещение шага 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 ГБ/с.

Как видно, пропускная способность падает по мере увеличения шага, что я считаю нормальным. Отчасти, я думаю, это связано с тем, что 12 МБ кэш-памяти L3 вызывает промахи в кэш-памяти, а отчасти это может быть механизм предварительной выборки контроллеров памяти, который не работает, когда чтения так далеко друг от друга. Я также считаю, что результат шага 1000 наиболее близок к фактической практической скорости памяти, поскольку он должен убить большую часть кешей ЦП и «надеюсь» убить механизм предварительной выборки. Более того, я предполагаю, что большая часть накладных расходов в этом цикле связана с операцией выборки памяти, а не с чем-то еще.

Оборудование для этого теста: Intel Core I7-3930 (спецификации: Краткое описание процессора, более подробная и действительно подробная спецификация ), используя в общей сложности 32 ГБ памяти DDR3-1600.

Открытые вопросы

  1. Правильно ли я делаю предположения, сделанные выше?

  2. Есть ли способ увеличить использование пропускной способности памяти? Например, сделать это на C/C++ вместо этого и распределить выделение памяти в куче, чтобы можно было использовать все четыре канала памяти.

  3. Есть ли лучший способ измерения передачи данных в память?

Премного благодарен за вклад в это. Я знаю, что это сложная область под капотом...

Весь приведенный здесь код доступен для загрузки по адресу https://github.com/Toby999/ThroughputTest. Не стесняйтесь обращаться ко мне по электронной почте для переадресации tobytemporary[at]gmail.com.


person Toby999    schedule 12.12.2013    source источник
comment
Хороший вопрос, если бы у него был какой-то код с тем, что вы пробовали, что вы ожидали и что вы на самом деле получили.   -  person Prashant Kumar    schedule 13.12.2013
comment
@Prashant: я думаю, что ожидаемое / фактически полученное уже присутствует (51,2 ГБ / с против ~ 10 ГБ / с).   -  person Oliver Charlesworth    schedule 13.12.2013
comment
@ Оли Чарльзворт Ах, верно. Тогда только код.   -  person Prashant Kumar    schedule 13.12.2013
comment
В порядке. Извини за это. Я обновил содержимое теперь с кодом.   -  person Toby999    schedule 13.12.2013
comment
Вам будет трудно реализовать полную пропускную способность памяти с .NET. Обычно это зарезервировано для тех, кто использует SIMD, к которому .NET не дает никакого доступа.   -  person Cory Nelson    schedule 13.12.2013
comment
Я только что реализовал реализацию SSE на C++ как часть этого тестового проекта. Но использование пропускной способности памяти по-прежнему интересно/важно знать больше, независимо от платформы. Возможно, преобразование того же теста в C++ принесет больше информации и больше возможностей. Это вопрос номер 2. :)   -  person Toby999    schedule 13.12.2013
comment
Разве вы не должны делить на 1048576, чтобы получить МБ/с? Хотя, поскольку вы разделили на 1e9, я думаю, что делитель должен быть 1073741824 и переменная с именем resultGBPerSec.   -  person Andrew Morton    schedule 17.12.2013
comment
Да вы правы. Я должен использовать двоичное представление для скорости передачи данных RAM. Хотя в приведенном коде нет измерения МБ. Только MRequests, поэтому я уточнил это и в названии. Спасибо.   -  person Toby999    schedule 18.12.2013
comment
Вас случайно не бьют ложным репостом?   -  person Chris O    schedule 18.12.2013
comment
Спасибо. Это может быть очень важной причиной. Мне нужно еще кое-что прочитать в качестве продолжения, но эта статья: bit.ly/JIqTVz кажется что указывает именно на это: [...] транзакции когерентности, которые возникают, когда разные процессоры обновляют разные слова одного и того же блока кэша в чередующемся режиме [...] измерения также показывают, что плохая пространственная локальность среди доступов к общим данным имеет даже большее воздействие.   -  person Toby999    schedule 18.12.2013
comment
51 ГБ/с — это пропускная способность графических карт, я был бы удивлен, если бы основная память могла так быстро разгоняться. Но, может быть, и может. Я также хотел бы знать, что SIMD может улучшить в памяти, поскольку, насколько я понимаю, SIMD касается инструкций и регистров ЦП, а не того, как данные передаются из ОЗУ? Наконец, разве 51 ГБ не является маркетинговой цифрой, которая может иметь место только тогда, когда доступ к памяти осуществляется 4 потоками, использующими каждый свой собственный узел NUMA?   -  person v.oddou    schedule 19.12.2013
comment
Да, я думаю, вы правы, что 51 ГБ/с может быть достигнуто только в очень особых случаях, когда каждый канал памяти используется по максимуму с использованием конфигурации NUMA, чего может быть сложно достичь в .NET. Что касается выполнения SIMD, насколько я понимаю, процессоры Intel по-прежнему используют кэши ЦП L1-L3 одновременно с обычной обработкой. Это хорошая вещь. Хотя я где-то читал, что можно обойти кеши ЦП для записи, но я не уверен, что это возможно и для чтения. Если это так, я думаю, что это может быть полезно для предотвращения проблем с когерентностью кеша в некоторых сценариях. ДЕЛО...   -  person Toby999    schedule 20.12.2013
comment
Когда вы пишете в одно и то же поле из многих потоков, вы пингуете строку кэша между ядрами. Это должно быть очень дорого. Попробуйте суммировать все элементы массива в локальную переменную. Суммирование — дешевая операция. Засунуть итоговую сумму в GC.KeepAlive. Я не понимаю, почему вы не сможете достичь максимальной скорости 51 ГБ/сек с .NET и 8 потоками. Это 6 ГБ/сек на ядро. У вас около 3g инструкций в секунду. Вам нужно и в среднем. 2 байта за цикл, что легко. Немного разверните петлю. Переместите доступ Config к локальным переменным. Не доверяйте JIT для оптимизации чего-либо.   -  person usr    schedule 27.12.2013
comment
Можете ли вы разместить где-нибудь автономный фрагмент кода? попробую улучшить.   -  person usr    schedule 27.12.2013
comment
@ Toby999, честно говоря, я не думаю, что это можно измерить в пользовательском режиме. Простой факт, что в большинстве случаев вы не получите полную секунду на ЦП, прежде чем вы заполните свой тик (win 8 отличается тем, что это «бестактная» ОС, но даже в такой ОС вы вряд ли получите полную секунду процессорного времени подряд). Когда вы вернете ЦП, кеш будет аннулирован, и у вас также будут ошибки страниц, с которыми нужно справиться, что значительно замедлит это.   -  person Mgetz    schedule 27.12.2013
comment
Уср: Извините. Я был очень занят чем-то другим на работе, поэтому мне пришлось на время закрыть это дело. Теперь я обновил сообщение, добавив проект Github, где вы можете загрузить весь проект .NET, если хотите. Я надеюсь, что вскоре у меня будет время лично протестировать некоторые из предложений, представленных ниже.   -  person Toby999    schedule 20.02.2014
comment
Mgetz: Спасибо за ваш вклад. Я учту это в моем, надеюсь, предстоящем дальнейшем анализе этого.   -  person Toby999    schedule 20.02.2014


Ответы (3)


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

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

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

Хотя .NET — не самый простой фреймворк для такого типа тестирования, его МОЖНО уговорить делать то, что вы хотите.

person Thomas Kejser    schedule 28.12.2013
comment
Спасибо за этот вклад, Томас. И специально для подтверждения моей гипотезы, что это возможно на .NET. :) Извините, у меня еще не было времени прокомментировать или опробовать ваши предложения, но я надеюсь, что скоро смогу это сделать. - person Toby999; 20.02.2014

Сообщенные результаты ОЗУ (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];
           }
        }
    }

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

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

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

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

person Roy Longbottom    schedule 28.12.2013
comment
Забыл сказать, что каждый поток имеет отдельный массив, выделенный как (X = от 1 до 8): arrayX = (IDEF *)_aligned_malloc(memoryBytes[sizes-1], 16); IDEF — это int или __int64 для 32- или 64-битных версий. - person Roy Longbottom; 28.12.2013
comment
Спасибо за отзыв. Я скоро попробую ваш тест, и, возможно, он достаточно хорош для того, что мне нужно. Прошу прощения, что мне потребовалось так много времени, чтобы вернуться на этот трек. Надеюсь, скоро я смогу проанализировать вашу работу. - person Toby999; 20.02.2014

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

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

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

ОБНОВЛЕНИЕ (20.02.2014): я помню, что видел некоторый код в XNA Framework, который выполнял некоторые мощные оптимизации на C#, которые могут дать вам именно то, что вы хотите. Пробовали ли вы использовать «небезопасный» код и указатели?

person Caleb Everett    schedule 27.12.2013
comment
Спасибо Калеб за ваш вклад. Я включу это в мое, надеюсь, предстоящее дальнейшее расследование по этому поводу. И да, процессор хороший, но теперь я понял, что мне нужна архитектура на основе Haswell, чтобы иметь возможность опробовать некоторые встроенные методы AVX2 (SIMD). :( - person Toby999; 20.02.2014
comment
У меня на домашнем компьютере процессор Haswell. Ядро i7 4770К. Я мог бы провести тесты для вас, если хотите. - person Caleb Everett; 21.02.2014
comment
Хм. Спасибо. Это было бы прекрасно. Это могло бы дать мне информацию, если бы это стоило обновления. Хотя на самом деле это не эталон, а полноценный масштаб текущего исследования, которым я занимаюсь. Но, возможно, я могу рассказать вам больше об этом по почте, если вы заинтересованы. Со мной можно связаться по адресу tobytemporary[at]gmail.com (и я отвечу, указав свой настоящий адрес). - person Toby999; 21.02.2014
comment
относительно небезопасного кода и указателей. Неа. Еще нет. Я мог бы попробовать это, я думаю, поскольку я, скорее всего, также буду тестировать написание этого на C ++ вместо этого. Хотя мой предыдущий опыт показывает, что простой компилятор C++ имеет огромное значение по сравнению с компилятором C#/JIT. - person Toby999; 21.02.2014