Was ist der Vorteil der Verwendung von memset () in C?

8

Ich war neugierig, ob es einen Vorteil in Bezug auf die Effizienz bei der Verwendung von memset () in einer ähnlichen Situation wie der folgenden gibt.

Gegeben die folgenden Pufferdeklarationen ...

%Vor%

Abgesehen davon, dass Sie weniger Codezeilen haben, ist es von Vorteil, dies zu verwenden:

%Vor%

Darüber hinaus:

%Vor%     
embedded_guy 16.12.2011, 00:50
quelle

6 Antworten

23

Dies gilt sowohl für memset() als auch für memcpy() :

  1. Weniger Code: Wie Sie bereits erwähnt haben, ist es kürzer - weniger Codezeilen.
  2. Besser lesbar: Kürzlicher macht es normalerweise auch lesbarer. ( memset() ist lesbarer als diese Schleife)
  3. Es kann schneller sein: Es kann manchmal aggressivere Compiler-Optimierungen zulassen. (so kann es schneller sein)
  4. Fehlausrichtung: In einigen Fällen, wenn Sie mit fehlausgerichteten Daten auf einem Prozessor arbeiten, der fehlausgerichtete Zugriffe nicht unterstützt, können memset() und memcpy() die einzige saubere Lösung sein.

Um den dritten Punkt zu erweitern, kann memset() vom Compiler mit SIMD und ähnlichem stark optimiert werden. Wenn Sie stattdessen eine Schleife schreiben, muss der Compiler zuerst herausfinden, was er tut, bevor er versuchen kann, ihn zu optimieren.

Die Grundidee ist hier, dass memset() und ähnliche Bibliotheksfunktionen dem Compiler in gewissem Sinne Ihre Absicht "sagen".

Wie von @Oli in den Kommentaren erwähnt, gibt es einige Nachteile. Ich werde sie hier erweitern:

  1. Sie müssen sicherstellen, dass memset() tatsächlich das tut, was Sie wollen. Der Standard sagt nicht, dass Nullen für die verschiedenen Datentypen notwendigerweise Null im Speicher sind.
  2. Für Daten ungleich Null ist memset() auf nur 1 Byte Inhalt beschränkt. Sie können also memset() nicht verwenden, wenn Sie ein Array von int s auf etwas anderes als null (oder 0x01010101 oder etwas ...) setzen möchten.
  3. Obwohl es selten ist, gibt es einige Fälle, in denen es tatsächlich möglich ist, den Compiler mit einer eigenen Schleife zu schlagen. *

* Ich gebe ein Beispiel aus meiner Erfahrung:

Obwohl memset() und memcpy() in der Regel Compiler-Intrinsics mit spezieller Behandlung durch den Compiler sind, sind sie immer noch generische Funktionen. Sie sagen nichts über den Datentyp einschließlich der Ausrichtung der Daten.

In einigen wenigen Fällen ist der Compiler daher nicht in der Lage, die Ausrichtung des Speicherbereichs zu bestimmen, und er muss daher zusätzlichen Code zur Behandlung von Fehlausrichtungen erzeugen. Während, wenn Sie als Programmierer 100% sicher sind, dass die Ausrichtung funktioniert, ist die Verwendung einer Schleife möglicherweise schneller.

Ein gängiges Beispiel ist die Verwendung von SSE / AVX-Eigen- schaften. (z. B. Kopieren eines 16/32-Byte ausgerichteten Arrays von float s) Wenn der Compiler die 16/32-Byte-Ausrichtung nicht bestimmen kann, muss er fehlausgerichtete Lade- / Speicher- und / oder Verarbeitungscodes verwenden. Wenn Sie einfach eine Schleife schreiben, die SSE / AVX ausgerichtete Lade / Speicher-Eigen- schaften verwendet, können Sie wahrscheinlich besser machen.

%Vor%     
Mysticial 16.12.2011, 00:54
quelle
7

Es hängt von der Qualität des Compilers und der Bibliotheken ab. In den meisten Fällen ist memset überlegen.

Der Vorteil von memset ist, dass es in vielen Plattformen tatsächlich ein Compiler intrinsisch ist; Das heißt, der Compiler kann die Absicht "verstehen", einen großen Speicherbereich auf einen bestimmten Wert zu setzen und möglicherweise einen besseren Code zu generieren.

Das kann insbesondere bedeuten, dass bestimmte Hardwareoperationen zum Festlegen großer Speicherbereiche wie SSE auf dem x86, AltiVec auf dem PowerPC, NEON auf dem ARM usw. verwendet werden. Dies kann eine enorme Leistungsverbesserung sein.

