Partager via


Accélération dans PLINQ

Cet article fournit des informations qui vous aideront à écrire des requêtes PLINQ aussi efficaces que possible tout en produisant des résultats corrects.

L’objectif principal de PLINQ est d’accélérer l’exécution des requêtes LINQ to Objects en exécutant les délégués de requête en parallèle sur les ordinateurs multicœurs. PLINQ fonctionne le mieux lorsque le traitement de chaque élément d’une collection source est indépendant, sans état partagé impliqué entre les délégués individuels. Ces opérations sont courantes dans LINQ to Objects et PLINQ, et sont souvent appelées « délicieusement parallèles », car elles se prêtent facilement à la planification sur plusieurs threads. Toutefois, toutes les requêtes ne se composent pas entièrement d’opérations délicieusement parallèles. Dans la plupart des cas, une requête implique certains opérateurs qui ne peuvent pas être parallélisés ou qui ralentissent l’exécution parallèle. Même avec les requêtes entièrement parallèles, PLINQ doit toujours partitionner la source de données et planifier le travail sur les threads, et généralement fusionner les résultats une fois la requête terminée. Toutes ces opérations ajoutent au coût de calcul de la parallélisation ; ces coûts d’ajout de parallélisation sont appelés surcharges. Pour obtenir des performances optimales dans une requête PLINQ, l’objectif est d’optimiser les parties délicieusement parallèles et de réduire les parties qui nécessitent une surcharge.

Facteurs qui affectent les performances des requêtes PLINQ

Les sections suivantes répertorient certains des facteurs les plus importants qui affectent les performances des requêtes parallèles. Il s’agit d’instructions générales qui, par eux-mêmes, ne sont pas suffisantes pour prédire les performances des requêtes dans tous les cas. Comme toujours, il est important de mesurer les performances réelles des requêtes spécifiques sur les ordinateurs avec une gamme de configurations et de charges représentatives.

  1. Coût de calcul du travail global.

    Pour atteindre l’accélération, une requête PLINQ doit avoir suffisamment de travail délicieusement parallèle pour compenser la surcharge. Le travail peut être exprimé en tant que coût de calcul de chaque délégué multiplié par le nombre d’éléments de la collection source. En supposant qu'une opération puisse être parallélisée, plus elle est coûteuse en calcul, plus il y a d'opportunité de l'accélérer. Par exemple, si une fonction prend une milliseconde pour s’exécuter, une requête séquentielle supérieure à 1 000 éléments prend une seconde pour effectuer cette opération, tandis qu’une requête parallèle sur un ordinateur avec quatre cœurs peut prendre seulement 250 millisecondes. Cela génère une vitesse de 750 millisecondes. Si la fonction a besoin d’une seconde pour s’exécuter pour chaque élément, la vitesse est de 750 secondes. Si le délégué est très coûteux, PLINQ peut offrir une accélération significative avec uniquement quelques éléments dans la collection source. À l’inverse, les petites collections de sources avec des délégués triviaux ne sont généralement pas de bons candidats pour PLINQ.

    Dans l’exemple suivant, queryA est probablement un bon candidat pour PLINQ, en supposant que sa fonction Select implique beaucoup de travail. queryB n’est probablement pas un bon candidat, car il n’y a pas suffisamment de travail dans l’instruction Select, et la surcharge de parallélisation compensera la plus grande partie ou la totalité de l’accélération.

    Dim queryA = From num In numberList.AsParallel()  
                 Select ExpensiveFunction(num); 'good for PLINQ  
    
    Dim queryB = From num In numberList.AsParallel()  
                 Where num Mod 2 > 0  
                 Select num; 'not as good for PLINQ  
    
    var queryA = from num in numberList.AsParallel()  
                 select ExpensiveFunction(num); //good for PLINQ  
    
    var queryB = from num in numberList.AsParallel()  
                 where num % 2 > 0  
                 select num; //not as good for PLINQ  
    
  2. Nombre de cœurs logiques sur le système (degré de parallélisme).

    Ce point est une conséquence évidente de la section précédente, les requêtes qui sont délicieusement parallèles s’exécutent plus rapidement sur les machines avec plus de cœurs, car le travail peut être divisé entre des threads plus simultanés. L'accélération globale dépend du pourcentage du travail total de la requête qui peut être parallélisé. Toutefois, ne supposez pas que toutes les requêtes s’exécutent deux fois plus rapidement sur un ordinateur de huit cœurs qu’un ordinateur à quatre cœurs. Lors du réglage des requêtes pour des performances optimales, il est important de mesurer les résultats réels sur les ordinateurs avec différents nombres de cœurs. Ce point est lié au point n° 1 : des jeux de données plus volumineux sont nécessaires pour tirer parti de ressources informatiques supérieures.

  3. Nombre et type d’opérations.

    PLINQ fournit l’opérateur AsOrdered pour les situations dans lesquelles il est nécessaire de conserver l’ordre des éléments dans la séquence source. Il y a un coût associé à la commande, mais ce coût est généralement modeste. Les opérations GroupBy et Join entraînent également une surcharge. PLINQ fonctionne le mieux lorsqu’il est autorisé à traiter des éléments dans la collection source dans n’importe quel ordre et à les transmettre à l’opérateur suivant dès qu’ils sont prêts. Pour plus d’informations, consultez La préservation de l’ordre dans PLINQ.

  4. Formulaire d'exécution des requêtes.

    Si vous stockez les résultats d’une requête en appelant ToArray ou ToList, les résultats de tous les threads parallèles doivent être fusionnés dans la structure de données unique. Cela implique un coût de calcul inévitable. De même, si vous itérez les résultats à l’aide d’une boucle foreach (For Each en Visual Basic), les résultats des threads de travail doivent être sérialisés sur le thread de l’énumérateur. Toutefois, si vous souhaitez simplement effectuer une action basée sur le résultat de chaque thread, vous pouvez utiliser la méthode ForAll pour effectuer ce travail sur plusieurs threads.

  5. Type d’options de fusion.

    PLINQ peut être configuré pour mettre en mémoire tampon sa sortie et la produire en blocs ou tout à la fois après la production de l’ensemble des résultats, ou pour diffuser en continu des résultats individuels au fur et à mesure de leur production. L’ancien entraîne une diminution du temps d’exécution global et celui-ci entraîne une latence réduite entre les éléments générés. Bien que les options de fusion n’aient pas toujours d’impact majeur sur les performances globales des requêtes, elles peuvent avoir un impact sur les performances perçues, car elles contrôlent la durée pendant laquelle un utilisateur doit attendre pour voir les résultats. Pour plus d’informations, consultez Options de fusion dans PLINQ.

  6. Type de partitionnement.

    Dans certains cas, une requête PLINQ sur une collection source indexable peut entraîner une charge de travail déséquilibré. Lorsque cela se produit, vous pouvez peut-être augmenter les performances de la requête en créant un partitionneur personnalisé. Pour plus d’informations, consultez Partitionneurs personnalisés pour PLINQ et TPL.

