различия в производительности memcpy между 32- и 64-битными процессами - PullRequest
9 голосов
/ 06 ноября 2008

У нас есть машины Core2 (Dell T5400) с XP64.

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

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

Спасибо за понимание.

Также поднят на форумах Intel .

Ответы [ 7 ]

8 голосов
/ 06 ноября 2008

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

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

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

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

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

Это перемещает 64 бита, 2 байта, от адреса к адресу2. Сам "mov", независимо от того, является ли он 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 не намного медленнее, чем обычный.

5 голосов
/ 11 февраля 2009

Я наконец дошел до сути (ответ Die в 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).

3 голосов
/ 25 ноября 2008

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

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

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

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

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

1 голос
/ 12 февраля 2009

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

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

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

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

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

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

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

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

1 голос
/ 06 ноября 2008

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

0 голосов
/ 18 декабря 2010

Вот пример подпрограммы 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/optimizing-memcpy-routines/

0 голосов
/ 11 ноября 2008

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

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

...