Dlaczego warto używać bzero zamiast memset?

156

Na zajęciach z programowania systemów, w których brałem udział w poprzednim semestrze, musieliśmy zaimplementować podstawowy klient / serwer w języku C. Podczas inicjowania struktur, takich jak sock_addr_inlub bufory znaków (których używaliśmy do przesyłania danych między klientem a serwerem) profesor poinstruował nas, abyśmy tylko ich używali, bzeroa nie memsetinicjowali. Nigdy nie wyjaśnił dlaczego, a jestem ciekawy, czy jest ku temu ważny powód?

Widzę tutaj: http://fdiv.net/2009/01/14/memset-vs-bzero-ultimate-showdown, który bzerojest bardziej wydajny ze względu na fakt, że zawsze będzie zerował pamięć, więc nie trzeba będzie wykonać dodatkowe czynności sprawdzające memset. To jednak niekoniecznie wydaje się być powodem, aby absolutnie nie używać memsetdo zerowania pamięci.

bzerojest uważany za przestarzały, a ponadto nie jest standardową funkcją C. Zgodnie z instrukcją, memsetjest preferowany z bzerotego powodu. Dlaczego więc chcesz nadal używać bzeroover memset? Tylko ze względu na wzrost wydajności, czy może to coś więcej? Podobnie, jakie są korzyści z memsetponad bzeroktóre sprawiają, że de facto korzystną opcją dla nowych programów?

PseudoPsyche
źródło
28
„Po co używać bzero zamiast memset?” - Nie. Memset jest standardem, bzero nie.
30
bzero to BSDism (). memset () jest ansi-c. obecnie bzero () zostanie prawdopodobnie zaimplementowane jako makro. Poproś profesora, aby się ogolił i przeczytał kilka książek. efektywność to fałszywy argument. Wywołanie systemowe lub przełącznik kontekstu może z łatwością kosztować dziesiątki tysięcy taktów zegara, a jedno przejście przez bufor działa z prędkością magistrali. Jeśli chcesz zoptymalizować programy sieciowe: zminimalizuj liczbę wywołań systemowych (czytając / pisząc większe fragmenty)
wildplasser
7
Pomysł, który memsetmoże być nieco mniej wydajny z powodu „trochę więcej sprawdzania”, jest zdecydowanie przypadkiem przedwczesnej optymalizacji: jakiekolwiek korzyści, które możesz zobaczyć z pominięcia jednej lub dwóch instrukcji procesora, nie są tego warte, gdy możesz zagrozić przenośności kod. bzerojest przestarzały i to wystarczający powód, aby go nie używać.
dasblinkenlight
4
Często można zamiast tego dodać inicjator `= {0}` i w ogóle nie wywoływać funkcji. Stało się to łatwiejsze, gdy na przełomie XIX i XX wieku C przestał wymagać uprzedniej deklaracji zmiennych lokalnych. Jednak niektóre naprawdę stare papierowe naczynia wciąż tkwią głęboko w poprzednim stuleciu.
MSalters
1
@SSAnne nie, ale najprawdopodobniej pochodzi z książki polecanej na kurs, na który miał wpływ, jak wspomniano w jednej z poniższych odpowiedzi: stackoverflow.com/a/17097072/1428743
PseudoPsyche

Odpowiedzi:

152

Nie widzę żadnego powodu, aby preferować bzeronad memset.

memsetjest standardową funkcją C, chociaż bzeronigdy nie była standardową funkcją C. Powodem jest prawdopodobnie to, że dokładnie tę samą funkcjonalność można osiągnąć za pomocą memsetfunkcji.

Jeśli chodzi o wydajność, kompilatory, na przykład, gccużywają wbudowanych implementacji, dla memsetktórych przełączają się na określoną implementację po 0wykryciu stałej . To samo, glibcgdy wbudowane są wyłączone.

ouah
źródło
Dzięki. To ma sens. Byłem prawie pewien, że memsetpowinno to być zawsze używane w tym przypadku, ale byłem zdezorientowany, dlaczego go nie używamy. Dziękuję za wyjaśnienie i potwierdzenie moich przemyśleń.
PseudoPsyche
1
Miałem wiele problemów z zepsutymi bzeroimplementacjami. W tablicach nie wyrównanych używał do przekroczenia podanej długości i wyzerowania trochę więcej bajtów. Nigdy nie miałem takiego problemu po przejściu na memset.
rustyx
Nie zapominaj o tym, memset_sktóre powinno być użyte, jeśli chcesz mieć pewność, że kompilator nie optymalizuje po cichu - usuwa wywołanie „wyczyszczenia” pamięci w jakimś celu związanym z bezpieczeństwem (takim jak wygaszenie obszaru pamięci zawierającego wrażliwy informacje, takie jak hasło w postaci zwykłego tekstu).
Christopher Schultz
69

