различия в производительности memcpy между 32- и 64-разрядными процессами

StackOverflow https://stackoverflow.com/questions/269408

Вопрос

У нас есть компьютеры Core2 (Dell T5400) с XP64.

Мы наблюдаем, что при запуске 32-разрядных процессов производительность memcpy составляет порядка 1,2 Гбайт / с;однако скорость memcpy в 64-разрядном процессе достигает примерно 2,2 Гбайт / с (или 2,4 Гбайт / с с memcpy от Intel compiler CRT).Хотя первоначальной реакцией могло бы быть простое объяснение этого поскольку из-за более широких доступных регистров в 64-битном коде мы наблюдаем, что наш собственный memcpy-подобный Ассемблерный код SSE (который должен использовать 128-битные хранилища с широкой загрузкой независимо от 32/64-разрядности процесса) демонстрирует аналогичные верхние пределы достигаемой пропускной способности копирования.

Мой вопрос в том, из-за чего на самом деле эта разница ?Должны ли 32-разрядные процессы проходить через несколько дополнительных циклов WOW64, чтобы получить доступ к оперативной памяти?Это как-то связано с TLBS, предварительными выборками или...что ?

Спасибо за любую информацию.

Также поднимался вопрос о Форумы Intel.

Это было полезно?

Решение

Конечно, вам действительно нужно взглянуть на фактические машинные инструкции, которые выполняются внутри самого внутреннего цикла memcpy, войдя в машинный код с помощью отладчика. Все остальное - просто предположение.

Я хотел бы знать, что он, вероятно, не имеет ничего общего с 32-разрядным по сравнению с 64-разрядным как таковым; я предполагаю, что более быстрая библиотечная подпрограмма была написана с использованием временных хранилищ SSE.

Если внутренний цикл содержит какие-либо вариации обычных инструкций хранения данных, затем целевая память должна быть считана в кэш машины, изменена и выписана обратно. Поскольку это чтение совершенно не нужно - считываемые биты немедленно перезаписываются - вы можете сэкономить половину пропускной способности памяти, используя & Quot; non-temporal & Quot; напишите инструкции, которые обходят кеши. Таким образом, память места назначения просто записывается, совершая одностороннюю поездку в память вместо обратной поездки.

Я не знаю библиотеку CRT компилятора Intel, так что это всего лишь предположение. Нет особой причины, по которой 32-битный libCRT не может делать то же самое, но ускорение, которое вы цитируете, находится на уровне, который я ожидал, просто преобразовав инструкции movdqa в movnt ...

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

Другие советы

Я думаю, что следующее может объяснить это:

Чтобы скопировать данные из памяти в регистр и обратно в память, вы делаете

mov eax, [address]
mov [address2], eax

Это перемещает 32 бита (4 байта) от адреса к адресу2. То же самое происходит с 64-битным в 64-битном режиме

mov rax, [address]
mov [address2], rax

Это перемещает 64 бита, 2 байта, от адреса к адресу2. Quot & & мов Quot; сам по себе, независимо от того, является ли он 64-битным или 32-битным, имеет задержку 0,5 и пропускную способность 0,5 в соответствии со спецификациями Intel. Задержка - это количество тактов, которое требуется команде для прохождения по конвейеру, а пропускная способность - это сколько времени ЦП должен ждать, прежде чем снова принять ту же инструкцию. Как вы можете видеть, он может делать два mov за тактовый цикл, однако ему приходится ждать половину такта между двумя mov, поэтому он может эффективно выполнять только один mov за такт (или я ошибаюсь здесь и неверно истолковываю термины? См. PDF здесь для получения подробной информации.

Конечно, mov reg, mem может быть длиннее 0,5 цикла, в зависимости от того, находятся ли данные в кэше 1-го или 2-го уровня или вообще не находятся в кэше, и их необходимо извлечь из памяти. Тем не менее, указанное выше время задержки игнорирует этот факт (поскольку PDF-файл, который я связал выше) предполагает, что все данные, необходимые для mov, уже присутствуют (в противном случае задержка будет увеличиваться в зависимости от того, сколько времени потребуется для извлечения данных из любой точки мира). прямо сейчас - это может быть несколько тактов и полностью не зависит от выполняемой команды, говорится в PDF на стр. 482 / C-30).

Что интересно, 32-битный или 64-битный mov не играет никакой роли. Это означает, что если полоса пропускания памяти не станет ограничивающим фактором, 64-битные MOV одинаково быстры по сравнению с 32-битными MOV, и, поскольку при перемещении 64-битных данных при перемещении одного и того же объема данных из A в B требуется только половина, пропускная способность может (в теории) быть вдвое выше (тот факт, что это не так, вероятно, потому что память не безгранична быстро).

Хорошо, теперь вы думаете, что при использовании больших регистров SSE вы должны получить более высокую пропускную способность, верно? AFAIK Регистры xmm имеют ширину не 256, а 128 бит, кстати ( ссылка в Википедии ). Однако учитывали ли вы время ожидания и пропускную способность? Либо данные, которые вы хотите переместить, выровнены по 128 битам, либо нет. В зависимости от этого, вы можете либо переместить его, используя

