В чем разница между memmove и memcpy?

120

В чем разница между memmoveи memcpy? Какой из них вы обычно используете и как?

Сообщество
источник
Обратите внимание на проблемы, которые могут возникнуть: lwn.net/Articles/414467
Zan Lynx

Ответы:

162

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

Например, memcpyвсегда можно копировать адреса снизу вверх. Если место назначения перекрывается после источника, это означает, что некоторые адреса будут перезаписаны перед копированием. memmoveобнаружит это и скопирует в другом направлении - от высокого к низкому - в этом случае. Однако проверка этого и переключение на другой (возможно, менее эффективный) алгоритм требует времени.

bdonlan
источник
1
при использовании memcpy, как я могу гарантировать, что адреса src и dest не перекрываются? Должен ли я лично убедиться, что src и dest не перекрываются?
Олкотт,
6
@Alcott, не используйте memcpy, если вы не знаете, что они не перекрываются - вместо этого используйте memmove. Когда нет перекрытия, memmove и memcpy эквивалентны (хотя memcpy может быть очень, очень, очень немного быстрее).
bdonlan 09
Вы можете использовать ключевое слово restrict, если вы работаете с длинными массивами и хотите защитить процесс копирования. Например, если ваш метод принимает в качестве параметров массивы ввода и вывода, и вы должны убедиться, что пользователь не передает тот же адрес, что и ввод и вывод. Подробнее читайте здесь stackoverflow.com/questions/776283/…
DanielHsH 01
10
@DanielHsH 'restrict' - это обещание, которое вы даете компилятору; компилятор не применяет его. Если вы поместите параметр «ограничить» в свои аргументы и на самом деле имеют перекрытие (или, в более общем смысле, доступ к ограниченным данным из указателя, полученного из нескольких мест), поведение программы будет неопределенным, возникнут странные ошибки, и компилятор обычно не предупреждает об этом.
bdonlan
@bdonlan Это не просто обещание компилятору, это требование для вызывающей стороны. Это требование не соблюдается, но если вы его нарушите, вы не сможете пожаловаться, если получите неожиданный результат. Нарушение требования является неопределенным поведением, как и i = i++ + 1undefined; компилятор не запрещает вам писать именно этот код, но результатом этой инструкции может быть что угодно, и разные компиляторы или процессоры будут показывать здесь разные значения.
Mecki
33

memmoveможет обрабатывать перекрывающуюся память, memcpyне может.

Рассматривать

char[] str = "foo-bar";
memcpy(&str[3],&str[4],4); //might blow up

Очевидно, что теперь источник и место назначения перекрываются, мы заменяем «-bar» на «bar». Это неопределенное поведение с использованием, memcpyесли источник и место назначения перекрываются, поэтому в этом случае нам нужны случаи memmove.

memmove(&str[3],&str[4],4); //fine
н.у.к.
источник
5
с чего бы сначала взорваться?
4
@ultraman: Потому что это МОЖЕТ быть реализовано с использованием низкоуровневой сборки, которая требует, чтобы память не перекрывалась. Если это так, вы можете, например, сгенерировать сигнал или аппаратное исключение для процессора, который прерывает приложение. В документации указано, что он не обрабатывает условие, но в стандарте не указано, что произойдет, когда эти условия будут выполнены (это известно как неопределенное поведение). Неопределенное поведение может делать что угодно.
Мартин Йорк,
с gcc 4.8.2, даже memcpy также принимает перекрывающиеся указатели источника и назначения и работает нормально.
GeekyJ
4
@jagsgediya Конечно, может. Но так как memcpy задокументировано, чтобы не поддерживать это, вы не должны полагаться на это поведение, специфичное для реализации, поэтому существует memmove (). В другой версии gcc все может быть иначе. Это может быть иначе, если gcc встраивает memcpy вместо вызова memcpy () в glibc, это может быть иначе в более старой или новой версии glibc и так далее.
На практике кажется, что memcpy и memmove сделали то же самое. Такое глубокое неопределенное поведение.
Life
22

На странице руководства memcpy .

Функция memcpy () копирует n байтов из области памяти src в область памяти dest. Области памяти не должны перекрываться. Используйте memmove (3), если области памяти перекрываются.

Джон Картер
источник
12

