15 votes

Pourquoi Java n'utilise-t-il pas tous les cœurs de mon processeur de manière efficace ?

J'utilise Ubuntu sur une machine dotée d'un processeur à quatre cœurs. J'ai écrit un code Java de test qui génère un nombre donné de processus qui incrémentent simplement une variable volatile pour un nombre donné d'itérations lors de l'exécution.

Je m'attendrais à ce que le temps de fonctionnement n'augmente pas de manière significative tant que le nombre de threads est inférieur ou égal au nombre de cœurs, c'est-à-dire 4. En fait, ce sont les temps que j'obtiens en utilisant le "temps réel" de l'UNIX time commandement :

1 fil : 1.005s

2 fils : 1.018s

3 fils : 1.528s

4 fils : 1.982s

5 fils : 2.479s

6 fils : 2.934s

7 fils : 3.356s

8 fils : 3.793s

Cela montre que l'ajout d'un thread supplémentaire n'augmente pas le temps comme prévu, mais le temps fait augmenter avec 3 et 4 fils.

Au début, j'ai pensé que cela pouvait être dû au fait que le système d'exploitation empêchait la JVM d'utiliser tous les cœurs, mais j'ai lancé l'opération suivante top Il a clairement montré qu'avec 3 threads, 3 cœurs fonctionnaient à ~100%, et qu'avec 4 threads, 4 cœurs étaient au maximum.

Ma question est la suivante : pourquoi le code qui s'exécute sur 3/4 CPU n'a pas à peu près la même vitesse que lorsqu'il s'exécute sur 1/2 CPU ? Parce qu'il est fonctionnant en parallèle sur tous les cœurs.

Voici ma méthode principale à titre de référence :

class Example implements Runnable {

    // using this so the compiler does not optimise the computation away
    volatile int temp;

    void delay(int arg) {
        for (int i = 0; i < arg; i++) {
            for (int j = 0; j < 1000000; j++) {
                this.temp += i + j;
            }
        }
    }

    int arg;
    int result;

    Example(int arg) {
        this.arg = arg;
    }

    public void run() {
        delay(arg);
        result = 42;
    }

    public static void main(String args[]) {

    // Get the number of threads (the command line arg)

    int numThreads = 1;
    if (args.length > 0) {
        try {
            numThreads = Integer.parseInt(args[0]);
        } catch (NumberFormatException nfe) {
            System.out.println("First arg must be the number of threads!");
        }
    }

    // Start up the threads

    Thread[] threadList = new Thread[numThreads];
    Example[] exampleList = new Example[numThreads];
    for (int i = 0; i < numThreads; i++) {
        exampleList[i] = new Example(1000);
        threadList[i] = new Thread(exampleList[i]);
        threadList[i].start();
    }

    // wait for the threads to finish

    for (int i = 0; i < numThreads; i++) {
        try {
            threadList[i].join();
            System.out.println("Joined with thread, ret=" + exampleList[i].result);
        } catch (InterruptedException ie) {
            System.out.println("Caught " + ie);
        }
    }
}

7voto

Peter Lawrey Points 229686

L'utilisation de plusieurs processeurs est utile jusqu'à ce que vous saturiez certaines ressources sous-jacentes.

Dans votre cas, la ressource sous-jacente n'est pas le nombre de processeurs mais le nombre de caches L1 dont vous disposez. Dans votre cas, il semble que vous ayez deux cœurs, avec chacun un cache de données L1 et puisque vous le frappez avec une écriture volatile, ce sont les caches L1 qui sont votre facteur limitant ici.

Essayez d'accéder moins au cache L1 avec

public class Example implements Runnable {
    // using this so the compiler does not optimise the computation away
    volatile int temp;

    void delay(int arg) {
        for (int i = 0; i < arg; i++) {
            int temp = 0;
            for (int j = 0; j < 1000000; j++) {
                temp += i + j;
            }
            this.temp += temp;
        }
    }

    int arg;
    int result;

    Example(int arg) {
        this.arg = arg;
    }

    public void run() {
        delay(arg);
        result = 42;
    }

