79 votes

Différence de performance énorme (26 fois plus rapide) lors de la compilation pour 32 et 64 bits

J'ai essayé de mesurer la différence de l'utilisation d'un for et foreach lors de l'accès à des listes de types valeur et les types référence.

J'ai utilisé la classe suivante pour faire le profilage.

public static class Benchmarker
{
    public static void Profile(string description, int iterations, Action func)
    {
        Console.Write(description);

        // Warm up
        func();

        Stopwatch watch = new Stopwatch();

        // Clean up
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();

        watch.Start();
        for (int i = 0; i < iterations; i++)
        {
            func();
        }
        watch.Stop();

        Console.WriteLine(" average time: {0} ms", watch.Elapsed.TotalMilliseconds / iterations);
    }
}

J'ai utilisé double pour mon type de valeur. Et j'ai créé ce "faux" de classe pour tester les types de référence:

class DoubleWrapper
{
    public double Value { get; set; }

    public DoubleWrapper(double value)
    {
        Value = value;
    }
}

Enfin, j'ai couru ce code et comparé les différences de temps.

static void Main(string[] args)
{
    int size = 1000000;
    int iterationCount = 100;

    var valueList = new List<double>(size);
    for (int i = 0; i < size; i++) 
        valueList.Add(i);

    var refList = new List<DoubleWrapper>(size);
    for (int i = 0; i < size; i++) 
        refList.Add(new DoubleWrapper(i));

    double dummy;

    Benchmarker.Profile("valueList for: ", iterationCount, () =>
    {
        double result = 0;
        for (int i = 0; i < valueList.Count; i++)
        {
             unchecked
             {
                 var temp = valueList[i];
                 result *= temp;
                 result += temp;
                 result /= temp;
                 result -= temp;
             }
        }
        dummy = result;
    });

    Benchmarker.Profile("valueList foreach: ", iterationCount, () =>
    {
        double result = 0;
        foreach (var v in valueList)
        {
            var temp = v;
            result *= temp;
            result += temp;
            result /= temp;
            result -= temp;
        }
        dummy = result;
    });

    Benchmarker.Profile("refList for: ", iterationCount, () =>
    {
        double result = 0;
        for (int i = 0; i < refList.Count; i++)
        {
            unchecked
            {
                var temp = refList[i].Value;
                result *= temp;
                result += temp;
                result /= temp;
                result -= temp;
            }
        }
        dummy = result;
    });

    Benchmarker.Profile("refList foreach: ", iterationCount, () =>
    {
        double result = 0;
        foreach (var v in refList)
        {
            unchecked
            {
                var temp = v.Value;
                result *= temp;
                result += temp;
                result /= temp;
                result -= temp;
            }
        }

        dummy = result;
    });

    SafeExit();
}

J'ai sélectionné Release et Any CPU options, a couru le programme et a obtenu les horaires suivants:

valueList for:  average time: 483,967938 ms
valueList foreach:  average time: 477,873079 ms
refList for:  average time: 490,524197 ms
refList foreach:  average time: 485,659557 ms
Done!

Puis j'ai sélectionné la Libération et x64 options, a couru le programme et a obtenu les horaires suivants:

valueList for:  average time: 16,720209 ms
valueList foreach:  average time: 15,953483 ms
refList for:  average time: 19,381077 ms
refList foreach:  average time: 18,636781 ms
Done!

Pourquoi est-x64 bits version beaucoup plus vite? Je m'attendais à une certaine différence, mais pas quelque chose d'aussi grand.

Je n'ai pas accès à d'autres ordinateurs. Pourriez vous s'il vous plaît l'exécuter sur votre machine et me dire les résultats? Je suis à l'aide de Visual Studio 2015 et j'ai un processeur Intel Core i7 930.

Voici l' SafeExit() méthode, de sorte que vous pouvez compiler/exécuter par vous-même:

private static void SafeExit()
{
    Console.WriteLine("Done!");
    Console.ReadLine();
    System.Environment.Exit(1);
}

Comme demandé, à l'aide de double? à la place de mon DoubleWrapper:

N'importe quel CPU

valueList for:  average time: 482,98116 ms
valueList foreach:  average time: 478,837701 ms
refList for:  average time: 491,075915 ms
refList foreach:  average time: 483,206072 ms
Done!

x64

valueList for:  average time: 16,393947 ms
valueList foreach:  average time: 15,87007 ms
refList for:  average time: 18,267736 ms
refList foreach:  average time: 16,496038 ms
Done!

Dernière mais non des moindres: la création d'un x86 profil me donne presque les mêmes résultats de l'utilisation de Any CPU.

87voto

usr Points 74796

Je peux reproduire ce sur 4.5.2. Pas de RyuJIT ici. Les versions x86 et x64 disassemblies paraître raisonnable. Gamme de chèques et ainsi de suite sont les mêmes. La même structure de base. Pas de déroulement de la boucle.