Quand PLINQ choisit le mode séquentiel

PLINQ tente toujours d’exécuter une requête au moins aussi rapidement que la requête s’exécutera séquentiellement. Bien que PLINQ ne regarde pas le coût de calcul des délégués utilisateur, ou la taille de la source d’entrée, il recherche certaines « formes » de requête. Plus précisément, il recherche des opérateurs de requête ou des combinaisons d’opérateurs qui entraînent généralement l’exécution d’une requête plus lentement en mode parallèle. Lorsqu’il trouve de telles formes, PLINQ par défaut revient en mode séquentiel.

Toutefois, après avoir mesuré les performances d’une requête spécifique, vous pouvez déterminer qu’elle s’exécute réellement plus rapidement en mode parallèle. Dans ce cas, vous pouvez utiliser l’indicateur ParallelExecutionMode.ForceParallelism via la WithExecutionMode méthode pour indiquer à PLINQ de paralléliser la requête. Pour plus d’informations, consultez Comment : spécifier le mode d’exécution dans PLINQ.

La liste suivante décrit les formes de requête que PLINQ par défaut exécute en mode séquentiel :

  • Requêtes qui contiennent une instruction Select, Where indexée, SelectMany indexée ou ElementAt après un opérateur de tri ou de filtrage qui a supprimé ou réorganisé les indices d’origine.

  • Requêtes qui contiennent un opérateur Take, TakeWhile, Skip, SkipWhile et où les index de la séquence source ne sont pas dans l’ordre d’origine.

  • Requêtes qui contiennent Zip ou SequenceEquals, sauf si l’une des sources de données a un index ordonné à l’origine et que l’autre source de données est indexable (c’est-à-dire un tableau ou IList(T)).

  • Requêtes qui contiennent Concat, sauf s’il est appliqué à des sources de données indexables.

  • Requêtes qui contiennent Reverse, sauf si elles sont appliquées à une source de données indexable.

Voir aussi