movdqa xmm1, [address]
movdqa [address2], xmm1

или если не выровнен

movdqu xmm1, [address]
movdqu [address2], xmm1

Ну, у movdqa / movdqu задержка 1 и пропускная способность 1. Таким образом, выполнение инструкций занимает вдвое больше времени, а время ожидания после инструкций вдвое больше, чем обычного mov.

И еще кое-что, что мы даже не приняли во внимание, это то, что процессор фактически разбивает команды на микрооперации и может выполнять их параллельно. Теперь это становится действительно сложным ... даже слишком сложным для меня.

В любом случае, я знаю из опыта, что загрузка данных в / из регистров xmm намного медленнее, чем загрузка данных в / из обычных регистров, поэтому ваша идея ускорить передачу с использованием регистров xmm была обречена с первой секунды. Я на самом деле удивлен, что в конце концов SSM memmove не намного медленнее, чем обычный.

Я, наконец, докопался до сути этого (и ответ Die in Sente был правильным, спасибо).

В приведенном ниже примере dst и src равны 512 МБайт std::vector.Я использую компилятор Intel 10.1.029 и CRT.

На 64-битных обоих

memcpy(&dst[0],&src[0],dst.size())

и

memcpy(&dst[0],&src[0],N)

где N - ранее объявленный const size_t N=512*(1<<20); звонить

__intel_fast_memcpy

основная часть которого состоит из:

  000000014004ED80  lea         rcx,[rcx+40h] 
  000000014004ED84  lea         rdx,[rdx+40h] 
  000000014004ED88  lea         r8,[r8-40h] 
  000000014004ED8C  prefetchnta [rdx+180h] 
  000000014004ED93  movdqu      xmm0,xmmword ptr [rdx-40h] 
  000000014004ED98  movdqu      xmm1,xmmword ptr [rdx-30h] 
  000000014004ED9D  cmp         r8,40h 
  000000014004EDA1  movntdq     xmmword ptr [rcx-40h],xmm0 
  000000014004EDA6  movntdq     xmmword ptr [rcx-30h],xmm1 
  000000014004EDAB  movdqu      xmm2,xmmword ptr [rdx-20h] 
  000000014004EDB0  movdqu      xmm3,xmmword ptr [rdx-10h] 
  000000014004EDB5  movntdq     xmmword ptr [rcx-20h],xmm2 
  000000014004EDBA  movntdq     xmmword ptr [rcx-10h],xmm3 
  000000014004EDBF  jge         000000014004ED80 

и работает со скоростью ~ 2200 Мбайт /с.

Но на 32-битном

memcpy(&dst[0],&src[0],dst.size())

звонки

__intel_fast_memcpy

основная часть которого состоит из

  004447A0  sub         ecx,80h 
  004447A6  movdqa      xmm0,xmmword ptr [esi] 
  004447AA  movdqa      xmm1,xmmword ptr [esi+10h] 
  004447AF  movdqa      xmmword ptr [edx],xmm0 
  004447B3  movdqa      xmmword ptr [edx+10h],xmm1 
  004447B8  movdqa      xmm2,xmmword ptr [esi+20h] 
  004447BD  movdqa      xmm3,xmmword ptr [esi+30h] 
  004447C2  movdqa      xmmword ptr [edx+20h],xmm2 
  004447C7  movdqa      xmmword ptr [edx+30h],xmm3 
  004447CC  movdqa      xmm4,xmmword ptr [esi+40h] 
  004447D1  movdqa      xmm5,xmmword ptr [esi+50h] 
  004447D6  movdqa      xmmword ptr [edx+40h],xmm4 
  004447DB  movdqa      xmmword ptr [edx+50h],xmm5 
  004447E0  movdqa      xmm6,xmmword ptr [esi+60h] 
  004447E5  movdqa      xmm7,xmmword ptr [esi+70h] 
  004447EA  add         esi,80h 
  004447F0  movdqa      xmmword ptr [edx+60h],xmm6 
  004447F5  movdqa      xmmword ptr [edx+70h],xmm7 
  004447FA  add         edx,80h 
  00444800  cmp         ecx,80h 
  00444806  jge         004447A0

и работает только со скоростью ~ 1350 Мбайт /с.

ОДНАКО

memcpy(&dst[0],&src[0],N)

где N - ранее объявленный const size_t N=512*(1<<20); компилируется (на 32 битах) для прямого вызова

__intel_VEC_memcpy

