C int Array auf Null zurücksetzen: der schnellste Weg?

102

Angenommen, wir haben ein T myarray[100]mit T = int, unsigned int, long long int oder unsigned long long int, was ist der schnellste Weg, um den gesamten Inhalt auf Null zurückzusetzen (nicht nur zur Initialisierung, sondern um den Inhalt in meinem Programm mehrmals zurückzusetzen). ? Vielleicht mit Memset?

Gleiche Frage für ein dynamisches Array wie T *myarray = new T[100].

Vincent
quelle
16
@ BoPersson: Nun, new ist C ++ ...
Matteo Italia
@ Matto - na ja. Hat die Antworten nicht sehr beeinflusst (bis jetzt :-).
Bo Persson
3
@ BoPersson: Ich fühlte mich schlecht, memsetwenn ich nur darüber sprach, wenn C ++ irgendwie involviert ist ... :)
Matteo Italia
2
Auf einem modernen Compiler können Sie eine einfache forSchleife nicht schlagen . Aber überraschenderweise können Sie viel schlimmeres tun, wenn Sie versuchen, klug zu sein.
David Schwartz
Verwenden Sie eine Struktur und kleben Sie ein Array hinein. Erstellen Sie eine Instanz, die nur aus Nullen besteht. Verwenden Sie dies, um andere, die Sie erstellen, auf Null zu setzen. Es funktioniert gut. Keine Includes, keine Funktionen, ziemlich schnell.
Xofo

Antworten:

170

memset(von <string.h>) ist wahrscheinlich der schnellste Standardweg, da es sich normalerweise um eine Routine handelt, die direkt in der Montage geschrieben und von Hand optimiert wird.

memset(myarray, 0, sizeof(myarray)); // for automatically-allocated arrays
memset(myarray, 0, N*sizeof(*myarray)); // for heap-allocated arrays, where N is the number of elements

Übrigens wäre in C ++ der idiomatische Weg, std::fill(von <algorithm>) zu verwenden:

std::fill(myarray, myarray+N, 0);

das kann automatisch in eine optimiermemset ; Ich bin mir ziemlich sicher, dass es so schnell wie memsetbei ints funktioniert , während es bei kleineren Typen etwas schlechter abschneidet, wenn der Optimierer nicht intelligent genug ist. Trotzdem, wenn Sie Zweifel haben, Profil.

Matteo Italia
quelle
10
Nach dem ISO C-Standard von 1999 war nicht garantiert, dass memseteine Ganzzahl auf 0 gesetzt wird. Es gab keine spezifische Aussage, dass All-Bits-Null eine Darstellung von ist 0. Eine technische Berichtigung fügte eine solche Garantie hinzu, die in der ISO C-Norm 2011 enthalten ist. Ich glaube , dass alle Bits von Null ist eine gültige Darstellung 0für alle Integer - Typen in allen existierenden C und C ++ Implementierungen, weshalb der Ausschuss der Lage war , diese Anforderung hinzuzufügen. (Es gibt keine ähnliche Garantie für Gleitkomma- oder Zeigertypen.)
Keith Thompson
3
Hinzufügen zu @ KeithThompsons Kommentar: Diese Garantie wurde in TC2 (2004) im Klartext zu 6.2.6.2/5 hinzugefügt. Wenn jedoch keine Füllbits vorhanden sind, haben 6.2.6.2/1 und / 2 bereits garantiert, dass alle Bits Null waren 0. (Mit Füllbits besteht die Möglichkeit, dass All-Bits-Null eine Trap-Darstellung sein könnte). In jedem Fall soll der TC fehlerhaften Text anerkennen und ersetzen. Ab 2004 sollten wir also so tun, als ob C99 diesen Text immer enthalten würde.
MM
Wenn Sie in C das dynamische Array korrekt zugewiesen haben , gibt es keinen Unterschied zwischen den beiden Memsets. Richtige dynamische Zuordnung wäre int (*myarray)[N] = malloc(sizeof(*myarray));.
Lundin
@Lundin: Natürlich - wenn Sie zur Kompilierungszeit wissen, wie groß sie Nsind, aber in den allermeisten Fällen, wenn Sie sie verwendet haben malloc, wussten Sie es nur zur Laufzeit.
Matteo Italia
@MatteoItalia Wir haben VLAs seit dem Jahr 1999.
Lundin
20

