Voici une autre version pour nous, les utilisateurs du Framework abandonnés par Microsoft. Il est 4 fois plus vite que Array.Clear
et plus rapide que la solution de Panos Theof et Eric J de et un parallèle Petar Petrov - jusqu'à deux fois plus rapide pour les grands tableaux.
Je veux d'abord vous présenter l'ancêtre de la fonction, car cela facilite la compréhension du code. En termes de performances, cela correspond à peu près au code de Panos Theof, et pour certaines choses qui peuvent déjà être suffisantes:
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);
}
Comme vous pouvez le voir, cela est basé sur le doublement répété de la partie déjà initialisée. C'est simple et efficace, mais cela va à l'encontre des architectures de mémoire modernes. D'où est née une version qui n'utilise le doublage que pour créer un bloc de départ compatible avec le cache, qui est ensuite dynamité de manière itérative sur la zone cible:
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);
}
Remarque: le code précédent devait (count + 1) >> 1
servir de limite à la boucle de doublage pour garantir que l'opération de copie finale contienne suffisamment de fourrage pour couvrir tout ce qui reste. Ce ne serait pas le cas pour les dénombrements impairs sicount >> 1
devaient être utilisés à la place. Pour la version actuelle, cela n'a pas d'importance car la boucle de copie linéaire va prendre le moindre mou.
La taille d'une cellule de tableau doit être transmise en tant que paramètre car - l'esprit boggle - les génériques ne sont pas autorisés à utiliser sizeof
sauf s'ils utilisent une contrainte ( unmanaged
) qui peut ou non devenir disponible à l'avenir. Des estimations erronées ne sont pas un gros problème, mais les performances sont meilleures si la valeur est exacte, pour les raisons suivantes:
Sous-estimer la taille de l'élément peut entraîner des tailles de bloc supérieures à la moitié du cache L1, augmentant ainsi la probabilité que les données de la source de copie soient expulsées de L1 et doivent être récupérées à partir de niveaux de cache plus lents.
La surestimation de la taille de l'élément entraîne une sous-utilisation du cache L1 du processeur, ce qui signifie que la boucle de copie de bloc linéaire est exécutée plus souvent qu'elle ne le serait avec une utilisation optimale. Ainsi, la surcharge de boucle / appel fixe est engagée plus que strictement nécessaire.
Voici une référence comparant mon code Array.Clear
et les trois autres solutions mentionnées précédemment. Les timings sont pour remplir des tableaux entiers ( Int32[]
) des tailles données. Afin de réduire la variation causée par les caprices du cache, etc., chaque test a été exécuté deux fois de suite et les temporisations ont été prises pour la deuxième exécution.
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
Si les performances de ce code ne sont pas suffisantes, une avenue prometteuse serait de paralléliser la boucle de copie linéaire (avec tous les threads utilisant le même bloc source), ou notre bon vieil ami P / Invoke.
Remarque: l'effacement et le remplissage des blocs sont normalement effectués par des routines d'exécution qui se ramifient en code hautement spécialisé à l'aide d'instructions MMX / SSE et ainsi de suite, donc dans tout environnement décent, on appellerait simplement l'équivalent moral respectif std::memset
et serait assuré de niveaux de performance professionnels. IOW, de plein droit, la fonction de bibliothèque Array.Clear
devrait laisser toutes nos versions roulées à la main dans la poussière. Le fait que ce soit l'inverse montre à quel point les choses sont vraiment éloignées. Il en va de même pour avoir à rouler le sien Fill<>
en premier lieu, car il n'est toujours que dans Core et Standard mais pas dans le Framework. .NET existe depuis près de vingt ans maintenant et nous devons encore P / Invoke gauche et droite pour les choses les plus élémentaires ou rouler les nôtres ...