Reimposta l'array C int a zero: il modo più veloce?


102

Supponendo di avere un file T myarray[100] con T = int, unsigned int, long long int o unsigned long long int, qual è il modo più veloce per ripristinare tutto il suo contenuto a zero (non solo per l'inizializzazione ma per ripristinare il contenuto più volte nel mio programma) ? Forse con memset?

Stessa domanda per un array dinamico come T *myarray = new T[100].


16
@BoPersson: beh, new è C ++ ...
Matteo Italia

@Matteo - beh, sì. Non ha influenzato molto le risposte (fino ad ora :-).
Bo Persson

3
@BoPersson: Mi sono sentito male a parlare solo di memsetquando C ++ è in qualche modo coinvolto ... :)
Matteo Italia

2
Su un compilatore moderno, non puoi battere un semplice forciclo. Ma, sorprendentemente, puoi fare molto di peggio cercando di essere intelligente.
David Schwartz

Usa una struttura e inserisci un array al suo interno. Crea un'istanza che sia tutti zeri. Usalo per azzerare gli altri che crei. Funziona bene. Nessun include, nessuna funzione, abbastanza veloce.
Xofo

Risposte:


170

memset(from <string.h>) è probabilmente il modo standard più veloce, poiché di solito è una routine scritta direttamente in assembly e ottimizzata a mano.

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

A proposito, in C ++ il modo idiomatico sarebbe usare std::fill(da <algorithm>):

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

che può essere ottimizzato automaticamente in un memset; Sono abbastanza sicuro che funzionerà alla stessa velocità memsetdi ints, mentre potrebbe funzionare leggermente peggio per i tipi più piccoli se l'ottimizzatore non è abbastanza intelligente. Tuttavia, in caso di dubbio, profilo.


10
A partire dallo standard ISO C del 1999, non era effettivamente garantito che memsetavrebbe impostato un numero intero a 0; non c'era alcuna dichiarazione specifica di cui all-bits-zero fosse una rappresentazione 0. Una rettifica tecnica ha aggiunto tale garanzia, inclusa nello standard ISO C del 2011. Credo che all-bits-zero sia una rappresentazione valida di 0tutti i tipi interi in tutte le implementazioni C e C ++ esistenti, motivo per cui il comitato è stato in grado di aggiungere tale requisito. (Non esiste una garanzia simile per i tipi a virgola mobile o puntatore.)
Keith Thompson,

3
In aggiunta al commento di @ KeithThompson: questa garanzia è stata aggiunta al 6.2.6.2/5 in testo normale in TC2 (2004); tuttavia, se non ci sono bit di riempimento, 6.2.6.2/1 e / 2 garantivano già che all-bits-zero fosse 0. (Con padding bits esiste la possibilità che all-bits-zero possa essere una rappresentazione trap). Ma in ogni caso, il TC dovrebbe riconoscere e sostituire il testo difettoso, quindi dal 2004 dovremmo agire come se C99 contenesse sempre questo testo.
MM

In C, se hai allocato correttamente l' array dinamico , non ci sarà alcuna differenza tra i due memset. La corretta allocazione dinamica sarebbe int (*myarray)[N] = malloc(sizeof(*myarray));.
Lundin

@ Lundin: ovviamente - se sai in fase di compilazione quanto Nè grande , ma nella stragrande maggioranza dei casi se lo usavi malloclo sapevi solo in fase di esecuzione.
Matteo Italia

@MatteoItalia Abbiamo VLA dal 1999.
Lundin

20

Questa domanda, sebbene piuttosto vecchia, necessita di alcuni benchmark, in quanto non richiede il modo più idiomatico, o il modo che può essere scritto nel minor numero di righe, ma il modo più veloce . Ed è sciocco rispondere a questa domanda senza alcuni test effettivi. Quindi ho confrontato quattro soluzioni, memset vs std :: fill vs ZERO della risposta di AnT rispetto a una soluzione che ho realizzato utilizzando AVX intrinsics.

Si noti che questa soluzione non è generica, funziona solo su dati di 32 o 64 bit. Si prega di commentare se questo codice sta facendo qualcosa di sbagliato.

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

