Question

J'écris une application Java multi-thread qui s'exécute sur le processeur Nehalem.Cependant, j'ai un problème : à partir de 4 threads, je ne vois presque pas l'accélération dans mon application.

J'ai fait un test simple.J'ai créé un fil qui alloue simplement un grand tableau et donne accès à des entrées aléatoires dans le tableau.Ainsi, lorsque j'exécute le nombre de threads, la durée d'exécution ne devrait pas changer (en supposant que je ne dépasse pas le nombre de cœurs de processeur disponibles).Mais ce que j’ai observé, c’est que l’exécution de 1 ou 2 threads prend presque le même temps, mais que l’exécution de 4 ou 8 threads est nettement plus lente.Donc, avant d'essayer de résoudre les problèmes d'algorithme et de synchronisation dans mon application, je veux savoir quelle est la parallélisation maximale possible que je peux réaliser.

j'ai utilisé -XX:+UseNUMA Option JVM, les tableaux doivent donc être alloués dans la mémoire à proximité des threads correspondants.

P.S.Si les threads effectuaient un calcul mathématique simple, il n'y avait pas de perte de temps pour 4 et même 8 threads, j'ai donc conclu que lorsque les threads accèdent à la mémoire, j'ai quelques problèmes.

Toute aide ou idée est appréciée, merci.


MODIFIER

Merci à tous pour les réponses.Je vois que je ne me suis pas assez bien expliqué.

Avant d'essayer d'éliminer les problèmes de synchronisation dans mon application, j'ai effectué un test simple qui vérifie la meilleure parallélisation possible.Le code est comme suit:

public class TestMultiThreadingArrayAccess {
    private final static int arrSize = 40000000;

    private class SimpleLoop extends Thread {
        public void run() {
            int array[] = new int[arrSize];
            for (long i = 0; i < arrSize * 10; i++) {
                array[(int) ((i * i) % arrSize)]++; // randomize a bit the access to the array
            }
            long sum = 0;
            for (int i = 0; i < arrSize; i++)
                sum += array[i];
        }
    }

    public static void main(String[] args) {
        TestMultiThreadingArrayAccess test = new TestMultiThreadingArrayAccess();
        for (int threadsNumber : new int[] { 1, 2, 4, 8 }) {
            Statistics timer = new Statistics("Executing " + threadsNumber+ " threads"); // Statistics is a simple helper class that measures the times
            timer.start();
            test.doTest(threadsNumber);
            timer.stop();
            System.out.println(timer.toString());
        }
    }

    public void doTest(int threadsNumber) {
        Thread threads[] = new Thread[threadsNumber];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new SimpleLoop();
            threads[i].start();
        }

        for (int i = 0; i < threads.length; i++)
            try {
                threads[i].join();
            } catch (InterruptedException e) {
            };
    }
}

Ainsi, comme vous le voyez, il n'y a aucune synchronisation du tout dans ce minitest et l'allocation du tableau se fait également à l'intérieur du thread, il doit donc être placé dans la partie de la mémoire accessible rapidement.De plus, il n'y a aucun conflit de mémoire dans ce code.Toujours pour 4 threads, il y a une baisse de 30% du temps d'exécution, et 8 threads s'exécutent deux fois plus lentement.Comme pour le code, j'attends simplement que tous les threads terminent leur travail, et comme leur travail est indépendant, le nombre de threads ne devrait pas affecter le temps total d'exécution.

Sur la machine, 2 processeurs Nehalem hyperthread quadricœurs sont installés (16 processeurs au total), donc avec 8 threads, chacun peut capter exclusivement son processeur.

Lorsque j'ai essayé d'exécuter ce test avec un tableau plus petit (20 000 entrées), la baisse du temps d'exécution de 4 threads était de 7 % et de 8 threads de 14 %, ce qui est satisfaisant.Mais lorsque j'essaie d'exploiter un accès aléatoire sur un grand tableau (40 millions d'entrées), les temps d'exécution augmentent considérablement, donc je pense qu'il y a un problème avec l'accès à de gros morceaux de mémoire (parce qu'ils ne rentrent pas dans la mémoire cache ?) -de manière efficace.

Y a-t-il des idées pour résoudre ce problème ?

J'espère que cela clarifie mieux la question, merci encore.

Était-ce utile?

La solution

Le goulot d'étranglement dans le test est la bande passante du processeur vers la mémoire.Même lorsque la mémoire locale est disponible, elle sera partagée par un certain nombre de threads.(La mémoire est locale sur un nœud, pas sur un cœur spécifique.) Une fois que le processeur peut facilement dépasser la bande passante disponible pour une simple boucle comme votre test ci-dessus, l'augmentation des threads sur un tel test n'améliorera pas les performances et peut les détériorer. en raison d'une détérioration de la cohérence du cache.

Juste un test de bon sens, utilisez-vous également le collecteur parallèle ? -XX:+UseParallelGC.UseNUMA ne prend effet qu’à ce moment-là.

Autres conseils

Sans savoir exactement ce que vous faites et quel est le problème que vous essayez de résoudre.Il semble que vous ayez une synchronisation importante autour de votre code, car cela pourrait être la principale raison pour laquelle il n'est pas suffisamment évolutif.Une synchronisation excessive ralentit toute accélération, une fois qu'elle rend votre application presque en série.Je vous suggère donc d'inspecter votre implémentation et d'essayer de comprendre cela.

AJOUTER.