x86 utilise un ensemble différent de flotter instructions. L'exécution de ces instructions semble être comparable avec le x64 instructions à l'exception de la division:

  1. Les 32 bits de x87 float instructions d'utilisation 10 octets de précision à l'interne.
  2. Précision étendue de la division est super lent.

L'opération de division fait la version 32 bits extrêmement lent. Décommentant la division égale des performances dans une large mesure (32 bits vers le bas à partir de 430ms à 3,25 ms).

Peter Cordes souligne que l'instruction des latences de deux virgule flottante unités ne sont pas que dissemblables. Peut-être que certains des résultats intermédiaires sont les nombres dénormalisés ou NaN. Ces pourrait déclencher une voie lente dans l'une des unités. Ou, peut-être que les valeurs divergent entre les deux implémentations en raison de 10 octets contre 8 octets flottante de précision.

Peter Cordes souligne également que tous les résultats intermédiaires sont NaN... la Suppression de ce problème (valueList.Add(i + 1) , de sorte que pas de diviseur de zéro), principalement à égaliser les résultats. Apparemment, les 32 bits de code n'aime pas NaN opérandes. Nous allons imprimer des valeurs intermédiaires: if (i % 1000 == 0) Console.WriteLine(result);. Cela confirme que les données sont maintenant sain d'esprit.

Lorsque l'analyse comparative vous avez besoin de la comparer à une charge de travail réaliste. Mais qui aurait pensé qu'un innocent division peut gâcher votre référence?!

Essayez simplement en additionnant les nombres pour obtenir un meilleur indice de référence.

La Division et le modulo sont toujours très lent. Si vous modifiez la BCL Dictionary code tout simplement de ne pas utiliser l'opérateur modulo pour calculer le seau de l'indice de performance mesurables améliore. C'est la lenteur de la division est.

Voici les 32 bits de code:

enter image description here

64 bits de code (même structure, division rapide):

enter image description here

Ce n'est pas vectorisé malgré les instructions SSE utilisé.

31voto

Peter Cordes Points 1375

valueList[i] = i, à partir de i=0, de sorte que la première itération de boucle n' 0.0 / 0.0. Ainsi, chaque opération dans l'ensemble de votre test est fait avec NaNs.

Comme @usr montré dans le démontage de sortie, la version 32 bits utilisé x87 virgule flottante, le tout en 64 bits utilisés ESS en virgule flottante.

Je ne suis pas un expert sur la performance avec NaNs, ou la différence entre x87 et l'ESS, mais je pense que cela explique la 26x perf différence. Je parie que tes résultats seront un lot plus étroite entre 32 et 64 bits, si vous initialisez valueList[i] = i+1. (mise à jour: usr confirmé le que de ce fait 32 et 64 bits, les performances assez proches.)

La Division est très lente par rapport à d'autres opérations. Voir mes commentaires sur @usr réponse. Voir aussi http://agner.org/optimize/ pour des tonnes de trucs sur le matériel et l'optimisation de l'asm et C/C++, certains de il un intérêt à C#. Il a de l'instruction tables de latence et de débit pour la plupart des instructions pour toutes les dernières les Processeurs x86.

Cependant, 10B x87 fdiv n'est pas beaucoup plus lent que SSE2 du 8B double précision divsd, pour des valeurs normales. IDK sur les différences de perf avec NaNs, infinis, ou denormals.

Ils ont différents contrôles pour ce qui se passe avec NaNs et d'autres FPU exceptions près, cependant. La FPU x87 mot de commande est séparé de l'ESS arrondi / exception registre de contrôle (MXCSR). Si x87 est l'obtention d'un PROCESSEUR exception pour chaque division, mais de l'ESS n'est-ce pas, qui explique facilement le facteur de 26. Ou peut-être il y a juste une différence de performance que de grands lors de la manipulation de NaNs. Le matériel n'est pas optimisé pour le barattage travers NaN après NaN.

