16 votes

Java s'adapte beaucoup moins bien que C# sur de nombreux cœurs ?

Je teste la création de plusieurs threads exécutant la même fonction sur un serveur 32 core pour Java et C#. J'exécute l'application avec 1000 itérations de la fonction, qui est répartie sur 1, 2, 4, 8, 16 ou 32 threads à l'aide d'un pool de threads.

À 1, 2, 4, 8 et 16 threads concurrents, Java est au moins deux fois plus rapide que C#. Cependant, à mesure que le nombre de threads augmente, l'écart se resserre et à 32 threads, C# a presque la même durée d'exécution moyenne, mais Java prend occasionnellement 2000 ms (alors que les deux langages tournent habituellement autour de 400 ms). Java commence à se dégrader avec des pics massifs dans le temps d'exécution par itération de thread.

EDIT Il s'agit de Windows Server 2008

EDIT2 J'ai modifié le code ci-dessous pour montrer l'utilisation du threadpool du service Executor. J'ai également installé Java 7.

J'ai défini les optimisations suivantes dans la VM hotspot :

-XX:+UseConcMarkSweepGC -Xmx 6000

mais les choses ne se sont pas améliorées pour autant. La seule différence entre le code est que j'utilise le threadpool ci-dessous et que pour la version C# nous utilisons :

http://www.codeproject.com/Articles/7933/Smart-Thread-Pool

Existe-t-il un moyen de rendre Java plus optimisé ? Peut-être pourriez-vous m'expliquer pourquoi je constate cette dégradation massive des performances ?

Existe-t-il un pool de threads Java plus efficace ?

(Attention, il ne s'agit pas de modifier la fonction de test)

import java.io.DataOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

public class PoolDemo {

    static long FastestMemory = 2000000;
    static long SlowestMemory = 0;
    static long TotalTime;
    static int[] FileArray;
    static DataOutputStream outs;
    static FileOutputStream fout;
    static Byte myByte = 0;

  public static void main(String[] args) throws InterruptedException, FileNotFoundException {

        int Iterations = Integer.parseInt(args[0]);
        int ThreadSize = Integer.parseInt(args[1]);

        FileArray = new int[Iterations];
        fout = new FileOutputStream("server_testing.csv");

        // fixed pool, unlimited queue
        ExecutorService service = Executors.newFixedThreadPool(ThreadSize);
        ThreadPoolExecutor executor = (ThreadPoolExecutor) service;

        for(int i = 0; i<Iterations; i++) {
          Task t = new Task(i);
          executor.execute(t);
        }

        for(int j=0; j<FileArray.length; j++){
            new PrintStream(fout).println(FileArray[j] + ",");
        }
      }

  private static class Task implements Runnable {

    private int ID;

    public Task(int index) {
      this.ID = index;
    }

    public void run() {
        long Start = System.currentTimeMillis();

        int Size1 = 100000;
        int Size2 = 2 * Size1;
        int Size3 = Size1;

        byte[] list1 = new byte[Size1];
        byte[] list2 = new byte[Size2];
        byte[] list3 = new byte[Size3];

        for(int i=0; i<Size1; i++){
            list1[i] = myByte;
        }

        for (int i = 0; i < Size2; i=i+2)
        {
            list2[i] = myByte;
        }

        for (int i = 0; i < Size3; i++)
        {
            byte temp = list1[i];
            byte temp2 = list2[i];
            list3[i] = temp;
            list2[i] = temp;
            list1[i] = temp2;
        }

        long Finish = System.currentTimeMillis();
        long Duration = Finish - Start;
        TotalTime += Duration;
        FileArray[this.ID] = (int)Duration;
        System.out.println("Individual Time " + this.ID + " \t: " + (Duration) + " ms");

        if(Duration < FastestMemory){
            FastestMemory = Duration;
        }
        if (Duration > SlowestMemory)
        {
            SlowestMemory = Duration;
        }
    }
  }
}

19voto

sparc_spread Points 2210

Résumé

Vous trouverez ci-dessous la réponse originale, la mise à jour 1 et la mise à jour 2. La mise à jour 1 traite des conditions de course autour des variables des statistiques de test en utilisant des structures de concurrence. La mise à jour 2 est une façon beaucoup plus simple de traiter le problème des conditions de course. J'espère que je ne ferai plus de mises à jour - désolé pour la longueur de la réponse, mais la programmation multithread est compliquée !

Réponse originale

La seule différence entre les deux codes est que j'utilise le code suivant threadpool

Je dirais qu'il s'agit là d'une différence absolument énorme. Il est difficile de comparer les performances des deux langages lorsque leurs implémentations du pool de threads sont des blocs de code complètement différents, écrits dans l'espace utilisateur. L'implémentation du pool de threads peut avoir un impact énorme sur les performances.

Vous devriez envisager d'utiliser les pools de threads intégrés de Java. Voir ThreadPoolExecutor et l'ensemble des java.util.concurrent dont il fait partie. Les Exécuteurs possède des méthodes statiques pratiques pour les pools et constitue une bonne interface de niveau supérieur. Tout ce dont vous avez besoin est le JDK 1.5+, mais plus il est récent, mieux c'est. Les solutions fork/join mentionnées par d'autres affiches font également partie de ce paquet - comme mentionné, elles nécessitent 1.7+.

Mise à jour 1 - Traiter les conditions de course en utilisant des structures de concurrence

Vous avez des conditions de course autour du réglage de FastestMemory , SlowestMemory y TotalTime . Pour les deux premiers, vous faites le < y > et la mise en place en plusieurs étapes. Il ne s'agit pas d'un processus atomique ; il est tout à fait possible qu'un autre processus mette à jour ces valeurs entre le test et le réglage. Les += la mise en place d'un TotalTime est également non atomique : un test et un ensemble déguisés.

Voici quelques suggestions de corrections.

Temps total

L'objectif est d'obtenir une méthode de travail atomique et sûre pour les threads. += de TotalTime .

// At the top of everything
import java.util.concurrent.atomic.AtomicLong;  

...    

// In PoolDemo
static AtomicLong TotalTime = new AtomicLong();    

...    

// In Task, where you currently do the TotalTime += piece
TotalTime.addAndGet (Duration); 

Mémoire la plus rapide / Mémoire la plus lente

L'objectif est de tester et de mettre à jour FastestMemory y SlowestMemory chacune dans une étape atomique, de sorte qu'aucun thread ne puisse se glisser entre les étapes de test et de mise à jour pour provoquer une condition de course.

L'approche la plus simple :

Protéger le test et la définition des variables en utilisant la classe elle-même comme moniteur. Nous avons besoin d'un moniteur qui contient les variables afin de garantir une visibilité synchronisée (merci @A.H. pour l'avoir remarqué.) Nous devons utiliser la classe elle-même parce que tout est static .