Diese Frage ist zwar ziemlich alt, erfordert jedoch einige Benchmarks, da sie nicht den idiomatischsten oder den Weg erfordert, der in der geringsten Anzahl von Zeilen geschrieben werden kann, sondern den schnellsten Weg. Und es ist albern, diese Frage ohne tatsächliche Tests zu beantworten. Also habe ich vier Lösungen verglichen, memset vs. std :: fill vs. ZERO der Antwort von AnT mit einer Lösung, die ich mit AVX intrinsics erstellt habe.

Beachten Sie, dass diese Lösung nicht generisch ist, sondern nur mit Daten von 32 oder 64 Bit funktioniert. Bitte kommentieren Sie, wenn dieser Code etwas falsch macht.

#include<immintrin.h>
#define intrin_ZERO(a,n){\
size_t x = 0;\
const size_t inc = 32 / sizeof(*(a));/*size of 256 bit register over size of variable*/\
for (;x < n-inc;x+=inc)\
    _mm256_storeu_ps((float *)((a)+x),_mm256_setzero_ps());\
if(4 == sizeof(*(a))){\
    switch(n-x){\
    case 3:\
        (a)[x] = 0;x++;\
    case 2:\
        _mm_storeu_ps((float *)((a)+x),_mm_setzero_ps());break;\
    case 1:\
        (a)[x] = 0;\
        break;\
    case 0:\
        break;\
    };\
}\
else if(8 == sizeof(*(a))){\
switch(n-x){\
    case 7:\
        (a)[x] = 0;x++;\
    case 6:\
        (a)[x] = 0;x++;\
    case 5:\
        (a)[x] = 0;x++;\
    case 4:\
        _mm_storeu_ps((float *)((a)+x),_mm_setzero_ps());break;\
    case 3:\
        (a)[x] = 0;x++;\
    case 2:\
        ((long long *)(a))[x] = 0;break;\
    case 1:\
        (a)[x] = 0;\
        break;\
    case 0:\
        break;\
};\
}\
}

Ich werde nicht behaupten, dass dies die schnellste Methode ist, da ich kein Experte für Optimierung auf niedriger Ebene bin. Es ist vielmehr ein Beispiel für eine korrekte architekturabhängige Implementierung, die schneller als memset ist.

Nun zu den Ergebnissen. Ich habe die Leistung für Arrays der Größe 100 int und long long berechnet, sowohl statisch als auch dynamisch zugewiesen. Mit Ausnahme von msvc, das bei statischen Arrays eine Eliminierung des toten Codes durchgeführt hat, waren die Ergebnisse äußerst vergleichbar, sodass nur die Leistung dynamischer Arrays angezeigt wird. Zeitmarkierungen sind ms für 1 Million Iterationen unter Verwendung der niedrigpräzisen Uhrfunktion von time.h.

clang 3.8 (Mit dem clang-cl-Frontend Optimierungsflags = / OX / arch: AVX / Oi / Ot)

int:
memset:      99
fill:        97
ZERO:        98
intrin_ZERO: 90

long long:
memset:      285
fill:        286
ZERO:        285
intrin_ZERO: 188

gcc 5.1.0 (Optimierungsflags: -O3 -march = native -mtune = native -mavx):

int:
memset:      268
fill:        268
ZERO:        268
intrin_ZERO: 91
long long:
memset:      402
fill:        399
ZERO:        400
intrin_ZERO: 185

msvc 2015 (Optimierungsflags: / OX / arch: AVX / Oi / Ot):

int
memset:      196
fill:        613
ZERO:        221
intrin_ZERO: 95
long long:
memset:      273
fill:        559
ZERO:        376
intrin_ZERO: 188

Hier ist viel Interessantes los: llvm töten gcc, MSVCs typische fleckige Optimierungen (es führt eine beeindruckende Eliminierung von totem Code auf statischen Arrays durch und hat dann eine schreckliche Leistung beim Füllen). Obwohl meine Implementierung erheblich schneller ist, kann dies nur daran liegen, dass das Löschen von Bits viel weniger Aufwand verursacht als jede andere Einstellungsoperation.