Основное различие между memmove()и в memcpy()том , что в буфера - временная память - используется, так что нет никакого риска дублирования. С другой стороны, данные напрямую копируются из места, указанного источником, в место, указанное адресатом . ( http://www.cplusplus.com/reference/cstring/memcpy/ )memmove()memcpy()

Рассмотрим следующие примеры:

  1. #include <stdio.h>
    #include <string.h>
    
    int main (void)
    {
        char string [] = "stackoverflow";
        char *first, *second;
        first = string;
        second = string;
    
        puts(string);
        memcpy(first+5, first, 5);
        puts(first);
        memmove(second+5, second, 5);
        puts(second);
        return 0;
    }
    

    Как вы и ожидали, это распечатает:

    stackoverflow
    stackstacklow
    stackstacklow
    
  2. Но в этом примере результаты не будут такими же:

    #include <stdio.h>
    #include <string.h>
    
    int main (void)
    {
        char string [] = "stackoverflow";
        char *third, *fourth;
        third = string;
        fourth = string;
    
        puts(string);
        memcpy(third+5, third, 7);
        puts(third);
        memmove(fourth+5, fourth, 7);
        puts(fourth);
        return 0;
    }
    

    Вывод:

    stackoverflow
    stackstackovw
    stackstackstw
    

Это потому, что "memcpy ()" делает следующее:

1.  stackoverflow
2.  stacksverflow
3.  stacksterflow
4.  stackstarflow
5.  stackstacflow
6.  stackstacklow
7.  stackstacksow
8.  stackstackstw
Мирак Сузгун
источник
2
Но похоже, что результат, который вы упомянули, перевернут!
kumar
1
Когда я запускаю ту же программу, я получаю следующий результат: stackoverflow stackstackstw stackstackstw // означает, что нет никакой разницы в выводе между memcpy и memmove
kumar
4
"это то, что в" memmove () "используется буфер - временная память;" Это не правда. он говорит «как если бы», поэтому он просто должен вести себя так, а не так, как должно быть. Это действительно актуально, поскольку большинство реализаций memmove просто выполняют замену по XOR.
dhein
2
Я не думаю, что memmove()для использования буфера требуется реализация. Он имеет полное право перемещаться на месте (при условии, что каждое чтение завершается до любой записи по тому же адресу).
Тоби Спейт
12

Предполагая, что вам придется реализовать оба варианта, реализация может выглядеть так:

void memmove ( void * dst, const void * src, size_t count ) {
    if ((uintptr_t)src < (uintptr_t)dst) {
        // Copy from back to front

    } else if ((uintptr_t)dst < (uintptr_t)src) {
        // Copy from front to back
    }
}

void mempy ( void * dst, const void * src, size_t count ) {
    if ((uintptr_t)src != (uintptr_t)dst) {
        // Copy in any way you want
    }
}

И это должно хорошо объяснить разницу. memmoveвсегда копирует таким образом, что это все еще безопасно, если srcи dstперекрываются, тогда как memcpyпросто все равно, как говорится в документации при использовании memcpy, две области памяти не должны перекрываться.

Например, если memcpyкопии "спереди назад" и блоки памяти выровнены следующим образом

[---- src ----]
            [---- dst ---]

копирование первого байта srcв dstуже уничтожает содержимое последних байтов srcдо того, как они были скопированы. Только копирование «назад вперед» приведет к правильным результатам.

Теперь поменяйте местами srcи dst:

[---- dst ----]
            [---- src ---]

В этом случае безопасно копировать только «спереди назад», поскольку копирование «назад вперед» уничтожит srcего переднюю часть уже при копировании первого байта.

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

void memmove ( void * dst, const void * src, size_t count ) {
    if ((uintptr_t)src < (uintptr_t)dst
        && (uintptr_t)src + count > (uintptr_t)dst
    ) {
        // Copy from back to front

    } else if ((uintptr_t)dst < (uintptr_t)src
        && (uintptr_t)dst + count > (uintptr_t)src
    ) {
        // Copy from front to back

    } else {
        // They don't overlap for sure
        memcpy(dst, src, count);
    }
}

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

Что это значит для вас, когда вы решаете, в какой из них позвонить?

  1. Если вы не знаете это наверняка srcи dstне перекрываетесь, вызывайте, memmoveпоскольку это всегда приводит к правильным результатам и обычно выполняется настолько быстро, насколько это возможно для требуемого вам варианта копирования.

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

Mecki
источник
+1 потому что ваши «рисунки ascii» были полезны, чтобы понять, почему не может быть перекрытия без искажения данных
Scylardor
10

Один обрабатывает перекрывающиеся пункты назначения, другой - нет.

KPexEA
источник
6

просто из стандарта ISO / IEC: 9899 это хорошо описано.

7.21.2.1 Функция memcpy

[...]

2 Функция memcpy копирует n символов из объекта, на который указывает s2, в объект, на который указывает s1. Если копирование происходит между перекрывающимися объектами, поведение не определено.

И

7.21.2.2 Функция memmove

[...]

2 Функция memmove копирует n символов из объекта, на который указывает s2, в объект, на который указывает s1. Копирование происходит так, как если бы n символов из объекта, на который указывает s2, сначала копируются во временный массив из n символов, который не перекрывает объекты, на которые указывают s1 и s2, а затем n символов из временного массива копируются в объект, на который указывает s1.

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

В обычном тексте memcpy()не допускает s1и s2перекрытия, а не допускает memmove().

dhein
источник
0

Есть два очевидных способа реализовать mempcpy(void *dest, const void *src, size_t n)(игнорируя возвращаемое значение):

  1. for (char *p=src, *q=dest;  n-->0;  ++p, ++q)
        *q=*p;
  2. char *p=src, *q=dest;
    while (n-->0)
        q[n]=p[n];

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

В memmove()простейшем случае реализация будет тестировать dest<src(некоторым платформо-зависимым способом) и выполнять соответствующее направление memcpy().

Код пользователя не может сделать это, конечно, потому что даже после отливки srcи dstв какой - то тип указателя бетона, они не (в целом) точек в тот же объект , и поэтому не могут быть сопоставлены. Но стандартная библиотека может иметь достаточно знаний о платформе, чтобы выполнить такое сравнение, не вызывая неопределенного поведения.


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

Тоби Спейт
источник
0

memmove может иметь дело с перекрывающимися областями источника и назначения, в то время как memcpy не может. Среди них memcpy намного эффективнее. Так что лучше ИСПОЛЬЗУЙТЕ memcpy, если можете.

Ссылка: https://www.youtube.com/watch?v=Yr1YnOVG-4g Доктор Джерри Кейн, (Стэнфордская вводная лекция по системам - 7) Время: 36:00

Ehsan
источник
В этом ответе говорится «возможно, немного быстрее» и предоставляются количественные данные, указывающие лишь на небольшую разницу. Этот ответ утверждает, что один «намного эффективнее». Насколько эффективнее вы нашли более быстрый? BTW: Я полагаю, вы имеете в виду, memcpy()а не memcopy().
chux
Комментарий сделан на основе лекции доктора Джерри Кейна. Прошу вас послушать его лекцию в 36:00, всего 2-3 минуты хватит. И спасибо за улов. : D
Ehsan