// In Task
synchronized (PoolDemo.class) {
    if (Duration < FastestMemory) {
        FastestMemory = Duration;
    }

    if (Duration > SlowestMemory) {
        SlowestMemory = Duration;
    }
}

Approche intermédiaire :

Il se peut que vous n'aimiez pas prendre toute la classe pour le moniteur, ou exposer le moniteur en utilisant la classe, etc. Vous pourriez créer un moniteur séparé qui ne contiendrait pas lui-même les éléments suivants FastestMemory y SlowestMemory mais vous rencontrerez alors des problèmes de visibilité de la synchronisation. Vous pouvez contourner ce problème en utilisant l'option volatile mot-clé.

// In PoolDemo
static Integer _monitor = new Integer(1);
static volatile long FastestMemory = 2000000;
static volatile long SlowestMemory = 0;

...

// In Task
synchronized (PoolDemo._monitor) {
    if (Duration < FastestMemory) {
        FastestMemory = Duration;
    }

    if (Duration > SlowestMemory) {
        SlowestMemory = Duration;
    }
}

Approche avancée :

Nous utilisons ici le java.util.concurrent.atomic au lieu de moniteurs. En cas de forte contention, cette méthode devrait être plus performante que la méthode synchronized l'approche. Essayez-la et vous verrez.

// At the top of everything
import java.util.concurrent.atomic.AtomicLong;    

. . . . 

// In PoolDemo
static AtomicLong FastestMemory = new AtomicLong(2000000);
static AtomicLong SlowestMemory = new AtomicLong(0);

. . . . .

// In Task
long temp = FastestMemory.get();       
while (Duration < temp) {
    if (!FastestMemory.compareAndSet (temp, Duration)) {
        temp = FastestMemory.get();       
    }
}

temp = SlowestMemory.get();
while (Duration > temp) {
    if (!SlowestMemory.compareAndSet (temp, Duration)) {
        temp = SlowestMemory.get();
    }
}

Tenez-moi au courant de ce qui se passera ensuite. Cela ne résoudra peut-être pas votre problème, mais la condition de course autour des variables mêmes qui suivent vos performances est trop dangereuse pour être ignorée.

J'ai initialement posté cette mise à jour sous forme de commentaire, mais je l'ai déplacée ici pour avoir la place de montrer le code. Cette mise à jour a fait l'objet de plusieurs itérations - grâce à A.H. pour avoir détecté un bogue que j'avais dans une version antérieure. Tout ce qui se trouve dans cette mise à jour remplace tout ce qui se trouve dans le commentaire.