IDK si l'ESS contrôles pour éviter les ralentissements avec denormals va entrer en jeu ici, car je crois result sera NaN tout le temps. IDK si C# définit la denormals-sont-indicateur de zéro dans le MXCSR, ou de la chasse d'eau-de-zero-drapeau (qui écrit des zéros en premier lieu, au lieu de traiter denormals à zéro lors de la lecture à l'arrière).

J'ai trouvé un Intel article à propos de l'ESS à virgule flottante contrôles, contrastant avec la FPU x87 mot de contrôle. Il n'a pas beaucoup à dire à propos de NaN, cependant. Il se termine par ceci:

Conclusion

Pour éviter la sérialisation et les problèmes de performance en raison de denormals et underflow numéros, utilisez le SSE et SSE2 instructions pour la configuration de Rincer à Zéro et Denormals-Sont-Zéro modes dans le matériel de permettre à plus haute performance pour floating-point des applications.

IDK si cela permet de tout avec une division par zéro.

pour vs foreach

Il pourrait être intéressant de tester un corps de boucle, qui est le débit limité, plutôt que d'être simplement une seule boucle-effectuer la chaîne de dépendances. Comme il est, l'ensemble de l'ouvrage dépend des résultats précédents; il n'y a rien pour le CPU pour faire en parallèle (autres que les limites de vérifier la matrice suivante, charge alors que la mul/div chaîne est en cours d'exécution).

Vous pouvez voir plus de différence entre les méthodes si le "vrai travail" occupé plus de la Cpu de l'exécution des ressources. Aussi, sur la pré-Intel Sandybridge, il y a une grande différence entre une boucle de montage dans la 28uop boucle tampon ou pas. Vous obtenez instruction décoder les goulots d'étranglement si pas, esp. lorsque le cours moyen de l'instruction de la longueur de la plus longue (ce qui arrive avec l'ESS). Les Instructions qui décodent à plus d'une uop permettra également de limiter décodeur débit, à moins qu'ils arrivent dans un modèle qui est agréable pour les décodeurs (par exemple 2-1-1). Donc une boucle avec plus d'instructions de la boucle de la surcharge peut faire la différence entre une boucle de montage dans le 28-entrée uop cache ou pas, ce qui est une grosse affaire sur Nehalem, et parfois utile sur Sandybridge et plus tard.

1voto

gnasher729 Points 5011

Nous avons l'observation que 99,9% de toutes les opérations en virgule flottante impliquera NaN, ce qui est pour le moins, très rare (trouvé par Peter Cordes en premier). Nous avons une autre expérience par l'usr, qui a constaté que la suppression de la division de la notice de la différence de temps presque complètement disparaître.

Le fait est, cependant, que les NaN sont à seulement généré en raison de la très première division calcule 0.0 / 0.0, qui donne la première NaN. Si les divisions ne sont pas réalisées, le résultat sera toujours 0.0, et nous allons toujours calculer 0.0 * temp -> 0.0, 0.0 + temp -> temp, temp - temp = 0.0. Donc supprimer la division n'est pas seulement supprimer les divisions, mais également supprimé les NaNs. Je m'attends à ce que les NaN sont en fait le problème, et que l'on mise en œuvre des poignées de NaN est que très lentement, tandis que l'autre n'ont pas le problème.

Il serait intéressant de départ de la boucle de i = 1 et mesurer à nouveau. Les quatre opérations de résultat * temp + temp / temp - temp effectivement ajouter (1 - temp) si nous n'aurions pas inhabituels chiffres (0, infini, NaN) pour la plupart des opérations.

Le seul problème pourrait être que la division donne toujours un résultat sous forme d'entier, et certains de la division des implémentations ont des raccourcis lorsque le résultat correct, ne pas utiliser de nombreuses bits. Par exemple, la division 310.0 / 31.0 donne 10.0 comme les quatre premiers bits avec un reste de 0.0, et certaines implémentations peuvent arrêter l'évaluation de la participation restante de 50 ou alors, bits, tandis que d'autres ne le peuvent pas. Si il y a un significiant différence, puis à partir de la boucle, avec un résultat = 1.0 / 3.0 ferait une différence.

-2voto

series0ne Points 4389

Il peut y avoir plusieurs raisons pourquoi cela est en cours d'exécution plus rapide, en 64 bits sur votre machine. La raison pour laquelle je demande quel est le PROCESSEUR que vous utilisez parce que quand les Processeurs 64 bits d'abord fait leur apparition, AMD et Intel ont des mécanismes différents pour gérer 64 bits de code.

Architecture de processeur:

Intel PROCESSEUR de l'architecture a été purement 64 bits. Afin d'exécuter du code 32 bits, 32 bits instructions nécessaires à convertir (à l'intérieur de la CPU), à 64 bits instructions avant l'exécution.

AMD PROCESSEUR de l'architecture a été la construction de 64 bits à droite sur le dessus de leur architecture 32 bits, c'est, c'est essentiellement une architecture 32 bits à 64 bits extentions - il n'y a pas de code de processus de conversion.

Évidemment, c'était il y a quelques années maintenant, donc j'ai aucune idée de la façon dont la technologie a changé, mais essentiellement, vous vous en doutez 64 bits de code à mieux performer sur une machine 64 bits puisque le PROCESSEUR est capable de travailler avec le double de la quantité de bits par instruction.

.NET JIT

Il est fait valoir que .NET (et d'autres langages comme Java) sont capables de surpasser les langages tels que le C++ parce que de la façon dont le compilateur JIT est en mesure d'optimiser votre code, selon l'architecture de votre processeur. À cet égard, vous trouverez peut-être que le compilateur JIT en utilisant quelque chose en 64bit architecture qui, éventuellement, n'était pas disponible ou nécessaire une solution de contournement lors de l'exécution de 32 bits.

Note:

Plutôt que d'utiliser DoubleWrapper, avez-vous envisagé d'utiliser des Nullable<double> ou l'abréviation de la syntaxe: double? - je serais curieux de voir si cela a un impact sur vos tests.

Note 2: Certaines personnes semblent être l'amalgame entre mes commentaires sur 64bit l'architecture IA-64. Juste pour préciser, dans ma réponse, 64bit désigne x86-64 et 32 bits désigne x86-32. Rien ici référencé IA-64!

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