73 votes

Pourquoi cette boucle à retard commence-t-elle à s'exécuter plus rapidement après plusieurs itérations sans sommeil ?

Pensez-y :

#include <time.h>
#include <unistd.h>
#include <iostream>
using namespace std;

const int times = 1000;
const int N = 100000;

void run() {
  for (int j = 0; j < N; j++) {
  }
}

int main() {
  clock_t main_start = clock();
  for (int i = 0; i < times; i++) {
    clock_t start = clock();
    run();
    cout << "cost: " << (clock() - start) / 1000.0 << " ms." << endl;
    //usleep(1000);
  }
  cout << "total cost: " << (clock() - main_start) / 1000.0 << " ms." << endl;
}

Voici l'exemple de code. Dans les 26 premières itérations de la boucle de chronométrage, la fonction run coûte environ 0,4 ms, mais le coût est ensuite réduit à 0,2 ms.

Lorsque le usleep est décommenté, la boucle à retard prend 0,4 ms pour toutes les exécutions, sans jamais s'accélérer. Pourquoi ?

Le code est compilé avec g++ -O0 (pas d'optimisation), donc la boucle de retard n'est pas optimisée. Il est exécuté sur Intel(R) Core(TM) i3-3220 CPU @ 3.30 GHz, avec 3.13.0-32-générique Ubuntu 14.04.1 LTS (Trusty Tahr).

0 votes

Vous devriez probablement vérifier le résultat de usleep() car il peut être interrompu ou ne rien faire parce que votre paramètre n'est pas valide. Cela rendrait tout chronométrage peu fiable.

0 votes

@John3136 : Le usleep est en dehors de la fenêtre de timing. Il chronomètre une boucle d'occupation soit dos à dos soit séparée par des sleeps de 1ms.

1 votes

Pour des raisons de benchmarking, vous devriez compiler avec gcc -O2 ou (puisque votre code est en C++) g++ -O2 au moins.

126voto

Peter Cordes Points 1375

Après 26 itérations, Linux fait passer le CPU à la vitesse d'horloge maximale, car votre processus utilise toute sa puissance. tranche de temps plusieurs fois de suite.

Si vous vérifiez avec des compteurs de performance plutôt qu'avec le temps d'horloge murale, vous verrez que les cycles d'horloge du noyau par boucle de retard sont restés constants, ce qui confirme qu'il s'agit simplement d'un effet de l'algorithme de l'horloge murale. DVFS (que tous les processeurs modernes utilisent pour fonctionner à une fréquence et une tension plus efficaces sur le plan énergétique la plupart du temps).

Si vous avez testé sur un Skylake avec le support du noyau pour le nouveau mode de gestion de l'énergie (où le matériel prend le contrôle total de la vitesse d'horloge) la montée en puissance serait beaucoup plus rapide.

Si vous le laissez fonctionner pendant un certain temps sur un Processeur Intel avec Turbo Vous verrez probablement le temps par itération augmenter à nouveau légèrement lorsque les limites thermiques exigeront que la vitesse d'horloge soit ramenée à la fréquence maximale soutenue. (Voir Pourquoi mon CPU n'arrive-t-il pas à maintenir des performances optimales en HPC ? pour en savoir plus sur le Turbo qui laisse le CPU fonctionner plus vite qu'il ne peut le supporter pour les charges de travail à haute puissance).


Présentation d'un usleep empêche Le gouverneur de fréquence du CPU de Linux d'augmenter la vitesse d'horloge, car le processus ne génère pas une charge de 100 % même à la fréquence minimale. (C'est-à-dire que l'heuristique du noyau décide que le CPU tourne suffisamment vite pour la charge de travail qui s'y exécute).



commentaires sur d'autres théories :

re : La théorie de David selon laquelle un changement de contexte potentiel de usleep pourrait polluer les caches : Ce n'est pas une mauvaise idée en général, mais cela n'aide pas à expliquer ce code.

