Ecco un'altra versione per noi utenti Framework abbandonati da Microsoft. Si tratta di 4 volte più veloce Array.Clear
e più veloce di soluzione di Panos Theof e Eric J di e una parallela di Petar Petrov - fino a due volte più veloce per grandi array.
Per prima cosa voglio presentarti l'antenato della funzione, perché ciò rende più semplice la comprensione del codice. Per quanto riguarda le prestazioni, questo è praticamente alla pari con il codice di Panos Theof, e per alcune cose che potrebbero già essere sufficienti:
public static void Fill<T> (T[] array, int count, T value, int threshold = 32)
{
if (threshold <= 0)
throw new ArgumentException("threshold");
int current_size = 0, keep_looping_up_to = Math.Min(count, threshold);
while (current_size < keep_looping_up_to)
array[current_size++] = value;
for (int at_least_half = (count + 1) >> 1; current_size < at_least_half; current_size <<= 1)
Array.Copy(array, 0, array, current_size, current_size);
Array.Copy(array, 0, array, current_size, count - current_size);
}
Come puoi vedere, questo si basa sul ripetuto raddoppio della parte già inizializzata. Questo è semplice ed efficiente, ma corre contro le moderne architetture di memoria. Da qui è nata una versione che utilizza il raddoppio solo per creare un blocco seed compatibile con la cache, che viene quindi fatto saltare in modo iterativo sull'area di destinazione:
const int ARRAY_COPY_THRESHOLD = 32; // 16 ... 64 work equally well for all tested constellations
const int L1_CACHE_SIZE = 1 << 15;
public static void Fill<T> (T[] array, int count, T value, int element_size)
{
int current_size = 0, keep_looping_up_to = Math.Min(count, ARRAY_COPY_THRESHOLD);
while (current_size < keep_looping_up_to)
array[current_size++] = value;
int block_size = L1_CACHE_SIZE / element_size / 2;
int keep_doubling_up_to = Math.Min(block_size, count >> 1);
for ( ; current_size < keep_doubling_up_to; current_size <<= 1)
Array.Copy(array, 0, array, current_size, current_size);
for (int enough = count - block_size; current_size < enough; current_size += block_size)
Array.Copy(array, 0, array, current_size, block_size);
Array.Copy(array, 0, array, current_size, count - current_size);
}
Nota: il codice precedente era necessario (count + 1) >> 1
come limite per il ciclo di raddoppio per garantire che l'operazione di copia finale abbia abbastanza foraggio per coprire tutto ciò che resta. Questo non sarebbe il caso di conteggi dispari se count >> 1
si dovesse usare invece. Per la versione attuale questo non ha alcun significato poiché il ciclo di copia lineare rileverà qualsiasi gioco.
La dimensione di una cella di matrice deve essere passata come parametro perché - la mente vacilla - i generici non possono usare a sizeof
meno che non utilizzino un vincolo ( unmanaged
) che potrebbe o non potrebbe essere disponibile in futuro. Le stime errate non sono un grosso problema, ma le prestazioni sono migliori se il valore è accurato, per i seguenti motivi:
Sottovalutare la dimensione dell'elemento può portare a blocchi di dimensioni superiori alla metà della cache L1, aumentando così la probabilità che i dati di origine della copia vengano sfrattati da L1 e che debbano essere recuperati da livelli di cache più lenti.
La sopravvalutazione della dimensione dell'elemento comporta un sottoutilizzo della cache L1 della CPU, il che significa che il ciclo di copia del blocco lineare viene eseguito più spesso di quanto sarebbe con un utilizzo ottimale. Pertanto, si verifica più dell'overhead fisso di loop / chiamata di quanto strettamente necessario.
Ecco un benchmark che mette a confronto il mio codice Array.Clear
e le altre tre soluzioni menzionate in precedenza. I tempi sono per il riempimento di array di numeri interi ( Int32[]
) delle dimensioni indicate. Al fine di ridurre le variazioni causate dai capricci della cache, ecc. Ogni test è stato eseguito due volte, schiena contro schiena, e i tempi sono stati presi per la seconda esecuzione.
array size Array.Clear Eric J. Panos Theof Petar Petrov Darth Gizka
-------------------------------------------------------------------------------
1000: 0,7 µs 0,2 µs 0,2 µs 6,8 µs 0,2 µs
10000: 8,0 µs 1,4 µs 1,2 µs 7,8 µs 0,9 µs
100000: 72,4 µs 12,4 µs 8,2 µs 33,6 µs 7,5 µs
1000000: 652,9 µs 135,8 µs 101,6 µs 197,7 µs 71,6 µs
10000000: 7182,6 µs 4174,9 µs 5193,3 µs 3691,5 µs 1658,1 µs
100000000: 67142,3 µs 44853,3 µs 51372,5 µs 35195,5 µs 16585,1 µs
Se le prestazioni di questo codice non fossero sufficienti, una strada promettente sarebbe quella di parallelizzare il ciclo lineare di copia (con tutti i thread che usano lo stesso blocco sorgente), o il nostro buon vecchio amico P / Invoke.
Nota: la cancellazione e il riempimento di blocchi viene normalmente eseguito da routine di runtime che si diramano verso un codice altamente specializzato utilizzando le istruzioni MMX / SSE e quant'altro, quindi in qualsiasi ambiente decente si potrebbe semplicemente chiamare il rispettivo equivalente morale std::memset
ed essere certi dei livelli di prestazioni professionali. IOW, per diritto la funzione di libreria Array.Clear
dovrebbe lasciare tutte le nostre versioni arrotolate a mano nella polvere. Il fatto che sia il contrario, dimostra quanto siano veramente lontane le cose fuori di testa. Lo stesso vale per doversi spostare da soli Fill<>
in primo luogo, perché è ancora solo in Core e Standard ma non nel Framework. .NET è in circolazione da quasi vent'anni ormai e dobbiamo ancora fare P / Invoke a destra e sinistra per le cose più elementari o girare il nostro ...