Oto kolejna wersja dla nas Użytkownicy platformy porzuceni przez Microsoft. Jest 4 razy Array.Clear
szybszy i szybszy niż rozwiązanie Panos Theof i równoległe rozwiązanie Erica J i Petara Pietrowa - do dwóch razy szybsze w przypadku dużych tablic.
Najpierw chcę przedstawić przodka funkcji, ponieważ ułatwia to zrozumienie kodu. Pod względem wydajności jest to prawie na równi z kodem Panos Theof, a dla niektórych rzeczy, które mogą już wystarczyć:
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);
}
Jak widać, jest to oparte na wielokrotnym podwajaniu już zainicjowanej części. Jest to proste i wydajne, ale działa w porównaniu z nowoczesnymi architekturami pamięci. Stąd narodziła się wersja, która wykorzystuje podwajanie tylko do utworzenia przyjaznego dla bufora bloku początkowego, który jest następnie wysadzany iteracyjnie nad obszarem docelowym:
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);
}
Uwaga: wcześniejszy kod potrzebny (count + 1) >> 1
jako limit dla pętli dublującej, aby upewnić się, że w końcowej operacji kopiowania jest wystarczająca ilość paszy, aby pokryć wszystko, co pozostało. Nie byłoby tak w przypadku nieparzystych zliczeń, gdyby count >> 1
zamiast nich zastosowano. W obecnej wersji nie ma to znaczenia, ponieważ pętla kopiowania liniowego wykryje luz.
Rozmiar komórki tablicowej musi zostać przekazany jako parametr, ponieważ - umysł zadziwia - nie można używać rodzajów ogólnych, sizeof
chyba że zastosują ograniczenie ( unmanaged
), które może, ale nie musi, stać się dostępne w przyszłości. Błędne szacunki nie są wielkim problemem, ale wydajność jest najlepsza, jeśli wartość jest dokładna, z następujących powodów:
Niedocenianie rozmiaru elementu może prowadzić do rozmiarów bloków większych niż połowa pamięci podręcznej L1, a tym samym zwiększa prawdopodobieństwo eksmisji źródłowych danych z L1 i konieczności ponownego pobrania z wolniejszych poziomów pamięci podręcznej.
Przeszacowanie rozmiaru elementu powoduje niepełne wykorzystanie pamięci podręcznej L1 procesora, co oznacza, że pętla kopiowania bloku liniowego jest wykonywana częściej niż przy optymalnym wykorzystaniu. W ten sposób powstaje więcej narzutu związanego z ustaloną pętlą / wywołaniem niż jest to absolutnie konieczne.
Oto test porównawczy mojego kodu Array.Clear
i pozostałe trzy wspomniane wcześniej rozwiązania. Czasy dotyczą wypełniania tablic liczb całkowitych ( Int32[]
) o podanych rozmiarach. W celu zmniejszenia zmienności spowodowanej błędami pamięci podręcznej itp. Każdy test był wykonywany dwukrotnie, z powrotem do tyłu, a czasy były brane pod uwagę przy drugim wykonaniu.
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
Jeśli wydajność tego kodu nie będzie wystarczająca, obiecującą ścieżką będzie równoległa pętla kopiowania liniowego (wszystkie wątki będą korzystały z tego samego bloku źródłowego) lub nasz stary dobry przyjaciel P / Invoke.
Uwaga: czyszczenie i wypełnianie bloków jest zwykle wykonywane przez procedury wykonawcze, które rozgałęziają się do wysoce wyspecjalizowanego kodu przy użyciu instrukcji MMX / SSE i tak dalej, więc w każdym przyzwoitym środowisku można po prostu nazwać odpowiedni moralny ekwiwalent std::memset
i być pewnym poziomu profesjonalnej wydajności. IOW, zgodnie z prawem funkcja biblioteki Array.Clear
powinna pozostawić wszystkie nasze ręcznie zwijane wersje w pył. Fakt, że jest na odwrót, pokazuje, jak dalece rzeczy są naprawdę zepsute. To samo dotyczy konieczności rzucania własnym Fill<>
, ponieważ wciąż jest tylko w Core i Standard, ale nie w Framework. .NET istnieje już od prawie dwudziestu lat i nadal musimy P / Invoke w lewo i prawo, aby uzyskać najbardziej podstawowe rzeczy lub rzucić własne ...