La pollution du cache / TLB n'est pas du tout importante pour cette expérience. . Il n'y a pratiquement rien à l'intérieur de la fenêtre de timing qui touche la mémoire autre que la fin de la pile. La plupart du temps est passé dans une minuscule boucle (1 ligne de cache d'instruction) qui ne touche qu'une seule ligne de mémoire. int de la mémoire de la pile. Toute pollution potentielle du cache pendant usleep est une fraction minuscule du temps pour ce code (le code réel sera différent) !

Plus en détail pour x86 :

L'appel à clock() peut lui-même manquer de cache, mais un manque de code en cache retarde la mesure du temps de démarrage, au lieu de faire partie de ce qui est mesuré. Le deuxième appel à clock() ne sera presque jamais retardée, car elle devrait être encore chaude dans le cache.

El run peut se trouver dans une ligne de cache différente de celle de la fonction main (depuis que gcc marque main comme "froide", elle est donc moins optimisée et placée avec d'autres fonctions/données froides). Nous pouvons nous attendre à un ou deux manquements au cache d'instruction . Ils sont probablement toujours dans la même page 4k, cependant, alors main aura déclenché le manque potentiel de TLB avant d'entrer dans la région temporisée du programme.

gcc -O0 compilera le code de l'OP en quelque chose comme ceci (explorateur de compilateur Godbolt),filterAsm:(commentOnly:!t,directives:!t,intel:!t,labels:!t),version:3) : garder le compteur de boucle en mémoire sur la pile.

La boucle vide conserve le compteur de boucle dans la mémoire de la pile, de sorte que sur un ordinateur typique CPU Intel x86 la boucle s'exécute à une itération par ~6 cycles sur le CPU IvyBridge du PO, grâce à la latence de transfert de mémoire qui fait partie de l'architecture de l'ordinateur. add avec une destination mémoire (lecture-modification-écriture). 100k iterations * 6 cycles/iteration est de 600 000 cycles, ce qui domine la contribution d'au plus quelques manques de cache (~200 cycles chacun pour les manques de code-fetch qui empêchent l'émission d'autres instructions jusqu'à ce qu'ils soient résolus).

L'exécution en dehors de l'ordre et le store-forwarding devraient permettre de masquer le risque d'erreur de cache lors de l'accès à la pile (dans le cadre du processus d'exécution de l'ordre). call instruction).

Même si le compteur de boucle était conservé dans un registre, 100 000 cycles, c'est beaucoup.

0 votes

J'augmente la valeur de N par 100x et utiliser cpufreq-info J'ai constaté que le processeur fonctionne toujours à la fréquence minimale lorsque le code est en cours d'exécution.

0 votes

@phyxnj : avec usleep non commenté ? Il s'accélère pour moi avec N=10000000. (J'utilise grep MHz /proc/cpuinfo puisque je n'ai jamais eu l'occasion d'installer cpufreq-utils sur cette machine). En fait, je viens de découvrir que cpupower frequency-info montre ce que cpufreq-info a fait, pour un noyau.

0 votes

@phyxnj : êtes-vous sûr de regarder tous les cœurs, et pas seulement un cœur ? cpupower semble avoir pour valeur par défaut le noyau 0.

3voto

David Schwartz Points 70129

Un appel à usleep peut ou non entraîner un changement de contexte. Si c'est le cas, cela prendra plus de temps que si ce n'est pas le cas.

1 votes

usleep abandonne volontairement le CPU, il devrait donc y avoir un changement de contexte à coup sûr (même si le système est inactif), n'est-ce pas ?

1 votes

@rakib Pas s'il n'y a rien pour changer de contexte ou si l'intervalle de temps est trop court. Lorsque vous parlez de moins de 10 ms environ, le système d'exploitation peut décider de ne pas effectuer de changement de contexte.

0 votes

@rakib : Il y a un passage en mode noyau et retour, c'est sûr. Il pourrait no être un passage à un autre processus avant de reprendre le processus qui a appelé usleep La pollution des caches, des TLB et des prédicteurs de branchement peut donc être minime.

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