Jaka jest różnica między memmove i memcpy?

120

Jaka jest różnica między memmovei memcpy? Którego zazwyczaj używasz i jak?

Społeczność
źródło
Zwróć uwagę na problemy, które mogą się pojawić: lwn.net/Articles/414467
Zan Lynx

Odpowiedzi:

162

W memcpyprzypadku miejsce docelowe nie może w ogóle pokrywać się ze źródłem. Dzięki memmovetemu może. Oznacza to, że memmovemoże to być nieco wolniejsze niż memcpy, ponieważ nie może przyjąć takich samych założeń.

Na przykład memcpymoże zawsze kopiować adresy od niskiego do wysokiego. Jeśli miejsce docelowe nakłada się na źródło, oznacza to, że niektóre adresy zostaną nadpisane przed skopiowaniem. memmovewykryje to i skopiuje w innym kierunku - od wysokiego do niskiego - w tym przypadku. Jednak sprawdzenie tego i przejście na inny (prawdopodobnie mniej wydajny) algorytm zajmuje trochę czasu.

bdonlan
źródło
1
podczas korzystania z memcpy, jak mogę zagwarantować, że adresy src i dest nie pokrywają się? Czy powinienem osobiście upewnić się, że src i dest nie nakładają się na siebie?
Alcott,
6
@Alcott, nie używaj memcpy, jeśli nie wiesz, że się nie nakładają - zamiast tego użyj memmove. Gdy nie ma nakładania się, memmove i memcpy są równoważne (chociaż memcpy może być bardzo, bardzo, bardzo nieznacznie szybsze).
bdonlan,
Możesz użyć słowa kluczowego „ogranicz”, jeśli pracujesz z długimi tablicami i chcesz zabezpieczyć proces kopiowania. Na przykład, jeśli metoda przyjmuje jako parametry tablice wejściowe i wyjściowe i musisz sprawdzić, czy użytkownik nie przekazuje tego samego adresu co wejście i wyjście. Przeczytaj więcej tutaj stackoverflow.com/questions/776283/…
DanielHsH
10
@DanielHsH 'ogranicz' to obietnica, którą składasz kompilatorowi; nie jest wymuszane przez kompilator. Jeśli ustawisz `` ogranicz '' na swoich argumentach i faktycznie pokrywają się one (lub, bardziej ogólnie, uzyskasz dostęp do zastrzeżonych danych ze wskaźnika pochodzącego z wielu miejsc), zachowanie programu jest niezdefiniowane, pojawią się dziwne błędy, a kompilator zazwyczaj Cię o tym nie ostrzeże.
bdonlan
@bdonlan To nie tylko obietnica dla kompilatora, to wymóg dla twojego rozmówcy. Jest to wymóg, który nie jest egzekwowany, ale jeśli go naruszysz, nie możesz narzekać, jeśli uzyskasz nieoczekiwane wyniki. Naruszenie wymagania jest niezdefiniowanym zachowaniem, tak samo jak i = i++ + 1niezdefiniowanym; kompilator nie zabrania ci pisania dokładnie tego kodu, ale wynikiem tej instrukcji może być cokolwiek, a różne kompilatory lub procesory pokażą tutaj różne wartości.
Mecki,
33

memmoveradzi sobie z nakładaniem się pamięci, memcpynie może.

Rozważać

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

Oczywiście źródło i miejsce docelowe nakładają się teraz, nadpisujemy „-bar” na „bar”. Jest to niezdefiniowane zachowanie, memcpygdy źródło i miejsce docelowe nakładają się, więc w tym przypadku potrzebujemy memmove.

memmove(&str[3],&str[4],4); //fine
nr
źródło
5
dlaczego najpierw wybuchł?
4
@ultraman: Ponieważ MOŻE zostać zaimplementowane przy użyciu kompilacji niskiego poziomu, która wymaga, aby pamięć się nie nakładała. Jeśli tak, możesz na przykład wygenerować sygnał lub wyjątek sprzętowy dla procesora, który przerywa aplikację. Dokumentacja określa, że ​​nie obsługuje warunku, ale standard nie określa, co się stanie, gdy te warunki zostaną uwzględnione (jest to znane jako niezdefiniowane zachowanie). Niezdefiniowane zachowanie może zrobić wszystko.
Martin York
z gcc 4.8.2, nawet memcpy akceptuje nakładające się wskaźniki źródłowe i docelowe i działa dobrze.
GeekyJ
4
@jagsgediya Jasne, że tak. Ale ponieważ memcpy jest udokumentowane, że nie obsługuje tego, nie powinieneś polegać na tym specyficznym zachowaniu implementacyjnym, dlatego istnieje memmove (). W innej wersji gcc może być inaczej. Może być inaczej, jeśli gcc wstawia memcpy zamiast wywoływać memcpy () w glibc, może być inaczej w starszej lub nowszej wersji glibc i tak dalej.
nr
Z praktyki wynika, że ​​memcpy i memmove zrobiły to samo. Takie głębokie, niezdefiniowane zachowanie.
Życie
22

