Hier ist eine weitere Version für uns Framework-Benutzer, die von Microsoft aufgegeben wurde. Es ist viermal so schnell Array.Clear
und schneller als die Lösung von Panos Theof und die parallele Lösung von Eric J und Petar Petrov - bis zu zweimal so schnell für große Arrays.
Zuerst möchte ich Ihnen den Vorfahren der Funktion vorstellen, da dies das Verständnis des Codes erleichtert. In Bezug auf die Leistung entspricht dies ziemlich genau dem Code von Panos Theof, und für einige Dinge, die möglicherweise bereits ausreichen:
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);
}
Wie Sie sehen können, basiert dies auf der wiederholten Verdoppelung des bereits initialisierten Teils. Dies ist einfach und effizient, verstößt jedoch gegen moderne Speicherarchitekturen. Daher wurde eine Version geboren, die das Verdoppeln nur verwendet, um einen cachefreundlichen Startblock zu erstellen, der dann iterativ über den Zielbereich gestrahlt wird:
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);
}
Hinweis: Der frühere Code wird (count + 1) >> 1
als Begrenzung für die Verdopplungsschleife benötigt, um sicherzustellen, dass der endgültige Kopiervorgang über genügend Futter verfügt, um alles verbleibende abzudecken. Dies wäre bei ungeraden Zählungen nicht der Fall, wenncount >> 1
stattdessen verwendet würde. Für die aktuelle Version ist dies nicht von Bedeutung, da die lineare Kopierschleife einen Durchhang aufnimmt.
Die Größe einer Array-Zelle muss als Parameter übergeben werden, da Generika - es ist ein Rätsel - nicht verwendet werden dürfen, es sizeof
sei denn, sie verwenden eine Einschränkung ( unmanaged
), die möglicherweise in Zukunft verfügbar wird oder nicht. Falsche Schätzungen sind keine große Sache, aber die Leistung ist aus folgenden Gründen am besten, wenn der Wert korrekt ist:
Eine Unterschätzung der Elementgröße kann zu Blockgrößen führen, die größer als die Hälfte des L1-Cache sind, wodurch die Wahrscheinlichkeit erhöht wird, dass Kopierquellendaten aus L1 entfernt werden und aus langsameren Cache-Ebenen erneut abgerufen werden müssen.
Das Überschätzen der Elementgröße führt zu einer Unterauslastung des L1-Cache der CPU, was bedeutet, dass die lineare Blockkopierschleife häufiger ausgeführt wird als bei optimaler Auslastung. Somit entsteht mehr Overhead für feste Schleifen / Anrufe als unbedingt erforderlich.
Hier ist ein Benchmark, an dem mein Code Array.Clear
und die anderen drei zuvor genannten Lösungen verglichen werden. Die Timings dienen zum Füllen von Integer-Arrays ( Int32[]
) der angegebenen Größen. Um die durch Cache-Abweichungen usw. verursachten Abweichungen zu verringern, wurde jeder Test zweimal hintereinander ausgeführt, und die Zeitpunkte wurden für die zweite Ausführung festgelegt.
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
Sollte die Leistung dieses Codes nicht ausreichen, wäre ein vielversprechender Weg die Parallelisierung der linearen Kopierschleife (wobei alle Threads denselben Quellblock verwenden) oder unseres guten alten Freundes P / Invoke.
Hinweis: Das Löschen und Füllen von Blöcken erfolgt normalerweise über Laufzeitroutinen, die mithilfe von MMX / SSE-Anweisungen und so weiter zu hochspezialisiertem Code verzweigen. In jeder anständigen Umgebung würde man einfach das jeweilige moralische Äquivalent std::memset
eines professionellen Leistungsniveaus nennen und sich dessen versichern. IOW, von Rechts wegen sollte die Bibliotheksfunktion Array.Clear
alle unsere handgerollten Versionen im Staub liegen lassen. Die Tatsache, dass es umgekehrt ist, zeigt, wie weit die Dinge wirklich entfernt sind. Gleiches gilt für Fill<>
das erstmalige Eigenrollen, da es sich immer noch nur um Core und Standard handelt, nicht aber um das Framework. .NET gibt es schon seit fast zwanzig Jahren und wir müssen immer noch links und rechts P / Invoke für die grundlegendsten Dinge oder unsere eigenen ...