Die Implementierung von Clang verdient mehr Aufmerksamkeit, da sie erheblich schneller ist. Einige zusätzliche Tests zeigen, dass sein Memset tatsächlich auf Null-Nicht-Null-Memsets für 400-Byte-Arrays spezialisiert ist, die viel langsamer sind (~ 220 ms) und mit gccs vergleichbar sind. Das Memsetting ungleich Null mit einem 800-Byte-Array macht jedoch keinen Geschwindigkeitsunterschied, weshalb in diesem Fall das Memset wahrscheinlich eine schlechtere Leistung aufweist als meine Implementierung - die Spezialisierung gilt nur für kleine Arrays, und der Cuttoff liegt bei etwa 800 Byte. Beachten Sie auch, dass gcc 'fill' und 'ZERO' nicht für memset optimiert sind (unter Berücksichtigung des generierten Codes). Gcc generiert einfach Code mit identischen Leistungsmerkmalen.

Fazit: memset ist nicht wirklich für diese Aufgabe optimiert, so wie es die Leute vorgeben würden (andernfalls hätten das memset von gcc und msvc und llvm die gleiche Leistung). Wenn es auf die Leistung ankommt, sollte memset keine endgültige Lösung sein, insbesondere für diese umständlichen mittelgroßen Arrays, da es nicht auf das Löschen von Bits spezialisiert ist und nicht besser von Hand optimiert wird, als es der Compiler alleine tun kann.

Benjamin
quelle
4
Ein Benchmark ohne Code und ohne Erwähnung der Compilerversion und der verwendeten Optionen? Hmm ...
Marc Glisse
Ich hatte bereits die Compiler-Versionen (sie waren nur ein wenig versteckt) und fügte nur die entsprechenden verwendeten Optionen hinzu.
Benjamin
ungültiges Typargument von unärem '*' (haben 'size_t {aka unsigned int}') |
Piotr Wasilewicz
Seien Sie so großzügig, dass Sie Ihre eigene optimierte Nullungsmethode schreiben. Können Sie bitte ein paar Worte darüber verlieren, wie es funktioniert und warum es schneller ist? Der Code ist alles andere als selbsterklärend.
Motti Shneor
1
@MottiShneor Es sieht komplizierter aus als es ist. Ein AVX-Register hat eine Größe von 32 Byte. Also berechnet er, wie viele Werte ain ein Register passen. Anschließend durchläuft er alle 32-Byte-Blöcke, die mit Zeigerarithmetik ( (float *)((a)+x)) vollständig überschrieben werden sollen . Die beiden Intrinsics (beginnend mit _mm256) erstellen einfach ein mit Null initialisiertes 32-Byte-Register und speichern es im aktuellen Zeiger. Dies sind die ersten 3 Zeilen. Der Rest behandelt nur alle Sonderfälle, in denen der letzte 32-Byte-Block nicht vollständig überschrieben werden sollte. Aufgrund der Vektorisierung ist es schneller. - Ich hoffe das hilft.
Wychmaster
11

Von memset():

memset(myarray, 0, sizeof(myarray));

Sie können verwenden, sizeof(myarray)wenn die Größe von myarrayzur Kompilierungszeit bekannt ist. Andernfalls müssen Sie die Länge verfolgen , wenn Sie ein Array mit dynamischer Größe verwenden, z. B. über mallocoder new.

Alex Reynolds
quelle
2
sizeof funktioniert auch dann, wenn die Größe des Arrays zur Kompilierungszeit nicht bekannt ist. (Natürlich nur, wenn es Array ist)
Asaelr
2
@asaelr: Wird in C ++ sizeofimmer zur Kompilierungszeit ausgewertet (und kann nicht mit VLAs verwendet werden). In C99 kann es sich bei VLAs um einen Laufzeitausdruck handeln.
Ben Voigt
@ BenVoigt Nun, die Frage betrifft beides cund c++. Ich kommentierte Alex 'Antwort: "Sie können sizeof (myarray) verwenden, wenn die Größe von myarray zur Kompilierungszeit bekannt ist."
Asaelr
2
@asaelr: Und in C ++ ist er völlig korrekt. Ihr Kommentar sagte nichts über C99 oder VLAs aus, deshalb wollte ich ihn klarstellen.
Ben Voigt
5

Sie können verwenden memset, aber nur, weil unsere Auswahl an Typen auf integrale Typen beschränkt ist.

Im Allgemeinen ist es in C sinnvoll, ein Makro zu implementieren

#define ZERO_ANY(T, a, n) do{\
   T *a_ = (a);\
   size_t n_ = (n);\
   for (; n_ > 0; --n_, ++a_)\
     *a_ = (T) { 0 };\
} while (0)