Auf der anderen Seite sagen Sie dem Compiler, indem Sie eine for-Schleife verwenden, etwas Spezifischeres: "Laden Sie diese Adresse in ein Register. Schreiben Sie eine Zahl. Fügen Sie der Adresse eine hinzu. Schreiben Sie eine Zahl, " und so weiter. Theoretisch würde ein perfekt intelligenter Compiler diese Schleife für das, was sie ist, erkennen und sie trotzdem in ein Memset verwandeln; aber ich habe noch nie einen echten Compiler gefunden, der das gemacht hat.

Also, die Annahme ist, dass memset von intelligenten Leuten geschrieben wurde, um die beste und schnellste Möglichkeit zu sein, eine ganze Speicherregion für die spezifische Plattform und Hardware zu setzen, die der Compiler unterstützt. Das ist oft , aber nicht immer , richtig.

    
Crashworks 16.12.2011 00:56
quelle
5

Denk daran, dass dies

ist %Vor%

kann auch schneller sein als

%Vor%

Wie bereits beantwortet, hat der Compiler oft handoptimierte Routinen für memset () memcpy () und andere String-Funktionen. Und wir sprechen deutlich schneller. jetzt ist die Menge an Code, die Anzahl der Anweisungen, die ein schneller -Memcpy oder -Memset vom Compiler ist, normalerweise viel größer als die von Ihnen vorgeschlagene Loop-Lösung. weniger Zeilen Code, weniger Anweisungen bedeutet nicht schneller.

Wie auch immer, meine Nachricht ist beides. den Code zusammenstellen, den Unterschied sehen, versuchen zu verstehen, Fragen beim Stapelüberlauf stellen, wenn Sie nicht. und dann verwenden Sie einen Timer und Zeit die beiden Lösungen, rufen Sie die Memcpy-Funktion Tausende oder Hunderttausende Male und Zeit das Ganze (um Fehler in der Zeit zu beseitigen). Stellen Sie sicher, dass Sie kurze Kopien wie sagen 7 Elemente oder 5 Elemente und große Kopien wie Hunderte von Bytes pro Memset und versuchen Sie einige Primzahlen, während Sie daran sind. Bei einigen Prozessoren auf manchen Systemen kann die Schleife für einige wenige Elemente wie 3 oder 5 oder so etwas schneller sein, obwohl es sehr langsam wird.

Hier ist ein Hinweis auf die Leistung. Der DDR-Speicher in Ihrem Computer ist wahrscheinlich 64 Bits breit und muss 64 Bits gleichzeitig geschrieben werden, vielleicht hat er ecc und Sie müssen über diese Bits berechnen und 72 Bits gleichzeitig schreiben. Nicht immer diese genaue Zahl, aber folgen Sie dem Gedanken, dass es für 32 Bits oder 64 oder 128 oder was auch immer sinnvoll ist. Wenn Sie eine Single-Byte-Schreibanweisung zum Ram ausführen, muss die Hardware eines von zwei Dingen tun: Wenn es keine Caches auf dem Weg gibt, muss das Speichersystem ein 64-Bit-Lesen durchführen und dann das eine Byte ändern schreib es zurück. Ohne eine Art von Hardware-Optimierung, schreiben 8 Bytes in dieser einen Dram-Zeile, 16 Speicherzyklen, und dram ist sehr, sehr langsam, lassen Sie sich nicht von den 1333MHz-Zahlen täuschen.

Nun, wenn Sie einen Cache haben, wird das Schreiben des ersten Bytes eine Cache-Zeile erfordern, die von dram gelesen wird, das ist ein oder mehrere dieser 64-Bit-Lesevorgänge, die nächsten 7 oder 15 oder was auch immer Byte-Schreiboperationen wahrscheinlich sein werden wirklich schnell, da sie nur zum cache gehen und nicht zu ddr, schließlich geht diese cache-zeile aus, um zu drimen, langsam, also ein oder zwei oder vier usw. dieser 64 bit oder was auch immer ddr standorte. Selbst wenn Sie nur schreiben, müssen Sie immer noch den ganzen Ram lesen und dann schreiben, also doppelt so viele Zyklen wie gewünscht. Wenn es möglich ist, und es ist mit einigen Prozessoren und Speichersystemen, der memset oder der Schreibteil eines memcpy, kann einzelne Anweisungen mit einer ganzen cache-Linie oder ganzer ddr-Standort sein und es ist kein Lesen erforderlich, verdoppelte Geschwindigkeit sofort. Auf diese Weise funktionieren nicht alle Optimierungen, aber es gibt Ihnen hoffentlich eine Vorstellung davon, wie Sie über das Problem nachdenken sollten. Wenn Ihr Programm in Cache-Zeilen in den Cache gezogen wird, können Sie die Anzahl der ausgeführten Befehle verdoppeln oder verdreifachen, wenn Sie die Anzahl der DDR-Zyklen um die Hälfte oder mehr reduzieren und Sie insgesamt gewinnen.

