Pourquoi le traitement d'un tableau trié plus rapide que le traitement d'un tableau non-trié?

Voici un morceau de code C++ qui montre une partie très particulière de comportement. Pour une raison étrange, le tri des données miraculeusement rend le code près de six fois plus rapide:

#include <algorithm>
#include <ctime>
#include <iostream>

int main()
{
    //Generate data
    const unsigned arraySize = 32768;
    int data[arraySize];

    for (unsigned c = 0; c < arraySize; ++c)
        data[c] = std::rand() % 256;


    //!!! With this, the next loop runs faster.
    std::sort(data, data + arraySize);


    //Test
    clock_t start = clock();
    long long sum = 0;

    for (unsigned i = 0; i < 100000; ++i)
    {
        //Primary loop
        for (unsigned c = 0; c < arraySize; ++c)
        {
            if (data[c] >= 128)
                sum += data[c];
        }
    }

    double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;

    std::cout << elapsedTime << std::endl;
    std::cout << "sum = " << sum << std::endl;
}
  • Sans std::sort(data, data + arraySize);, le code s'exécute dans 11.54 secondes.
  • Avec les données triées, le code s'exécute dans 1.93 secondes.

Au départ, j'ai pensé que cela pourrait être juste une langue ou le compilateur anomalie, j'ai donc essayé de Java:

import java.util.Arrays;
import java.util.Random;

public class Main
{
    public static void main(String[] args)
    {
        //Generate data
        int arraySize = 32768;
        int data[] = new int[arraySize];

        Random rnd = new Random(0);
        for (int c = 0; c < arraySize; ++c)
            data[c] = rnd.nextInt() % 256;


        //!!! With this, the next loop runs faster
        Arrays.sort(data);


        //Test
        long start = System.nanoTime();
        long sum = 0;

        for (int i = 0; i < 100000; ++i)
        {
            //Primary loop
            for (int c = 0; c < arraySize; ++c)
            {
                if (data[c] >= 128)
                    sum += data[c];
            }
        }

        System.out.println((System.nanoTime() - start) / 1000000000.0);
        System.out.println("sum = " + sum);
    }
}

avec un semblable, mais moins extrême résultat.


Ma première pensée a été que le tri regroupe les données dans le cache, mais ensuite j'ai pensé comment stupide que c'était parce que le tableau a été qui vient d'être généré.

  • Ce qui se passe?
  • Pourquoi le traitement d'un tableau trié plus rapide que le traitement d'un tableau non-trié? Le code est en résumant certains indépendants termes, l'ordre n'a plus d'importance.
  • Juste pour le record. Sur Windows / VS2017 / i7-6700K 4GHz il n'y a PAS de différence entre les deux versions. Il prend 0,6 s pour les deux cas. Si le nombre d'itérations de la boucle externe est augmenté de 10 fois le temps d'exécution augmente de 10 fois trop à 6s dans les deux cas.
  • un compilateur qui utilise un cmov ou d'autres dépourvu de branches de mise en œuvre (comme auto-vectorisation avec pcmpgtd) auront des performances pas de données dépend de la CPU. Mais si c'est le branchu, il sera tri-dépendant à un CPU avec de l'exécution spéculative. (Même à haute performance dans l'ordre Cpu usage de la branche de prédiction pour éviter d'extraction/décoder des bulles sur les prises de branches; la miss peine est plus petite).
  • Oups... re: Effondrement et le Spectre
  • a-t-elle quelque chose à voir avec les deux? Je n'ai pas lu beaucoup sur les deux
  • deux de ces failles de sécurité de rentrer dans une large catégorie de vulnérabilités classés comme “direction de la cible d'injection” attaques
  • Il a essayé avec 200M de tableau sur la JVM hotspot 1.8. Pas de différence pour triés et non triés. Toutes les explications?
  • Sur le dessus de ma tête: 1) La JVM peut-être finalement assez intelligent pour utiliser conditionnelle se déplace. 2) Le code est liés à la mémoire. 200 m est trop grand pour tenir dans le cache du PROCESSEUR. Donc, la performance peut être un goulot d'étranglement par la bande passante de la mémoire au lieu de ramification.
  • 2). Je pensais que la prédiction de la table assure le suivi des patrons(indépendamment des variables réelles qui ont été vérifiés pour ce motif) et de changer la prédiction de la sortie en fonction de l'histoire. Pourriez-vous me donner une raison, pourquoi un super grand tableau ne serait pas bénéficier de direction de la prévision?
  • Il le fait, mais quand le tableau est très grande, d'autant plus un facteur probable entre dans le jeu - la bande passante mémoire. La mémoire est ce n'est pas plat. Accès à la mémoire est très lent, et il ya une quantité limitée de la bande passante. De sur-simplifier les choses, il y a une limite au nombre d'octets qui peuvent être transférés entre la CPU et de la mémoire en un montant fixe de temps. Code Simple comme celui de cette question sera probablement frappé de cette limite, même si elle est ralentie par mispredictions. Ce n'est pas le cas avec un tableau de 32768 (128 KO) car il s'inscrit dans le cache L2 du PROCESSEUR.
  • Il y a une nouvelle faille de sécurité a appelé BranchScope: cs.ucr.edu/~nael/pubs/asplos18.pdf
  • Pour l'enregistrement de vos données n'ont pas besoin d'être triés, seulement partitionné qui est beaucoup plus rapide pour l'opération.
  • Une autre observation est que vous n'avez pas besoin de trier le tableau, mais vous avez juste besoin de la partition avec la valeur 128. Le tri est n*log(n), alors que le partitionnement est seulement linéaire. Fondamentalement, c'est juste une exécution du tri rapide étape de partitionnement avec le pivot choisi à 128. Malheureusement, en C++ il y a juste nth_element fonction de partition en position, et non par valeur.
  • Qu'en est std::partition()?
  • En effet, std::la partition est la réponse correcte. Merci
  • Est-il une explication pour pourquoi il faut le même temps, en dépit de la direction de la prévision encore utilisé?
  • FWIW: Le développeur moyen ne jamais avoir d'expérience avec de très uniques matériel de questions de ce genre. Direction de la prévision n'est pas encore connu pour le développeur moyen.
  • Sur Linux avec un processeur Intel i3-7020U (4) @ 2.3 GHz, le speed-up est tout le contraire quand il s'agit de la langue. Le temps d'exécution pour le C++ permet de réduire de 29.7285 à 10.3184 (près de 3 fois). Mais quand j'utilise Java, il va de 13.3513 à 3.2957 (près de 4 fois).
  • Pouvez-vous en fournir des preuves? Ce banc de marque, montre une très grande différence.

InformationsquelleAutor GManNickG | 2012-06-27