Enfin, une excellente source couvrant l'ensemble de ce matériel est la suivante Concurrence Java en pratique Le meilleur livre sur la concurrence en Java, et l'un des meilleurs livres sur Java en général.

Mise à jour 2 - Traiter les conditions de course de manière beaucoup plus simple

J'ai récemment remarqué que votre code actuel ne se terminera jamais à moins que vous n'ajoutiez executorService.shutdown() . En d'autres termes, les threads non démon qui vivent dans ce pool doivent être terminés, faute de quoi le thread principal ne sortira jamais. Cela m'a amené à penser que puisque nous devons attendre que tous les threads se terminent, pourquoi ne pas comparer leurs durées après qu'ils se soient terminés, et ainsi contourner la mise à jour concurrente de FastestMemory etc. C'est plus simple et pourrait être plus rapide ; il n'y a plus de verrouillage ou de surcharge CAS, et vous faites déjà une itération de FileArray à la fin des choses de toute façon.

Nous pouvons également tirer parti du fait que votre mise à jour simultanée des FileArray est parfaitement sûr, puisque chaque thread écrit dans une cellule séparée et qu'il n'y a pas de lecture de FileArray lors de sa rédaction.

Vous effectuez ensuite les modifications suivantes :

// In PoolDemo
// This part is the same, just so you know where we are
for(int i = 0; i<Iterations; i++) {
    Task t = new Task(i);
    executor.execute(t);
}

// CHANGES BEGIN HERE
// Will block till all tasks finish. Required regardless.
executor.shutdown();
executor.awaitTermination(10, TimeUnit.SECONDS);

for(int j=0; j<FileArray.length; j++){
    long duration = FileArray[j];
    TotalTime += duration;

    if (duration < FastestMemory) {
        FastestMemory = duration;
    }

    if (duration > SlowestMemory) {
        SlowestMemory = duration;
    }

    new PrintStream(fout).println(FileArray[j] + ",");
}

. . . 

// In Task
// Ending of Task.run() now looks like this
long Finish = System.currentTimeMillis();
long Duration = Finish - Start;
FileArray[this.ID] = (int)Duration;
System.out.println("Individual Time " + this.ID + " \t: " + (Duration) + " ms");

Essayez également cette approche.

Vous devriez certainement vérifier votre code C# pour des conditions de course similaires.

5voto

A.H. Points 23369

...mais Java prend parfois 2000ms...

Et

    byte[] list1 = new byte[Size1];
    byte[] list2 = new byte[Size2];
    byte[] list3 = new byte[Size3];

Les problèmes seront dus au nettoyage des tableaux par le ramasse-miettes. Si vous voulez vraiment régler cela, je vous suggère d'utiliser une sorte de cache pour les tableaux.

Editer

Celui-ci

   System.out.println("Individual Time " + this.ID + " \t: " + (Duration) + " ms");

fait un ou plusieurs synchronized en interne. Ainsi, votre code hautement "concurrent" sera sérialisé de manière satisfaisante à ce stade. Il suffit de le supprimer et de le tester à nouveau.

4voto

Aviad Ben Dov Points 4170

La réponse de @sparc_spread est excellente, mais j'ai remarqué une autre chose :

J'exécute l'application avec 1000 itérations de la fonction

Remarquez que la JVM HotSpot travaille sur interprétées pour les premières 1,5k itérations de n'importe quelle fonction en mode client, et pour 10k itérations en mode serveur. Les ordinateurs dotés de ce nombre de cœurs sont automatiquement considérés comme des "serveurs" par la JVM HotSpot.

Cela signifierait que C# utiliserait le JIT (et fonctionnerait en code machine) avant Java, et aurait une chance d'obtenir de meilleures performances au moment de l'exécution de la fonction. Essayez d'augmenter le nombre d'itérations à 20 000 et commencez à compter à partir de 10 000 itérations.

La raison en est que la JVM recueille des données statistiques sur la meilleure façon d'exécuter la JIT. Elle est convaincue que votre fonction sera souvent exécutée au fil du temps, et elle adopte donc un mécanisme de "démarrage lent" pour un temps d'exécution global plus rapide. En d'autres termes, "20 % des fonctions sont exécutées 80 % du temps", alors pourquoi toutes les exécuter ?

2voto

ojota84 Points 943

Utilisez-vous Java 6 ? Java 7 est doté de fonctionnalités permettant d'améliorer les performances de la programmation parallèle :

http://www.oracle.com/technetwork/articles/java/fork-join-422606.html

2voto

Mark Rotteveel Points 20766

Vous pouvez également examiner le service ExecutorService, créé à l'aide de la fonction Executors.newFixedThreadPool(noOfCores) ou une méthode similaire.

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