Domyślam się, że użyłeś (lub twój nauczyciel był pod wpływem) programowania sieciowego UNIX autorstwa W. Richarda Stevensa. Często używa bzerozamiast memset, nawet w najbardziej aktualnym wydaniu. Książka jest tak popularna, że ​​wydaje mi się, że stała się idiomem w programowaniu sieciowym i dlatego nadal ją używasz.

Trzymałbym się memsetpo prostu dlatego, że bzerojest przestarzały i ogranicza przenośność. Wątpię, żebyś dostrzegł jakiekolwiek korzyści z używania jednego nad drugim.

austin
źródło
4
Miałbyś rację. Nie mieliśmy wymaganych podręczników do tego kursu, ale właśnie ponownie sprawdziłem sylabus i programowanie sieciowe w UNIX jest rzeczywiście wymienione jako zasób opcjonalny. Dzięki.
PseudoPsyche
9
Właściwie jest gorzej. Był przestarzały w POSIX.1-2001 i usunięty w POSIX.1-2008.
paxdiablo
9
Cytując stronę 8 trzeciej edycji UNIX Network Programming autorstwa W. Richarda Stevensa - Rzeczywiście, autor TCPv3 popełnił błąd zamieniając drugi i trzeci argument na memset w 10 wystąpieniach pierwszego wydruku. Kompilator AC nie może wychwycić tego błędu, ponieważ oba wystąpienia są takie same ... był to błąd i można go było uniknąć używając bzero, ponieważ zamiana dwóch argumentów na bzero zawsze zostanie przechwycona przez kompilator C, jeśli używane są prototypy funkcji. Jednak, jak wskazał paxdiablo, bzero jest przestarzałe.
Aaron Newton,
@AaronNewton, powinieneś dodać to do odpowiedzi Michaela, ponieważ potwierdza to, co powiedział.
Synetech
52

Jedna zaletę, że myślę, że bzero()ma ponad memset()ustawiania pamięć do zera, jest to, że istnieje zmniejszenia możliwości pomyłek.

Nieraz natrafiłem na błąd, który wyglądał tak:

memset(someobject, size_of_object, 0);    // clear object

Kompilator nie będzie narzekał (chociaż może w niektórych kompilatorach podniesienie niektórych poziomów ostrzegawczych), a efekt będzie taki, że pamięć nie zostanie wyczyszczona. Ponieważ nie powoduje to niszczenia obiektu - po prostu pozostawia go w spokoju - istnieje spora szansa, że ​​błąd nie przejawi się w nic oczywistego.

Fakt, że bzero()nie jest to standard, jest trochę drażniący. (FWIW, nie zdziwiłbym się, gdyby większość wywołań funkcji w moich programach była niestandardowa; w rzeczywistości pisanie takich funkcji to rodzaj mojej pracy).

W komentarzu do innej odpowiedzi tutaj Aaron Newton zacytował następujący fragment z Unix Network Programming, tom 1, wydanie trzecie, Stevens, et al., Sekcja 1.2 (wyróżnienie dodane):

bzeronie jest funkcją ANSI C. Wywodzi się z wczesnego kodu sieciowego Berkely. Niemniej jednak używamy go w całym tekście zamiast funkcji ANSI C memset, ponieważ bzerojest łatwiejszy do zapamiętania (tylko z dwoma argumentami) niż memset(z trzema argumentami). Prawie każdy dostawca obsługujący interfejs API gniazd również udostępnia bzero, a jeśli nie, podajemy definicję makra w naszym unp.hnagłówku.

Rzeczywiście, autor TCPv3 [TCP / IP Illustrated, tom 3 - Stevens 1996] popełnił błąd, zamieniając drugi i trzeci argument na memset10 wystąpień w pierwszym druku . Kompilator AC nie może wychwycić tego błędu, ponieważ oba argumenty są tego samego typu. (W rzeczywistości drugim argumentem jest an, inta trzecim argumentem jest size_t, zwykle an unsigned int, ale podane wartości, odpowiednio, 0 i 16, są nadal dopuszczalne dla innego typu argumentu.) Wywołanie do memsetnadal działało, ponieważ tylko a kilka funkcji gniazd faktycznie wymaga, aby ostatnie 8 bajtów struktury adresu gniazda internetowego było ustawione na 0. Niemniej jednak był to błąd, którego można było uniknąć używając bzero, ponieważ zamiana dwóch argumentów na bzerozawsze zostanie przechwycona przez kompilator C, jeśli używane są prototypy funkcji.

Uważam również, że zdecydowana większość wywołań memset()ma zerową pamięć, więc dlaczego nie użyć interfejsu API, który jest dostosowany do tego przypadku użycia?