    public static void main(String... ignored) {

        int MAX_THREADS = Integer.getInteger("max.threads", 8);
        long[] times = new long[MAX_THREADS + 1];
        for (int numThreads = MAX_THREADS; numThreads >= 1; numThreads--) {
            long start = System.nanoTime();

            // Start up the threads

            Thread[] threadList = new Thread[numThreads];
            Example[] exampleList = new Example[numThreads];
            for (int i = 0; i < numThreads; i++) {
                exampleList[i] = new Example(1000);
                threadList[i] = new Thread(exampleList[i]);
                threadList[i].start();
            }

            // wait for the threads to finish

            for (int i = 0; i < numThreads; i++) {
                try {
                    threadList[i].join();
                    System.out.println("Joined with thread, ret=" + exampleList[i].result);
                } catch (InterruptedException ie) {
                    System.out.println("Caught " + ie);
                }
            }
            long time = System.nanoTime() - start;
            times[numThreads] = time;
            System.out.printf("%d: %.1f ms%n", numThreads, time / 1e6);
        }
        for (int i = 2; i <= MAX_THREADS; i++)
            System.out.printf("%d: %.3f time %n", i, (double) times[i] / times[1]);
    }
}

Sur mon ordinateur portable double cœur et hyperthreadé, il produit sous la forme threads: factor

2: 1.093 time 
3: 1.180 time 
4: 1.244 time 
5: 1.759 time 
6: 1.915 time 
7: 2.154 time 
8: 2.412 time 

par rapport au test original de

2: 1.092 time 
3: 2.198 time 
4: 3.349 time 
5: 3.079 time 
6: 3.556 time 
7: 4.183 time 
8: 4.902 time 

Le cache L3 est une ressource souvent surutilisée. Il est partagé par tous les processeurs et, bien qu'il permette un certain degré de concurrence, il ne s'étend pas bien au-delà de plusieurs processeurs. Je vous suggère de vérifier ce que fait votre code Exemple et de vous assurer qu'il peut fonctionner indépendamment et ne pas utiliser de ressources partagées. Par exemple, la plupart des puces ont un nombre limité de FPU.

5voto

Affe Points 24993

Le Core i5 du Lenovo X1 Carbon n'est pas un processeur quadri-cœur. C'est un processeur à deux cœurs avec hyperthreading. Si vous n'effectuez que des opérations triviales qui n'entraînent pas d'arrêts fréquents et prolongés du pipeline, le planificateur de l'hyperthreading n'aura pas beaucoup d'occasions d'intégrer d'autres opérations dans le pipeline bloqué et les performances ne seront pas équivalentes à celles de quatre cœurs réels.

2voto

Tim B Points 19851

Plusieurs facteurs peuvent limiter l'efficacité du multithreading d'une application.

  1. Saturation d'une ressource telle que la bande passante de la mémoire/du bus/etc.

  2. Problèmes de verrouillage/contention (par exemple si les threads doivent constamment attendre que les autres se terminent).

  3. Les autres processus en cours d'exécution sur le système.

Dans votre cas, vous utilisez un entier volatil auquel tous les threads ont accès, ce qui signifie que les threads doivent constamment envoyer la nouvelle valeur de cet entier entre eux. Cela entraîne un certain niveau de contention et d'utilisation de la mémoire et de la bande passante.

Essayez de faire en sorte que chaque thread travaille sur sa propre portion de données sans variable volatile. Cela devrait réduire toute forme de conflit.

1voto

TwoThe Points 4053

Si vous l'exécutez sur le Core i5 (d'après ce que Google me dit à propos du Lenovo X1 Carbon), alors vous avez une double cœur machine avec 2 hyper-cores. L'i5 se présente au système d'exploitation - et donc à Java - comme un quad-core, donc les hyper-cores sont utilisés comme de vrais cœurs, mais tout ce qu'ils font est d'accélérer le changement de contexte des threads.

C'est pourquoi vous obtenez la différence minimale attendue dans le temps d'exécution avec 2 threads (1 par cœur réel), et pourquoi le temps n'augmente pas linéairement avec des threads supplémentaires, parce que les 2 hyper-cœurs prennent une charge mineure des cœurs réels.

0voto

user3140929 Points 1

Il y a déjà deux bonnes réponses à votre question, les deux sont parfaites pour expliquer ce qui se passe.

Regardez votre processeur, la plupart des "quad core" d'intel sont en fait des dual core, qui simulent un quad core do OS (oui, ils vous disent que vous avez 4 core, mais vous n'en avez que 2 en fait...). C'est la meilleure explication à votre problème, car le temps s'incrémente comme un processeur double cœur.

Si vous avez un vrai 4 cœurs, l'autre réponse est que vous avez une certaine concurrence.

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