250 votes

Pourquoi le traitement d'un tableau trié plus lent qu'un tableau non trié?

J'ai une liste de 500000 généré de façon aléatoire, Tuple<long,long,string> objets sur lesquels je suis en effectuant une simple "entre" recherche:

var data = new List<Tuple<long,long,string>>(500000);
...
var cnt = data.Count(t => t.Item1 <= x && t.Item2 >= x);

Lorsque je génère mon ensemble aléatoire et exécuter ma recherche de 100 généré de façon aléatoire les valeurs de x, les recherches se terminer dans environ quatre secondes. Connaître les grandes merveilles que le tri n'est à la recherche, cependant, j'ai décidé de trier mes données en Item1, puis, en Item2, et, enfin, en Item3 - avant de lancer mon 100 recherches. J'attend la version triée à effectuer un peu plus rapide en raison de la direction de la prévision: ma pensée a été qu'une fois qu'on arrive au point où l' Item1 == x, toutes les autres vérifications d' t.Item1 <= x de prédire la branche correctement comme "ne pas prendre", l'accélération de la queue de la partie de la recherche. À ma grande surprise, les perquisitions qui ont eu deux fois plus longtemps sur un tableau trié!

J'ai essayé de commutation autour de l'ordre dans lequel j'ai couru mes expériences, et utilisé différents de la graine pour le générateur de nombre aléatoire, mais l'effet a été le même: recherche dans un tableau non trié couru près de deux fois plus rapide que les recherches dans le même tableau, mais triés!

Quelqu'un aurait-il une bonne explication de cet étrange effet? Le code source de mes tests en suit, je suis à l'aide .NET 4.0.


private const int TotalCount = 500000;
private const int TotalQueries = 100;
private static long NextLong(Random r) {
    var data = new byte[8];
    r.NextBytes(data);
    return BitConverter.ToInt64(data, 0);
}
private class TupleComparer : IComparer<Tuple<long,long,string>> {
    public int Compare(Tuple<long,long,string> x, Tuple<long,long,string> y) {
        var res = x.Item1.CompareTo(y.Item1);
        if (res != 0) return res;
        res = x.Item2.CompareTo(y.Item2);
        return (res != 0) ? res : String.CompareOrdinal(x.Item3, y.Item3);
    }
}
static void Test(bool doSort) {
    var data = new List<Tuple<long,long,string>>(TotalCount);
    var random = new Random(1000000007);
    var sw = new Stopwatch();
    sw.Start();
    for (var i = 0 ; i != TotalCount ; i++) {
        var a = NextLong(random);
        var b = NextLong(random);
        if (a > b) {
            var tmp = a;
            a = b;
            b = tmp;
        }
        var s = string.Format("{0}-{1}", a, b);
        data.Add(Tuple.Create(a, b, s));
    }
    sw.Stop();
    if (doSort) {
        data.Sort(new TupleComparer());
    }
    Console.WriteLine("Populated in {0}", sw.Elapsed);
    sw.Reset();
    var total = 0L;
    sw.Start();
    for (var i = 0 ; i != TotalQueries ; i++) {
        var x = NextLong(random);
        var cnt = data.Count(t => t.Item1 <= x && t.Item2 >= x);
        total += cnt;
    }
    sw.Stop();
    Console.WriteLine("Found {0} matches in {1} ({2})", total, sw.Elapsed, doSort ? "Sorted" : "Unsorted");
}
static void Main() {
    Test(false);
    Test(true);
    Test(false);
    Test(true);
}

Populated in 00:00:01.3176257
Found 15614281 matches in 00:00:04.2463478 (Unsorted)
Populated in 00:00:01.3345087
Found 15614281 matches in 00:00:08.5393730 (Sorted)
Populated in 00:00:01.3665681
Found 15614281 matches in 00:00:04.1796578 (Unsorted)
Populated in 00:00:01.3326378
Found 15614281 matches in 00:00:08.6027886 (Sorted)

282voto

usr Points 74796

Lorsque vous utilisez la liste non triée tous les tuples sont accessibles à mémoire-commande. Ils ont été affectés de manière consécutive dans la mémoire RAM. Les processeurs amour accéder à la mémoire de manière séquentielle, car ils peuvent éventuellement demander à la prochaine ligne de mémoire cache, de sorte qu'il sera toujours présent en cas de besoin.

Lorsque vous êtes de tri de la liste, vous la mettez dans un ordre aléatoire , car vos clés de tri sont générés aléatoirement. Cela signifie que les accès à la mémoire de tuple membres sont imprévisibles. Le PROCESSEUR ne peut pas prélecture de la mémoire et presque tous l'accès à un n-uplet est un cache miss.

C'est un bel exemple d'un avantage spécifique de GC de gestion de la mémoire: structures de données qui ont été attribués ensemble et, ensemble, très bien. Ils ont une grande localité de référence.

La peine de défauts de cache , l'emportent sur les sauvés de la branche de prédiction de la peine dans ce cas.

Essayez de passer à un struct-n-uplet. Cela permettra de restaurer les performances, car aucun pointeur-déréférencement doit se produire lors de l'exécution d'accès tuple membres.

Chris Sinclair notes dans les commentaires que "pour TotalCount autour de 10 000 ou moins, la version triée n'effectuer plus rapidement". C'est parce que une petite liste s'inscrit entièrement dans le cache du PROCESSEUR. Les accès à la mémoire peut être imprévisible, mais la cible est toujours dans le cache. Je crois qu'il n'est encore qu'une petite peine, parce que même une charge de cache prend quelques cycles. Mais cela ne semble pas être un problème parce que le CPU peut jongler avec de multiples circulation des charges, augmentant ainsi le débit. Chaque fois que le PROCESSEUR atteint une attente pour mémoire, il sera toujours à la vitesse supérieure dans le volet enseignement de la file d'attente que de nombreuses opérations de mémoire comme il peut. Cette technique est utilisée pour masquer la latence.

Ce genre de comportement montre combien il est difficile de prédire les performances sur les Processeurs modernes. Le fait que nous sommes seulement 2x plus lent lorsqu'on passe du séquentiel au hasard d'accès à la mémoire me dire tout ce qui se passe sous les couvertures pour cacher la latence de la mémoire. Un accès à la mémoire peut ralentir le CPU de 50 à 200 cycles. Étant donné que le nombre que l'on peut attendre que le programme devienne >10x plus lent lors de l'introduction aléatoire l'accès à la mémoire.

5voto

Emperor Orionii Points 464

LINQ ne sais pas si vous la liste est triée ou non.

Depuis Comte avec prédicat paramètre de la méthode d'extension pour tous les IEnumerables, je pense qu'il ne sait même pas si il fonctionne sur la collecte efficace de l'accès aléatoire. Donc, tout simplement, il vérifie chaque élément et de l' Usr a expliqué pourquoi le rendement obtenu plus bas.

Pour exploiter les avantages de performance de tableau trié (tels que les binaires de recherche), vous aurez à faire un peu plus de codage.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X