Możliwą wadą bzero()jest to, że kompilatory mogą być bardziej skłonne do optymalizacji, memcpy()ponieważ są standardowe, więc mogą zostać napisane, aby je rozpoznawać. Należy jednak pamiętać, że poprawny kod jest nadal lepszy niż nieprawidłowy kod, który został zoptymalizowany. W większości przypadków użycie bzero()nie spowoduje zauważalnego wpływu na wydajność programu, a bzero()może to być makro lub funkcja wbudowana, która rozwija się do memcpy().

Michael Burr
źródło
Tak, przypuszczam, że może to być uzasadnienie podczas pracy w klasie, takiej jak ta, aby uczynić ją potencjalnie mniej zagmatwaną dla uczniów. Nie sądzę jednak, żeby tak było w przypadku mojego profesora. Był bardzo dużym nauczycielem RTFM. Gdybyś miał pytanie, na które można by odpowiedzieć w podręczniku, przywoływałby strony man na projektorze w klasie i pokazywał ci. Bardzo zależało mu na tym, aby wszyscy myśleli, że podręcznik jest po to, aby je przeczytać i zawiera odpowiedzi na większość pytań. Jestem za to wdzięczny, w przeciwieństwie do niektórych innych profesorów.
PseudoPsyche
5
Myślę, że jest to argument, który można sformułować nawet poza salą lekcyjną - widziałem ten błąd w kodzie produkcyjnym. Wydaje mi się, że łatwo popełnić błąd. Przypuszczam również, że zdecydowana większość memset()wywołań ma na celu po prostu wyzerowanie bloku pamięci, co moim zdaniem jest kolejnym argumentem za bzero(). Co właściwie oznacza „b” bzero()?
Michael Burr
7
+1. To memsetnarusza wspólną kolejność parametrów „buffer, buffer_size”, co czyni go szczególnie podatnym na błędy IMO.
jamesdlin
W Pascalu unikają tego nazywając to „fillchar” i zajmuje char. Większość kompilatorów C / C ++ by to wybrała. Co sprawia, że ​​zastanawiam się, dlaczego kompilatory nie mówią „przekazujesz wskaźnik 32/64 bitowy tam, gdzie oczekiwany jest bajt” i nie dają ci rady w błędach kompilatora.
Móż
1
@Gewure, drugi i trzeci argument są w złej kolejności; cytowane wywołanie funkcji nie robi dokładnie nic
Ichthyo
4

Chciałem wspomnieć o argumentach bzero vs. memset. Zainstaluj ltrace i porównaj co robi pod maską. W systemie Linux z libc6 (2.19-0ubuntu6.6) wykonywane wywołania są dokładnie takie same (przez ltrace ./test123):

long m[] = {0}; // generates a call to memset(0x7fffefa28238, '\0', 8)
int* p;
bzero(&p, 4);   // generates a call to memset(0x7fffefa28230, '\0', 4)

Powiedziano mi, że jeśli nie pracuję w głębokich trzewiach libc lub dowolnej liczbie interfejsów jądra / syscall, nie muszę się o nie martwić. Jedyne, o co powinienem się martwić, to to, że wywołanie spełnia warunek zerowania bufora. Inni wspominali o tym, który jest lepszy od drugiego, więc zatrzymam się tutaj.

gryzaki
źródło
Dzieje się tak, ponieważ niektóre wersje GCC będą emitować kod, memset(ptr, 0, n)gdy zobaczą, bzero(ptr, n)i nie mogą go przekonwertować na kod wbudowany.
zwolniony
@zwol To właściwie makro.
SS Anne
1
@SSAnne gcc 9.3 na moim komputerze dokonuje tej transformacji samodzielnie, bez żadnej pomocy makr w nagłówkach systemowych. extern void bzero(void *, size_t); void clear(void *p, size_t n) { bzero(p, n); }tworzy wywołanie memset. (Dołącz stddef.hdo size_tbez niczego innego, co mogłoby kolidować.)
Zwol
4

Prawdopodobnie nie powinieneś używać bzero, to właściwie nie jest standardowe C, to była rzecz POSIX.

Zauważ, że słowo „było” - zostało uznane za przestarzałe w POSIX.1-2001 i usunięte w POSIX.1-2008 ze względu na memset, więc lepiej jest używać standardowej funkcji C.

paxdiablo
źródło
Co masz na myśli mówiąc o standardowym C? Masz na myśli to, że nie ma go w standardowej bibliotece C?
Koray Tugay
@Koray, standard C oznacza normę ISO i tak, bzeronie jest jej częścią.
paxdiablo
Nie, to znaczy, nie wiem, co masz na myśli przez żaden standard. Czy standard ISO oznacza standardową bibliotekę C? To przychodzi z językiem? Minimalna biblioteka, o której wiemy, że tam będzie?
Koray Tugay
2
@Koray, ISO jest organizacją normalizacyjną odpowiedzialną za normę C, obecnie C11, a wcześniejszymi C99 i C89. Określają zasady, których musi przestrzegać implementacja, aby została uznana za C. Więc tak, jeśli norma mówi, że implementacja musi zapewniać memset, będzie on dostępny dla Ciebie. W przeciwnym razie nie jest to C.
paxdiablo
2

