Каковы реальные существенные случаи, когда memcpy() работает быстрее, чем memmove()?

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

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

Ответ 1

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

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

Ответ 2

Там, по крайней мере, неявная ветвь для копирования вперед или назад для 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, что означает, что стоимость производительности может быть немного больше, чем одна ветвь для реализаций C этих функций.

Ответ 3

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

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

Ответ 4

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

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

Ответ 5

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

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

Ответ 6

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

Ответ 7

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

  • В некоторых реализациях определение перекрытия адресов может быть дорогостоящим. В стандарте C нет способа определить, указывают ли исходные и целевые объекты на одну и ту же выделенную область памяти, и, следовательно, никоим образом не могут использоваться на них более или менее операторы без спонтанного причинения кошек и собак ладить друг с другом (или ссылаться на другое поведение Undefined). Вполне вероятно, что любая практическая реализация будет иметь некоторые эффективные способы определения того, перекрываются ли указатели, но стандарт не требует наличия таких средств. Функция memmove(), полностью написанная в переносном C, на многих платформах, вероятно, займет как минимум в два раза длиннее, как и memcpy(), также полностью написанный в переносном C.
  • Реализациям разрешено расширять функции в строке, если это не изменит их семантику. В компиляторе 80x86, если регистры ESI и EDI не имеют ничего важного, memcpy (src, dest, 1234) может генерировать код:
      mov esi,[src]
      mov edi,[dest]
      mov ecx,1234/4 ; Compiler could notice it 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 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() не будет безопасно. Я даже не знаю, что делать с такой оптимизацией.