J'étudie la planification d'OpenMP et en particulier les différents types. Je comprends le comportement général de chaque type, mais des éclaircissements seraient utiles sur le moment de choisir entre dynamic
et guided
planification.
documents d'Intel décrivent dynamic
planification:
Utilisez la file d'attente de travail interne pour donner un bloc d'itérations de boucle de la taille d'un bloc à chaque thread. Lorsqu'un thread est terminé, il récupère le bloc d'itérations de boucle suivant en haut de la file d'attente de travail. Par défaut, la taille du segment est 1. Soyez prudent lorsque vous utilisez ce type de planification en raison de la surcharge supplémentaire impliquée.
Il décrit également guided
la planification:
Similaire à l'ordonnancement dynamique, mais la taille des morceaux commence à grande et diminue pour mieux gérer le déséquilibre de charge entre les itérations. Le paramètre de bloc facultatif spécifie les blocs de taille minimale à utiliser. Par défaut, la taille du bloc est approximativement loop_count/number_of_threads.
Étant donné que guided
l'ordonnancement diminue de façon dynamique la taille des blocs à l'exécution, pourquoi devrais-je jamais utiliser l'ordonnancement dynamic
?
J'ai fait des recherches sur cette question et a trouvé ce tableau de Dartmouth :
guided
est répertorié comme ayant high
surcharge, tandis que dynamic
a une surcharge moyenne.
Cela avait initialement un sens, mais après une enquête plus approfondie, j'ai lire un article Intel sur le sujet. D'après le tableau précédent, j'ai théorisé que l'ordonnancement de guided
prendrait plus de temps en raison de l'analyse et des ajustements de la taille de bloc lors de l'exécution (même lorsqu'il est utilisé correctement). Cependant, dans l'article d'Intel, il indique:
Les horaires guidés fonctionnent mieux avec de petites tailles de bloc comme limite; cela donne le plus de flexibilité. On ne sait pas pourquoi elles s’aggravent à des tailles de bloc plus importantes, mais elles peuvent prendre trop de temps si elles sont limitées à de grandes tailles de bloc.
Pourquoi la taille du bloc serait-elle liée au fait que guided
prend plus de temps que dynamic
? Il serait logique que le manque de "flexibilité" entraîne une perte de performances en verrouillant la taille de bloc trop élevée. Cependant, je ne décrirais pas cela comme une "surcharge", et le problème de verrouillage discréditerait la théorie précédente.
Enfin, il est indiqué dans l'article:
Les plannings dynamiques offrent la plus grande flexibilité, mais prennent le plus grand impact sur les performances lorsqu'ils sont mal programmés.
Il est logique que l'ordonnancement dynamic
soit plus optimal que static
, mais pourquoi est-il plus optimal que guided
? Est-ce juste les frais généraux que je remets en question?
Ceci quelque peu lié SO post explique NUMA lié aux types de planification. Cela n'a aucun rapport avec cette question, car l'organisation requise est perdue par le "premier arrivé, premier servi") comportement de ces types de planification.
dynamic
la planification peut être coalescente, entraînant une amélioration des performances, mais la même hypothèse devrait s'appliquer à guided
.
Voici le calendrier de chaque type de planification sur différentes tailles de blocs de l'article Intel pour référence. Ce ne sont que des enregistrements d'un programme et certaines règles s'appliquent différemment par programme et par machine (en particulier avec la programmation), mais cela devrait fournir les tendances générales.
[~ # ~] éditez [~ # ~] (cœur de ma question):
guided
? Exemples spécifiques? Pourquoi est-il plus lent que dynamic
dans certains cas?guided
plutôt que dynamic
ou vice versa?Qu'est-ce qui affecte le temps d'exécution de la planification guidée?
Il y a trois effets à considérer:
L'intérêt de l'ordonnancement dynamique/guidé est d'améliorer la distribution du travail dans le cas où chaque itération de boucle ne contient pas la même quantité de travail. Fondamentalement:
schedule(dynamic, 1)
fournit un équilibrage de charge optimal dynamic, k
Aura toujours identique ou meilleur équilibrage de charge que guided, k
La norme stipule que la taille de chaque bloc est proportionnelle au nombre d'itérations non attribuées divisé par le nombre de threads dans l'équipe, diminuant à k
.
Le exécution de GCC OpenMP prend cela littéralement, en ignorant le proportionnel. Par exemple, pour 4 threads, k=1
, Il y aura 32 itérations comme 8, 6, 5, 4, 3, 2, 1, 1, 1, 1
. Maintenant, à mon humble avis, c'est vraiment stupide: cela entraîne un mauvais déséquilibre de charge si les premières itérations 1/n contiennent plus de 1/n du travail.
Exemples spécifiques? Pourquoi est-il plus lent que dynamique dans certains cas?
Ok, jetons un coup d'œil à un exemple trivial où le travail intérieur diminue avec l'itération de la boucle:
#include <omp.h>
void work(long ww) {
volatile long sum = 0;
for (long w = 0; w < ww; w++) sum += w;
}
int main() {
const long max = 32, factor = 10000000l;
#pragma omp parallel for schedule(guided, 1)
for (int i = 0; i < max; i++) {
work((max - i) * factor);
}
}
L'exécution ressemble à ceci1:
Comme vous pouvez le voir, guided
fait vraiment mal ici. guided
fera beaucoup mieux pour différents types de distributions de travail. Il est également possible de mettre en œuvre guidée différemment. L'implémentation en clang (dont l'IIRC provient d'Intel) est beaucoup plus sophistiquée . Je ne comprends vraiment pas l'idée derrière l'implémentation naïve de GCC. À mes yeux, cela défait efficacement le but de l'effacement dynamique de la charge si vous donnez 1/n
Du travail au premier thread.
Désormais, chaque segment dynamique a un impact sur les performances en raison de l'accès à l'état partagé. La surcharge de guided
sera légèrement plus élevée par morceau que dynamic
, car il y a un peu plus de calcul à faire. Cependant, guided, k
Aura moins de blocs dynamiques totaux que dynamic, k
.
Les frais généraux dépendront également de la mise en œuvre, par exemple qu'il utilise atomiques ou verrous pour protéger l'état partagé.
Disons écrire sur un vecteur d'entiers dans l'itération de votre boucle. Si chaque seconde itération devait être exécutée par un thread différent, chaque deuxième élément du vecteur serait écrit par un noyau différent. C'est vraiment mauvais car ce faisant, ils concurrencent les lignes de cache qui contiennent des éléments voisins (faux partage). Si vous avez de petites tailles de morceaux et/ou des tailles de morceaux qui ne s'alignent pas bien sur les caches, vous obtenez de mauvaises performances aux "bords" des morceaux. C'est pourquoi vous préférez généralement de grandes tailles de morceaux Nice (2^n
). guided
peut vous donner des tailles de morceaux plus grandes en moyenne, mais pas 2^n
(ou k*m
).
Cette réponse (que vous avez déjà référencé), discute longuement de l'inconvénient de la planification dynamique/guidée en termes de NUMA, mais cela s'applique également aux localités/caches.
Étant donné les différents facteurs et la difficulté de prévoir des spécificités, je ne peux que recommander de mesurer votre application spécifique, sur votre système spécifique, dans votre configuration spécifique, avec votre compilateur spécifique. Malheureusement, il n'y a pas de portabilité parfaite des performances. Je dirais personnellement que c'est particulièrement vrai pour guided
.
Quand est-ce que je préfèrerais être guidé plutôt que dynamique ou vice-versa?
Si vous avez des connaissances spécifiques sur la surcharge/travail par itération, je dirais que dynamic, k
Vous donne les résultats les plus stables en choisissant un bon k
. En particulier, vous ne dépendez pas tellement de la façon dont la mise en œuvre est intelligente.
D'un autre côté, guided
peut être une bonne première supposition, avec un rapport frais généraux/équilibrage de charge, au moins pour une implémentation intelligente. Soyez particulièrement prudent avec guided
si vous savez que les temps d'itération ultérieurs sont plus courts.
Gardez à l'esprit qu'il existe également schedule(auto)
, qui donne un contrôle complet sur le compilateur/runtime, et schedule(runtime)
, qui vous permet de sélectionner la politique de planification pendant l'exécution.
Une fois que cela a été expliqué, les sources ci-dessus soutiennent-elles votre explication? Se contredisent-ils du tout?
Prenez les sources, y compris cet anser, avec un grain de sel. Ni le graphique que vous avez publié, ni ma photo chronologique ne sont des chiffres scientifiquement exacts. Il y a une énorme variation dans les résultats et il n'y a pas de barres d'erreur, elles seraient probablement partout avec ces très peu de points de données. Le graphique combine également les multiples effets que j'ai mentionnés sans divulguer le code Work
.
[De la documentation Intel]
Par défaut, la taille du bloc est approximativement loop_count/number_of_threads.
Cela contredit mon observation selon laquelle l'icc gère beaucoup mieux mon petit exemple.
1: Utilisation de GCC 6.3.1, Score-P/Vampir pour la visualisation, deux fonctions de travail alternées pour la coloration.