Non affermerò che questo sia il metodo più veloce, poiché non sono un esperto di ottimizzazione di basso livello. Piuttosto è un esempio di una corretta implementazione dipendente dall'architettura che è più veloce di memset.

Ora, sui risultati. Ho calcolato le prestazioni per array di dimensioni 100 int e long long, allocati sia staticamente che dinamicamente, ma con l'eccezione di msvc, che ha eseguito un'eliminazione del codice morto su array statici, i risultati erano estremamente comparabili, quindi mostrerò solo le prestazioni dell'array dinamico. I contrassegni del tempo sono ms per 1 milione di iterazioni, utilizzando la funzione di orologio a bassa precisione di time.h.

clang 3.8 (Utilizzando il frontend clang-cl, flag di ottimizzazione = / 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 (flag di ottimizzazione: -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 (flag di ottimizzazione: / 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

C'è molto di interessante qui: llvm killing gcc, le tipiche ottimizzazioni spotty di MSVC (esegue un'impressionante eliminazione del codice morto su array statici e quindi ha prestazioni terribili per il riempimento). Sebbene la mia implementazione sia significativamente più veloce, ciò potrebbe essere dovuto solo al fatto che riconosce che la cancellazione dei bit ha un sovraccarico molto inferiore rispetto a qualsiasi altra operazione di impostazione.

L'implementazione di Clang merita un'analisi più approfondita, poiché è significativamente più veloce. Alcuni test aggiuntivi mostrano che il suo memset è in realtà specializzato per zero - i memset diversi da zero per array da 400 byte sono molto più lenti (~ 220ms) e sono paragonabili a quelli di gcc. Tuttavia, il memset diverso da zero con un array di 800 byte non fa differenza di velocità, motivo per cui in quel caso il loro memset ha prestazioni peggiori della mia implementazione: la specializzazione è solo per piccoli array e il cuttoff è di circa 800 byte. Si noti inoltre che gcc 'fill' e 'ZERO' non si ottimizzano per memset (guardando il codice generato), gcc sta semplicemente generando codice con caratteristiche di prestazioni identiche.

Conclusione: memset non è realmente ottimizzato per questo compito così come le persone vorrebbero fingere che lo sia (altrimenti gcc, msvc e memset di llvm avrebbero le stesse prestazioni). Se le prestazioni contano, memset non dovrebbe essere una soluzione definitiva, specialmente per questi scomodi array di medie dimensioni, perché non è specializzato per la cancellazione dei bit e non è ottimizzato a mano meglio di quanto il compilatore possa fare da solo.


4
Un benchmark senza codice e senza menzione della versione del compilatore e delle opzioni utilizzate? Hmm ...
Marc Glisse

Avevo già le versioni del compilatore (erano solo un po 'nascoste) e ho appena aggiunto le opzioni applicabili utilizzate.
Benjamin

argomento di tipo non valido di unario '*' (avere 'size_t {aka unsigned int}') |
Piotr Wasilewicz

Essendo così generoso da scrivere il tuo metodo di azzeramento ottimizzato, potresti per favore risparmiare qualche parola su COME funziona e PERCHÉ è più veloce? il codice è tutto fuorché autoesplicativo.
Motti Shneor

1
@MottiShneor Sembra più complicato di quello che è. Un registro AVX ha una dimensione di 32 byte. Quindi calcola quanti valori di aadattamento in un registro. Successivamente, esegue un ciclo su tutti i blocchi da 32 byte, che dovrebbero essere completamente sovrascritti utilizzando il puntatore aritmetico ( (float *)((a)+x)). I due intrinseci (che iniziano con _mm256) creano semplicemente un registro di 32 byte inizializzato con zero e lo memorizzano nel puntatore corrente. Queste sono le prime 3 righe. Il resto gestisce solo tutti i casi speciali in cui l'ultimo blocco di 32 byte non dovrebbe essere completamente sovrascritto. È più veloce grazie alla vettorializzazione. - Spero che aiuti.
wychmaster

11

Da memset():

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

È possibile utilizzare sizeof(myarray)se la dimensione di myarrayè nota in fase di compilazione. In caso contrario, se si utilizza un array di dimensioni dinamiche, come ottenuto tramite malloco new, sarà necessario tenere traccia della lunghezza.


2
sizeof funzionerà anche se la dimensione dell'array non è nota in fase di compilazione. (ovviamente, solo quando è array)
asaelr

2
@asaelr: in C ++, sizeofviene sempre valutato in fase di compilazione (e non può essere utilizzato con VLA). In C99, può essere un'espressione di runtime nel caso di VLA.
Ben Voigt

@ BenVoigt Bene, la domanda riguarda sia ce c++. Ho commentato la risposta di Alex, che dice "Puoi usare sizeof (myarray) se la dimensione di myarray è nota in fase di compilazione".
asaelr

2
@asaelr: E in C ++ ha completamente ragione. Il tuo commento non ha detto nulla su C99 o VLA, quindi volevo chiarirlo.
Ben Voigt

5

Puoi usare memset , ma solo perché la nostra selezione di tipi è limitata ai tipi integrali.

In generale, in C ha senso implementare una macro

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

Questo ti darà funzionalità simili a C ++ che ti permetteranno di "resettare a zero" un array di oggetti di qualsiasi tipo senza dover ricorrere a hack come memset . Fondamentalmente, questo è un analogo C del modello di funzione C ++, tranne per il fatto che devi specificare esplicitamente l'argomento del tipo.

Inoltre puoi costruire un "modello" per array non decaduti

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

Nel tuo esempio sarebbe applicato come

int a[100];

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

Vale anche la pena notare che specificamente per oggetti di tipi scalari è possibile implementare una macro indipendente dal tipo

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

e

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

trasformando l'esempio sopra in

 int a[100];

 ZERO(a, 100);
 // or
 ZERO_A(a);

1
Vorrei omettere il ;dopo while(0), in modo da poter chiamare ZERO(a,n);, +1 ottima risposta
0x90

@ 0x90: Sì, hai assolutamente ragione. L'intero punto del do{}while(0)linguaggio non richiede ;nella definizione macro. Fisso.
AnT

3

Per la dichiarazione statica penso che potresti usare:

T myarray[100] = {0};

Per la dichiarazione dinamica suggerisco allo stesso modo: memset


2
La domanda dice: "Non solo per l'inizializzazione".
Ben Voigt

2

zero(myarray); è tutto ciò di cui hai bisogno in C ++.

Basta aggiungerlo a un'intestazione:

template<typename T, size_t SIZE> inline void zero(T(&arr)[SIZE]){
    memset(arr, 0, SIZE*sizeof(T));
}

1
Questo non è corretto, cancellerà SIZE byte. 'memset (arr, 0, SIZE * sizeof (T));' sarebbe corretto.
Kornel Kisielewicz

@KornelKisielewicz D'oh! Spero che nessuno abbia copiato questa funzione negli ultimi 1,5 anni :(
Navin

1
spero di no, ho commentato perché google mi ha portato qui :)
Kornel Kisielewicz

1
Si noti che questa funzione zeroè corretta anche per es. T=char[10]Come potrebbe essere il caso quando l' arrargomento è un array multidimensionale es char arr[5][10].
Mandrake

1
Sì, ho testato diversi casi con gcc 4.7.3. Trovo che sarebbe utile notare per questa risposta, poiché altrimenti avresti bisogno di specializzazioni del modello per ogni conteggio delle dimensioni dell'array. Anche altre risposte non generalizzano, come la ARRAY_SIZEmacro, che dà la dimensione sbagliata se usata su un array multidimensionale, forse sarebbe un nome migliore ARRAY_DIM<n>_SIZE.
Mandrake

1

Ecco la funzione che utilizzo:

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

Puoi chiamarlo così:

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

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

Sopra c'è più modo C ++ 11 che usare memset. Inoltre si ottiene un errore in fase di compilazione se si utilizza un array dinamico con la specifica della dimensione.


la domanda originale è su C, non C ++, quindi std :: fill non può essere una risposta corretta
Motti Shneor
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.