Dla funkcji memset drugim argumentem jest an, inta trzecim argumentem jest size_t,

void *memset(void *s, int c, size_t n);

co zwykle jest an unsigned int, ale jeśli wartości takie jak, odpowiednio, 0 and 16dla drugiego i trzeciego argumentu zostaną wprowadzone w niewłaściwej kolejności, jako 16 i 0, to takie wywołanie memset może nadal działać, ale nic nie da. Ponieważ liczba bajtów do zainicjowania jest określona jako 0.

void bzero(void *s, size_t n)

Takiego błędu można uniknąć używając bzero, ponieważ zamiana dwóch argumentów na bzero zawsze zostanie przechwycona przez kompilator C, jeśli używane są prototypy funkcji.

havish
źródło
1
Takiego błędu można również uniknąć dzięki memset, jeśli po prostu pomyślisz o wywołaniu jako „ustaw tę pamięć na tę wartość dla tego rozmiaru” lub jeśli masz IDE, które daje Ci prototyp lub nawet jeśli po prostu wiesz, kim jesteś robi :-)
paxdiablo
Zgadzam się, ale ta funkcja powstała w czasie, gdy takie inteligentne IDE nie były dostępne do wsparcia.
havish
2

Krótko mówiąc: memset wymagają więcej operacji montażowych bzero.

To jest źródło: http://fdiv.net/2009/01/14/memset-vs-bzero-ultimate-showdown

Tal Bar
źródło
Tak, to jedna rzecz, o której wspomniałem w PO. Właściwie to nawet podałem link do tej dokładnej strony. Okazuje się, że nie ma to większego znaczenia ze względu na pewne optymalizacje kompilatora. Aby uzyskać więcej informacji, zobacz zaakceptowaną odpowiedź od ouah.
PseudoPsyche
6
To tylko pokazuje, że jedna kiepska implementacja memset jest powolna. W systemie MacOS X i niektórych innych systemach, memset używa kodu, który jest ustawiany podczas rozruchu w zależności od używanego procesora, w pełni wykorzystuje rejestry wektorowe, aw przypadku dużych rozmiarów używa instrukcji pobierania wstępnego w sprytny sposób, aby uzyskać ostatni bit prędkości.
gnasher729
mniej instrukcji nie oznacza szybszego wykonania. W rzeczywistości optymalizacje często zwiększają rozmiar binarny i liczbę instrukcji z powodu rozwijania pętli, wstawiania funkcji, wyrównania pętli ... Spójrz na jakikolwiek przyzwoicie zoptymalizowany kod, a zobaczysz, że często zawiera on znacznie więcej instrukcji niż gówniane implementacje
phuclv
2

Zrób to, jak chcesz. :-)

#ifndef bzero
#define bzero(d,n) memset((d),0,(n))
#endif

Zauważ, że:

  1. Oryginał bzeronic nie zwraca, memsetzwraca void pointer ( d). Można to naprawić, dodając rzutowanie typu do void w definicji.
  2. #ifndef bzeronie zapobiega ukryciu oryginalnej funkcji, nawet jeśli istnieje. Testuje istnienie makra. Może to spowodować wiele zamieszania.
  3. Niemożliwe jest utworzenie wskaźnika funkcji do makra. W przypadku używania bzerowskaźników funkcji to nie zadziała.
Bruce
źródło
1
Jaki jest z tym problem, @Leeor? Ogólna niechęć do makr? A może nie podoba ci się fakt, że to makro można pomylić z funkcją (a może nawet ją ukryć)?
Palec
1
@Palec, ten drugi. Ukrywanie przedefiniowania jako makro może prowadzić do wielu nieporozumień. Inny programista używający tego kodu sądzi, że używa jednej rzeczy i jest nieświadomie zmuszony do użycia drugiej. To bomba zegarowa.
Leeor
1
Po ponownym przemyśleniu zgadzam się, że jest to rzeczywiście złe rozwiązanie. Między innymi znalazłem techniczny powód: podczas używania bzerowskaźników funkcji to nie zadziała.
Palec
Naprawdę powinieneś był nazwać swoje makro inaczej niż bzero. To jest okrucieństwo.
Dan Bechard
-2

memset przyjmuje 3 parametry, bzero zajmuje 2 w pamięci ograniczone, że dodatkowy parametr zajmie 4 bajty więcej i przez większość czasu będzie używany do ustawiania wszystkiego na 0

Skynight
źródło