Mindestens die Compiler-Memset- und Memcpy-Routinen werden eine Byte-Operation ausführen, wenn die Startadresse ungerade ist, dann ein 16-Bit, wenn sie nicht auf 32 Bits ausgerichtet ist. Dann ein 32-Bit, wenn nicht auf 64 ausgerichtet und weiter, bis sie die optimale Übertragungsgröße für diesen Befehlssatz / dieses System erreichen. Am Arm zielen sie auf 128 Bits ab. Der schlechteste Fall am Front-End wäre dann ein einzelnes Byte, dann ein einzelnes Halbwort, dann ein paar Worte, dann gehts in den Haupt-Satz oder die Kopier-Schleife. Im Fall von 128-Bit-ARM-Übertragungen werden 128 Bits pro Anweisung geschrieben. Dann auf dem Back-End, wenn nicht ausgerichtete der gleiche Deal, ein paar Worte, ein halbes Wort, ein Byte schlimmsten Fall. Sie werden auch sehen, dass die Bibliotheken Dinge wie, wenn die Anzahl der Bytes ist kleiner als X, wo X ist eine kleine Zahl wie 13 oder so, dann geht es in eine Schleife wie Ihre, kopieren Sie einfach einige Bytes, weil die Anzahl der Anweisungen und Taktzyklen diese Schleife zu unterstützen ist kleiner / schneller. disassemblieren oder finden Sie den GCC-Quellcode für ARM und wahrscheinlich Mips und einige andere gute Prozessoren und sehen, was ich rede.

    
old_timer 16.12.2011 02:09
quelle
3

Zwei Vorteile:

  1. Die Version mit memset ist einfacher zu lesen - dies ist verwandt mit, aber nicht das gleiche wie mit weniger Codezeilen. Es braucht weniger Denken zu wissen, was die memset Version macht, besonders wenn Sie es schreiben

    %Vor%

    statt mit der indirect durch p und die unnötige Umwandlung in void * (HINWEIS: nur unnötig, wenn Sie wirklich in C codieren und nicht C ++ - einige Leute sind unklar auf der Unterschied).

  2. memset ist wahrscheinlich um in der Lage zu sein, 4 oder 8 Bytes gleichzeitig zu schreiben und / oder spezielle Cache-Hinweisbefehle zu nutzen; Daher ist es möglicherweise schneller als Ihre byteweise Schleife. (HINWEIS: Einige Compiler sind clever genug, um eine Bulk-Clearing-Schleife zu erkennen und entweder größere Schreibvorgänge im Speicher oder einen Aufruf von memset zu ersetzen. Ihre Laufleistung kann variieren. Messen Sie immer die Leistung, bevor Sie versuchen, Zyklen zu bereinigen.)

zwol 16.12.2011 00:57
quelle
1

memset bietet eine Standardmethode zum Schreiben von Code, wobei die jeweiligen Plattform / Compiler-Bibliotheken den effizientesten Mechanismus bestimmen können. Basierend auf Datengrößen kann es beispielsweise 32-Bit- oder 64-Bit-Speicher so viel wie möglich speichern.

    
TJD 16.12.2011 00:55
quelle
1

Ihre Variable p wird nur für die Initialisierungsschleife benötigt. Der Code für das Memset sollte einfach

sein %Vor%

was einfacher und weniger fehleranfällig ist. Der Punkt eines void* -Parameters ist genau, dass er jeden Zeigertyp akzeptiert, die explizite Umwandlung ist nicht notwendig und die Zuweisung zu einem Zeiger eines anderen Typs ist sinnlos.

Ein Vorteil der Verwendung von memset() in diesem Fall besteht also darin, eine unnötige Zwischenvariable zu vermeiden.

Ein weiterer Vorteil ist, dass memset () auf einer bestimmten Plattform wahrscheinlich für die Zielplattform optimiert wird, während Ihre Schleifeneffizienz von den Compiler- und Compilereinstellungen abhängt.

    
Clifford 16.12.2011 17:31
quelle

Tags und Links