Auf diese Weise erhalten Sie C ++ - ähnliche Funktionen, mit denen Sie ein Array von Objekten eines beliebigen Typs auf Null zurücksetzen können, ohne auf Hacks wie zurückgreifen zu müssen memset. Grundsätzlich ist dies ein C-Analogon der C ++ - Funktionsvorlage, außer dass Sie das Typargument explizit angeben müssen.

Darüber hinaus können Sie eine "Vorlage" für nicht verfallene Arrays erstellen

#define ARRAY_SIZE(a) (sizeof (a) / sizeof *(a))
#define ZERO_ANY_A(T, a) ZERO_ANY(T, (a), ARRAY_SIZE(a))

In Ihrem Beispiel würde es als angewendet

int a[100];

ZERO_ANY(int, a, 100);
// or
ZERO_ANY_A(int, a);

Es ist auch erwähnenswert, dass speziell für Objekte von Skalartypen ein typunabhängiges Makro implementiert werden kann

#define ZERO(a, n) do{\
   size_t i_ = 0, n_ = (n);\
   for (; i_ < n_; ++i_)\
     (a)[i_] = 0;\
} while (0)

und

#define ZERO_A(a) ZERO((a), ARRAY_SIZE(a))

das obige Beispiel in verwandeln

 int a[100];

 ZERO(a, 100);
 // or
 ZERO_A(a);
Ameise
quelle
1
Ich würde das ;nach dem weglassen while(0), damit man anrufen kann ZERO(a,n);, +1 tolle Antwort
0x90
@ 0x90: Ja, du hast absolut recht. Der ganze Sinn der do{}while(0)Redewendung erfordert Nein ;in der Makrodefinition. Fest.
AnT
3

Für die statische Deklaration könnten Sie Folgendes verwenden:

T myarray[100] = {0};

Für die dynamische Deklaration schlage ich den gleichen Weg vor: memset

Bruno Soares
quelle
2
Die Frage lautet: "Nicht nur zur Initialisierung".
Ben Voigt
2

zero(myarray); ist alles was Sie in C ++ brauchen.

Fügen Sie dies einfach einem Header hinzu:

template<typename T, size_t SIZE> inline void zero(T(&arr)[SIZE]){
    memset(arr, 0, SIZE*sizeof(T));
}
Navin
quelle
1
Dies ist falsch, es werden SIZE-Bytes gelöscht. 'memset (arr, 0, SIZE * sizeof (T));' wäre richtig.
Kornel Kisielewicz
@KornelKisielewicz D'oh! Ich hoffe, niemand hat diese Funktion in den letzten 1,5 Jahren kopiert :(
Navin
1
hoffe nicht, ich habe kommentiert, weil Google mich hierher gebracht hat :)
Kornel Kisielewicz
1
Beachten Sie, dass diese Funktion zeroauch z. B. korrekt T=char[10]ist, wenn das arrArgument ein mehrdimensionales Array ist, z char arr[5][10].
Mandrake
1
Ja, ich habe eine Reihe von Fällen mit gcc 4.7.3 getestet. Ich finde, dass dies für diese Antwort gut zu beachten wäre, da Sie sonst Template-Spezialisierungen für jede Anzahl von Array-Dimensionen benötigen würden. Andere Antworten verallgemeinern sich ebenfalls nicht, wie das ARRAY_SIZEMakro, das bei Verwendung in einem mehrdimensionalen Array die falsche Größe angibt. Ein besserer Name wäre vielleicht ARRAY_DIM<n>_SIZE.
Mandrake
1

Hier ist die Funktion, die ich benutze:

template<typename T>
static void setValue(T arr[], size_t length, const T& val)
{
    std::fill(arr, arr + length, val);
}

template<typename T, size_t N>
static void setValue(T (&arr)[N], const T& val)
{
    std::fill(arr, arr + N, val);
}

Sie können es so nennen:

//fixed arrays
int a[10];
setValue(a, 0);

//dynamic arrays
int *d = new int[length];
setValue(d, length, 0);

Oben ist mehr C ++ 11 als die Verwendung von Memset. Außerdem wird ein Fehler bei der Kompilierung angezeigt, wenn Sie ein dynamisches Array mit Angabe der Größe verwenden.

Shital Shah
quelle
Die ursprüngliche Frage ist auf C, nicht auf C ++, daher kann std :: fill keine richtige Antwort sein
Motti Shneor