основная часть которого состоит из

  0043FF40  movdqa      xmm0,xmmword ptr [esi] 
  0043FF44  movdqa      xmm1,xmmword ptr [esi+10h] 
  0043FF49  movdqa      xmm2,xmmword ptr [esi+20h] 
  0043FF4E  movdqa      xmm3,xmmword ptr [esi+30h] 
  0043FF53  movntdq     xmmword ptr [edi],xmm0 
  0043FF57  movntdq     xmmword ptr [edi+10h],xmm1 
  0043FF5C  movntdq     xmmword ptr [edi+20h],xmm2 
  0043FF61  movntdq     xmmword ptr [edi+30h],xmm3 
  0043FF66  movdqa      xmm4,xmmword ptr [esi+40h] 
  0043FF6B  movdqa      xmm5,xmmword ptr [esi+50h] 
  0043FF70  movdqa      xmm6,xmmword ptr [esi+60h] 
  0043FF75  movdqa      xmm7,xmmword ptr [esi+70h] 
  0043FF7A  movntdq     xmmword ptr [edi+40h],xmm4 
  0043FF7F  movntdq     xmmword ptr [edi+50h],xmm5 
  0043FF84  movntdq     xmmword ptr [edi+60h],xmm6 
  0043FF89  movntdq     xmmword ptr [edi+70h],xmm7 
  0043FF8E  lea         esi,[esi+80h] 
  0043FF94  lea         edi,[edi+80h] 
  0043FF9A  dec         ecx  
  0043FF9B  jne         ___intel_VEC_memcpy+244h (43FF40h) 

и работает со скоростью ~ 2100 Мбит / с (и доказывает, что 32-битная пропускная способность каким-то образом не ограничена).

Я отзываю свое утверждение о том, что мой собственный SSE-код, подобный memcpy, страдает от аналогичного ~ 1300 МБайт / лимита в 32-битных сборках;Теперь у меня нет никаких проблем получение> 2 Гбайт / с на 32 или 64 битах;хитрость (как подсказывают приведенные выше результаты) заключается в использовании нестационарных ("потоковых") хранилищ (например _mm_stream_ps внутренний).

Кажется немного странным , что 32 - битный "dst.size()"функции memcpy не в конечном итоге вызывайте быстрее "movnt" версия (если вы зайдете в memcpy, там будет самое невероятное количество CPUID проверка и эвристическая логика, например, сравнение количества байтов, которые нужно скопировать, с размером кэша и т.д., прежде чем оно приблизится к вашим фактическим данным), Но, по крайней мере, я понимаю наблюдаемое поведение сейчас (и это не связанный с SysWOW64 или H / W).

Я предпочитаю, что 64-битные процессы используют собственный 64-битный объем памяти процессора, что оптимизирует использование шины памяти.

Спасибо за положительный отзыв! Я думаю, что могу частично объяснить, что здесь происходит.

Использование невременных хранилищ для memcpy - определенно быстрый if : вы только синхронизируете вызов memcpy.

С другой стороны, если вы тестируете приложение, хранилища movdqa имеют то преимущество, что они оставляют целевую память в кеше. Или, по крайней мере, та его часть, которая помещается в кеш.

Итак, если вы разрабатываете библиотеку времени выполнения и можете предположить, что приложение, которое вызвало memcpy, будет использовать целевой буфер сразу после вызова memcpy, тогда вы захотите предоставить версию movdqa. Это эффективно оптимизирует отключение памяти из процессора обратно в процессор, который будет следовать версии movntdq, и все инструкции после вызова будут выполняться быстрее.

Но, с другой стороны, если буфер назначения больше по сравнению с кешем процессора, эта оптимизация не работает, и версия movntdq даст вам более быстрые тесты приложений.

Так что идея memcpy будет иметь несколько версий под капотом. Если целевой буфер мал по сравнению с кешем процессора, используйте movdqa, в противном случае целевой буфер большой по сравнению с кешем процессора, используйте movntdq. Похоже, это то, что происходит в 32-битной библиотеке.

Конечно, это никак не связано с различиями между 32-разрядными и 64-разрядными.

Моя гипотеза состоит в том, что 64-битная библиотека не настолько развита. Разработчики просто еще не удосужились предоставить обе подпрограммы в этой версии библиотеки.

У меня нет ссылки передо мной, поэтому я не совсем уверен в сроках / инструкциях, но я все еще могу дать теорию. Если вы перемещаете память в 32-битном режиме, вы будете делать что-то вроде & Quot; rep movsd & Quot; который перемещает одно 32-битное значение каждый тактовый цикл. В 64-битном режиме вы можете выполнить & Quot; rep movsq & Quot; который делает один 64-битный ход за такт. Эта инструкция недоступна для 32-битного кода, поэтому вы будете делать 2 повторения movsd (по 1 циклу на фрагмент) за половину скорости выполнения.

ОЧЕНЬ сильно упрощено, игнорируя все проблемы пропускной способности / выравнивания памяти и т. д., но это то, с чего все начинается ...

Вот пример подпрограммы memcpy, предназначенной специально для 64-битной архитектуры.

void uint8copy(void *dest, void *src, size_t n){
    uint64_t * ss = (uint64_t)src;
    uint64_t * dd = (uint64_t)dest;
    n = n * sizeof(uint8_t)/sizeof(uint64_t); 

    while(n--)
        *dd++ = *ss++;
}//end uint8copy()

Полная статья здесь: http://www.godlikemouse.com/2008/03/04/ оптимизируя-тетср-процедура /

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top