Ze strony man memcpy .

Funkcja memcpy () kopiuje n bajtów z obszaru pamięci src do obszaru pamięci dest. Obszary pamięci nie powinny się pokrywać. Użyj memmove (3), jeśli obszary pamięci się pokrywają.

John Carter
źródło
12

Główną różnicą pomiędzy memmove()i memcpy()jest to, że w bufor - pamięć tymczasowa - jest używany, więc nie ma ryzyka nakładania. Z drugiej strony bezpośrednio kopiuje dane z lokalizacji wskazanej przez źródło do lokalizacji wskazanej przez cel . ( http://www.cplusplus.com/reference/cstring/memcpy/ )memmove()memcpy()

Rozważ następujące przykłady:

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

    Zgodnie z oczekiwaniami wydrukuje się:

    stackoverflow
    stackstacklow
    stackstacklow
  2. Ale w tym przykładzie wyniki nie będą takie same:

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

    Wynik:

    stackoverflow
    stackstackovw
    stackstackstw

Dzieje się tak, ponieważ „memcpy ()” wykonuje następujące czynności:

1.  stackoverflow
2.  stacksverflow
3.  stacksterflow
4.  stackstarflow
5.  stackstacflow
6.  stackstacklow
7.  stackstacksow
8.  stackstackstw
Mirac Suzgun
źródło
2
Ale wygląda na to, że dane wyjściowe, o których wspomniałeś, są odwrócone!
kumar
1
Kiedy uruchamiam ten sam program, otrzymuję następujący wynik: stackoverflow stackstackstw stackstackstw // oznacza, że ​​nie ma różnicy w danych wyjściowych między memcpy i memmove
kumar
4
"to że w" memmove () "używany jest bufor - pamięć tymczasowa;" To nie jest prawda. mówi „jak gdyby”, więc po prostu musi się tak zachowywać, a nie, żeby tak było. Jest to rzeczywiście istotne, ponieważ większość implementacji memmove wykonuje po prostu zamianę XOR.
dhein
2
Nie sądzę, aby implementacja memmove()była wymagana do korzystania z bufora. Jest całkowicie uprawniony do przenoszenia w miejscu (o ile każdy odczyt kończy się przed jakimkolwiek zapisem na ten sam adres).
Toby Speight
12

Zakładając, że musiałbyś zaimplementować oba, implementacja mogłaby wyglądać tak:

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

I to powinno całkiem dobrze wyjaśniać różnicę. memmovezawsze kopiuje w taki sposób, że nadal jest bezpieczne srci dstzachodzi na siebie, ale memcpypo prostu nie obchodzi, jak mówi dokumentacja podczas używania memcpy, oba obszary pamięci nie mogą się pokrywać.

Np. Jeśli memcpykopiuje "od przodu do tyłu" i bloki pamięci są wyrównane w ten sposób

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

skopiowanie pierwszego bajtu srcdo dstjuż niszczy zawartość ostatnich bajtów srcsprzed ich skopiowania. Tylko kopiowanie „od tyłu do przodu” da poprawne wyniki.

Teraz zamień srci dst:

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

W takim przypadku kopiowanie „od przodu do tyłu” jest bezpieczne, ponieważ kopiowanie „od tyłu do przodu” spowodowałoby zniszczenie w srcpobliżu przodu już podczas kopiowania pierwszego bajtu.

Być może zauważyłeś, że memmovepowyższa implementacja nawet nie testuje, czy faktycznie pokrywają się, po prostu sprawdza ich względne pozycje, ale samo to sprawi, że kopia będzie bezpieczna. Jak memcpyzwykle używa najszybszego możliwego sposobu kopiowania pamięci w dowolnym systemie, memmovejest zwykle raczej implementowany jako:

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

Czasami, jeśli memcpyzawsze kopiuje „od przodu do tyłu” lub „od tyłu do przodu”, memmovemoże również użyć memcpyw jednym z nakładających się przypadków, ale memcpymoże nawet skopiować w inny sposób w zależności od tego, jak dane są wyrównane i / lub ile danych ma być skopiowane, więc nawet jeśli przetestowałeś, jak memcpykopiuje się w twoim systemie, nie możesz polegać na tym, że wynik testu będzie zawsze poprawny.

Co to oznacza dla Ciebie, gdy decydujesz, do kogo zadzwonić?

  1. O ile nie masz pewności, że to się nie pokrywa srci dstnie nakładasz się na siebie, zadzwoń, memmoveponieważ zawsze prowadzi to do poprawnych wyników i zwykle jest tak szybkie, jak to możliwe w przypadku kopii, której potrzebujesz.

  2. Jeśli wiesz na pewno srci dstnie nakładasz się na siebie, zadzwoń, memcpyponieważ nie ma znaczenia, który z nich wywołasz, oba będą działać poprawnie w tym przypadku, ale memmovenigdy nie będą szybsze niż, memcpya jeśli masz pecha, może nawet wolniej, więc możesz wygrać tylko dzwonienie memcpy.

Mecki
źródło
+1, ponieważ Twoje „rysunki ascii” były przydatne, aby zrozumieć, dlaczego nie mogą się pokrywać bez uszkodzenia danych
Scylardor,
10

Jeden obsługuje nakładające się miejsca docelowe, a drugi nie.

KPexEA
źródło
6

po prostu z normy ISO / IEC: 9899 jest dobrze opisany.

7.21.2.1 Funkcja memcpy

[…]

2 Funkcja memcpy kopiuje n znaków z obiektu wskazywanego przez s2 do obiektu wskazywanego przez s1. Jeśli kopiowanie odbywa się między nakładającymi się obiektami, zachowanie jest niezdefiniowane.

I

7.21.2.2 Funkcja memmove

[…]

2 Funkcja memmove kopiuje n znaków z obiektu wskazywanego przez s2 do obiektu wskazywanego przez s1. Kopiowanie odbywa się tak, jakby n znaków z obiektu wskazywanego przez s2 było najpierw kopiowanych do tymczasowej tablicy n znaków, która nie pokrywa się z obiektami wskazywanymi przez s1 i s2, a następnie n znaków z tymczasowej tablicy jest kopiowanych do obiekt wskazywany przez s1.

Który z nich zwykle używam zgodnie z pytaniem, zależy od potrzebnej funkcjonalności.

W zwykłym tekście memcpy()nie pozwala s1i s2nakłada się, podczas gdy memmove()nie.

dhein
źródło
0

Istnieją dwa oczywiste sposoby implementacji mempcpy(void *dest, const void *src, size_t n)(ignorowanie zwracanej wartości):

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

W pierwszej implementacji kopia przechodzi od niskiego do wysokiego adresu, aw drugiej od wysokiego do niskiego. Jeśli zakres do skopiowania nakłada się (jak ma to miejsce na przykład podczas przewijania bufora ramki), to tylko jeden kierunek operacji jest poprawny, a drugi nadpisuje lokalizacje, z których będą później odczytywane.

memmove()Wdrożenie, w swojej najprostszej, przetestuje dest<src(w jakiś sposób zależne od platformy) i wykonać odpowiedni kierunek memcpy().

Kod użytkownika nie może tego oczywiście zrobić, ponieważ nawet po rzutowaniu srci dstdo konkretnego typu wskaźnika nie wskazują (na ogół) tego samego obiektu, więc nie można ich porównać. Ale standardowa biblioteka może mieć wystarczającą wiedzę o platformie, aby wykonać takie porównanie bez powodowania niezdefiniowanego zachowania.


Należy pamiętać, że w rzeczywistości implementacje są znacznie bardziej złożone, aby uzyskać maksymalną wydajność z większych transferów (jeśli pozwala na to wyrównanie) i / lub dobre wykorzystanie pamięci podręcznej danych. Powyższy kod ma na celu uproszczenie sprawy.

Toby Speight
źródło
0

memmove radzi sobie z nakładającymi się regionami źródłowymi i docelowymi, podczas gdy memcpy nie. Spośród tych dwóch memcpy jest znacznie wydajniejszy. Więc lepiej UŻYWAJ memcpy, jeśli możesz.

Źródła : https://www.youtube.com/watch?v=Yr1YnOVG-4g Dr Jerry Cain, (Wykład Stanford Intro Systems - 7) Godz .: 36:00

Ehsan
źródło
Ta odpowiedź mówi „prawdopodobnie odrobinę szybciej” i dostarcza danych ilościowych wskazujących tylko na niewielką różnicę. Ta odpowiedź stwierdza, że ​​jest się „znacznie bardziej wydajnym”. O ile wydajniejszy okazał się być szybszy? BTW: Zakładam, że masz na myśli memcpy()i nie memcopy().
chux - Przywróć Monikę
Komentarz jest oparty na wykładzie dr Jerry'ego Caina. Proszę o wysłuchanie jego wykładu o godzinie 36:00, wystarczą tylko 2-3 minuty. Dzięki za połów. : D
Ehsan