Après avoir ajouté votre implémentation de ce que vous faites.La dégradation des performances pourrait s’expliquer par un accès mémoire important et massif.Une fois que vous exécutez tous vos threads et qu'ils doivent accéder au contrôleur de mémoire pour les données non mises en cache, puisqu'ils s'exécutent sur des processeurs différents, le contrôleur de mémoire empêche les processeurs de le faire simultanément, ce qui signifie qu'il y a une synchronisation au niveau matériel à chaque échec de cache.Dans votre cas, c'est presque la même chose que si vous exécutiez 10 programmes indépendants différents.Je suppose que si vous lancez 10 (vous pouvez remplacer 10 par n'importe quel grand nombre) copies de votre navigateur Web, par exemple, vous verrez le même effet, mais cela ne signifie pas que la mise en œuvre du navigateur est inefficace, vous créez simplement un énorme fardeau sur mémoire d'ordinateur.

Comme le note Artem, il est possible que vous ayez une synchronisation inutile.Mais je commencerais par établir les faits.Votre application fonctionne-t-elle VRAIMENT plus lentement que vous le décrivez ?

Voici un article perspicace sur le sujet : http://codeidol.com/java/java-concurrency/Testing-Concurrent-Programs/Avoiding-Performance-Testing-Pitfalls/

Il est en fait assez difficile d'écrire des micro-benchmarks utiles, surtout lorsqu'il s'agit de code concurrent.Par exemple, vous pouvez avoir une « élimination du code mort » dans laquelle le compilateur optimise le code que vous pensez être en cours d'exécution.Il est également difficile de deviner quand le garbage collection est exécuté.L'optimisation du temps d'exécution de Hotspot rend également la mesure plus difficile.Dans le cas des threads, vous devez prendre en compte le temps utilisé pour les créer.Vous devrez donc peut-être utiliser un `CyclicBarrier`, etc.pour avoir une mesure précise.Des choses comme ça..

Cela dit, j'ai du mal à avoir des problèmes d'accès à la mémoire si vous ne faites que lire.Nous pourrons peut-être mieux vous aider si vous pouvez publier le code...

Deux problèmes potentiels évidents me viennent à l’esprit.

  • Utiliser plus de threads alloue plus de tableaux, ce qui fait éclater le cache.Les accès à la mémoire principale ou aux niveaux inférieurs de cache sont beaucoup plus lents.
  • Si vous utilisez la même source d'instance de générateur de nombres aléatoires, les threads se disputeront l'accès à celle-ci.Il ne s’agit peut-être pas d’une synchronisation complète, mais plutôt de barrières de mémoire avec un algorithme sans verrouillage.Les algorithmes généralement sans verrouillage, bien que généralement rapides, deviennent beaucoup plus lents en cas de forte contention.

Outre les problèmes de concurrence, la cause la plus probable de votre ralentissement est un conflit de cache mémoire.

Si tous les threads accèdent au même élément de stockage, il est probable qu'il se trouve dans le cache mémoire des autres processeurs lorsque vous souhaitez y accéder.

Si le stockage est "en lecture seule", vous pouvez donner à chaque thread sa propre copie, ce qui permettrait à la JVM et au processeur d'optimiser les accès à la mémoire.

J'ai modifié votre test avec les conseils de l'article que j'ai posté.Sur ma machine à 2 cœurs (c'est tout ce que j'ai pour le moment), le résultat semble raisonnable (notez que j'ai exécuté 2 tests pour chaque numéro de thread) :

Peut-être que tu peux essayer ça ?(Attention, j'ai dû modifier légèrement votre test (voir commentaire) car il mettait beaucoup de temps à s'exécuter sur mon mauvais matériel)

Notez également que j'exécute ce test en utilisant le -server option.

Test with threadNum 1 took 2095717473 ns
Test with threadNum 1 took 2121744523 ns
Test with threadNum 2 took 2489853040 ns
Test with threadNum 2 took 2465152974 ns
Test with threadNum 4 took 5044335803 ns
Test with threadNum 4 took 5041235688 ns
Test with threadNum 8 took 10279012556 ns
Test with threadNum 8 took 10347970483 ns

code:

import java.util.concurrent.*;

public class Test{
    private final static int arrSize = 20000000;

    public static void main(String[] args) throws Exception {
        int[] nums = {1,1,2,2,4,4,8,8};//allow hotspot optimization
        for (int threadNum : nums) {
            final CyclicBarrier gate = new CyclicBarrier(threadNum+1);
            final CountDownLatch latch = new CountDownLatch(threadNum);
            ExecutorService exec = Executors.newFixedThreadPool(threadNum);
            for(int i=0; i<threadNum; i++){
                Runnable test = 
                  new Runnable(){
                     public void run() {
                         try{
                             gate.await();
                         }catch(Exception e){
                             throw new RuntimeException(e);
                         }
                         int array[] = new int[arrSize];
                         //arrSize * 10 took very long to run so made it
                         // just arrSize.
                         for (long i = 0; i < arrSize; i++) {
                             array[(int) ((i * i) % arrSize)]++;
                         }//for
                         long sum = 0;
                         for (int i = 0; i < arrSize; i++){
                              sum += array[i]; 
                         }
                         if(new Object().hashCode()==sum){
                              System.out.println("oh");
                         }//if
                         latch.countDown();
                      }//run
                   };//test
                exec.execute(test);
             }//for
             gate.await();
             long start = System.nanoTime();
             latch.await();
             long finish = System.nanoTime();
             System.out.println("Test with threadNum " +
                 threadNum +" took " + (finish-start) + " ns ");
             exec.shutdown();
             exec.awaitTermination(Long.MAX_VALUE,TimeUnit.SECONDS);           
        }//for
    }//main

}//Test
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top