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

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

11 ответов

Решение

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

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

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

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

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,

Со страницы руководства memcpy.

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

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

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

  1. #include <stdio.h>
    #include <string.h>
    
    int main (void)
    {
        char string [] = "stackru";
        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;
    }
    

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

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

    #include <stdio.h>
    #include <string.h>
    
    int main (void)
    {
        char string [] = "stackru";
        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;
    }
    

    Выход:

    stackru
    stackstackovw
    stackstackstw
    

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

1.  stackru
2.  stacksverflow
3.  stacksterflow
4.  stackstarflow
5.  stackstacflow
6.  stackstacklow
7.  stackstacksow
8.  stackstackstw

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

Просто из стандарта 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() делает.

Есть два очевидных способа реализации 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 к какому-то конкретному типу указателя они (вообще) не указывают на один и тот же объект и поэтому не могут сравниваться. Но у стандартной библиотеки может быть достаточно знаний о платформе, чтобы выполнить такое сравнение, не вызывая неопределенного поведения.


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

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

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

Я попытался запустить приведенный выше код: Основная причина, по которой я хочу узнать разницу memcpy а также memmove,

#include <stdio.h>
#include <string.h>

int main (void)
{
    char string [] = "stackru";

    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;
}

дает ниже вывод

stackru
stackstackovw
stackstackstw

Теперь я заменил memmove с memcpy и я получил такой же вывод

#include <stdio.h>
#include <string.h>

int main (void)
{
    char string [] = "stackru";

    char *first, *second;

    first = string;

    second = string;

    puts(string);

    memcpy(first+5, first,7);

    puts(first);

    memcpy(second+5, second, 7);

    puts(second);

    return 0;
}

Выход:

stackru
stackstackovw
stackstackstw

char string [] = "stackru";

char *first, *second;

first = string;

second = string;

puts(string);

o / p - переполнение стека

memcpy(first+5, first,7);

puts(first);

Здесь 7 символов, обозначенных вторым, т.е. "стеков", вставленных в +5 позиции, в результате

o/p - stackstackovw

memcpy(second+5, second, 7);

puts(second);

здесь входная строка "stackstackovw", 7 символов, на которые указывает секунда (то есть "stackst"), копируются в буфер, а затем вставляются в место +5, в результате

o / p - stackstackstw

0 ----- + 5
стек стек

Техническое различие было хорошо задокументировано предыдущими ответами. Также следует отметить практические различия:

Memcpy потребуется примерно вдвое больше памяти для выполнения той же задачи, но memmove может занять значительно больше времени, чем memcpy.

Пример:

Скажем, у вас есть список из 100 элементов в памяти, занимающий 100 МБ памяти. Вы хотите бросить 1-й предмет, чтобы у вас было только 99.

Memcpy потребует исходные 100 МБ и дополнительные 99 МБ для вашего нового списка из 99 элементов. Для выполнения операции требуется примерно 199 МБ, но это должно быть очень быстро.

Memmove, в худшем сценарии потребуются исходные 100 МБ и будет перемещать каждый элемент памяти на 1 адрес за раз. Это требует только оригинальных 100 МБ, но будет значительно медленнее, чем Memcpy.

Конечно, создание нового указателя, указывающего на 2-й элемент в вашем списке, приведет к тому же эффекту "удаления" первого элемента из вашего списка, но пример хорошо показывает различия в memcpy и memmove.

- РЕДАКТИРОВАТЬ, ЧТОБЫ ПОМОЧЬ УТОЧНИТЬ МОЙ КРЕПИ ОТВЕТ
Да, реализации memcpy() и memmove(), вероятно, не отличаются по использованию памяти (я действительно не знаю), но то, как вы их используете, сильно повлияет на использование памяти вашей программой. Вот что я имел в виду под практическими различиями memcpy() и memmove().

int SIZE = 100;
Item *ptr_item = (Item *) malloc(size_of(Item) * SIZE);
Item *ptr_src_item = ptr_item + 1;
Item *ptr_dst_item = ptr_item;
memmove(ptr_dst_item, ptr_src_item, SIZE - 1);

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

int SIZE = 100;
Item *ptr_src_item = (Item *) malloc(size_of(Item) * SIZE);
Item *ptr_dst_item = (Item *) malloc(size_of(Item) * (SIZE - 1));
memcpy(ptr_dst_item, ptr_src_item, SIZE - 1);

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

Вот как я пытался объяснить практические различия в этих двух... но, возможно, я тоже не прав?

Другие вопросы по тегам