Каковы действительно важные случаи, когда memcpy () работает быстрее, чем memmove ()? - PullRequest
16 голосов
/ 13 сентября 2010

Разница между memcpy() и memmove() в том, что memmove() будет работать нормально, когда источник и пункт назначения перекрываются. Когда буферы наверняка не перекрываются, memcpy () предпочтительнее , поскольку это потенциально быстрее.

Что меня беспокоит, так это потенциально . Это микрооптимизация или есть действительно важные примеры, когда memcpy() быстрее, так что нам действительно нужно использовать memcpy(), а не придерживаться memmove() везде?

Ответы [ 7 ]

19 голосов
/ 13 сентября 2010

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

Считывание кода eglibc-2.11.1 для memcpy() и memmove() подтверждает это как подозрение. Кроме того, нет возможности копирования страниц при обратном копировании, существенное ускорение доступно только в том случае, если нет шансов для наложения.

В итоге это означает: если вы можете гарантировать, что регионы не перекрываются, то выбор memcpy() over memmove() позволяет избежать ветвления. Если источник и назначение содержат соответствующие области, выровненные по размеру страницы и по размеру страницы, и не перекрываются, некоторые архитектуры могут использовать аппаратно ускоренные копии для этих областей, независимо от того, вызвали ли вы memmove() или memcpy().

Update0

На самом деле есть еще одно отличие от предположений и наблюдений, которые я перечислил выше. Начиная с C99, для двух функций существуют следующие прототипы:

void *memcpy(void * restrict s1, const void * restrict s2, size_t n);
void *memmove(void * s1, const void * s2, size_t n);

Из-за возможности предположить, что 2 указателя s1 и s2 не указывают на перекрывающуюся память, простые реализации C memcpy могут использовать это для генерации более эффективного кода без обращения к ассемблеру, см. здесь для более. Я уверен, что memmove может сделать это, однако потребуются дополнительные проверки сверх тех, которые я видел в eglibc, что означает, что затраты производительности могут быть немного больше, чем одна ветвь для реализаций этих функций на Си.

13 голосов
/ 13 сентября 2010

В лучшем случае вызов memcpy вместо memmove сохранит сравнение указателей и условную ветвь.Для большой копии это совершенно незначительно.Если вы делаете много маленьких копий, то, возможно, стоит измерить разницу;это единственный способ определить, важно это или нет.

Это определенно микрооптимизация, но это не значит, что вам не следует использовать memcpy, когда вы можете легко доказать, что это безопасно.Преждевременная пессимизация - корень большого зла.

4 голосов
/ 13 сентября 2010

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

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

2 голосов
/ 21 ноября 2011

Вполне возможно, что в большинстве реализаций стоимость вызова функции memmove () не будет значительно выше, чем memcpy () в любом сценарии, в котором определяется поведение обоих.Однако есть еще два момента, которые еще не упомянуты:

  1. В некоторых реализациях определение перекрытия адресов может быть дорогостоящим.В стандарте C нет способа определить, указывают ли исходные и целевые объекты на одну и ту же выделенную область памяти, и, следовательно, нельзя использовать операторы «больше» или «меньше», не вызывая спонтанных побуждений кошек и собак.ладить друг с другом (или вызывать другое неопределенное поведение).Вполне вероятно, что любая практическая реализация будет иметь некоторые эффективные средства для определения, перекрываются ли указатели, но стандарт не требует, чтобы такие средства существовали.Функция memmove (), написанная полностью на переносимом языке C, на многих платформах, вероятно, займет по крайней мере вдвое больше времени, чем memcpy (), также написанная полностью на переносимом языке C.
  2. Реализациям разрешается расширять функции при работетак что не изменил бы их семантику.На компиляторе 80x86, если регистры ESI и EDI не содержат ничего важного, memcpy (src, dest, 1234) может сгенерировать код:
      mov esi,[src]
      mov edi,[dest]
      mov ecx,1234/4 ; Compiler could notice it's a constant
      cld
      rep movsl
    
    Это займет столько же встроенного кода, но будет выполненонамного быстрее чем:
      push [src]
      push [dest]
      push dword 1234
      call _memcpy
    
      ...
    
    _memcpy:
      push ebp
      mov  ebp,esp
      mov  ecx,[ebp+numbytes]
      test ecx,3   ; See if it's a multiple of four
      jz   multiple_of_four
    
    multiple_of_four:
      push esi ; Can't know if caller needs this value preserved
      push edi ; Can't know if caller needs this value preserved
      mov esi,[ebp+src]
      mov edi,[ebp+dest]
      rep movsl
      pop edi
      pop esi
      ret  
    

Многие компиляторы будут выполнять такие оптимизации с помощью memcpy ().Я не знаю ни одного, который будет делать это с memmove, хотя в некоторых случаях оптимизированная версия memcpy может предлагать ту же семантику, что и memmove.Например, если numbytes было 20:

; Assuming values in eax, ebx, ecx, edx, esi, and edi are not needed
  mov esi,[src]
  mov eax,[esi]
  mov ebx,[esi+4]
  mov ecx,[esi+8]
  mov edx,[esi+12]
  mov edi,[esi+16]
  mov esi,[dest]
  mov [esi],eax
  mov [esi+4],ebx
  mov [esi+8],ecx
  mov [esi+12],edx
  mov [esi+16],edi

Это будет работать правильно, даже если диапазоны адресов перекрываются, поскольку эффективно создает копию (в регистрах) всей области, которую нужно переместить, прежде чем какая-либо из них будетнаписано.Теоретически, компилятор мог бы обработать memmove (), увидев, что если его использовать как memcpy (), получится реализация, которая будет безопасна, даже если диапазоны адресов перекрываются, и вызовет _memmove в тех случаях, когда подстановка реализации memcpy () не будетбезопасный.Я не знаю никого, кто бы делал такую ​​оптимизацию.

2 голосов
/ 13 сентября 2010

Просто упростите и всегда используйте memmove. Функция, которая работает всегда, лучше, чем функция, которая работает только наполовину.

2 голосов
/ 13 сентября 2010

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

СТем не менее, я бы рекомендовал никогда не использовать memmove, если вы не перемещаете данные в пределах одного буфера.Это может быть не медленнее, но опять же, это может быть, так зачем рисковать, если вы знаете, что memmove?

не нужно
2 голосов
/ 13 сентября 2010

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

Сказав это, я видел платформы с одинаковым кодом для memcpy и memmove.

...