Que signifient les termes «lié au processeur» et «lié aux E / S»?
Que signifient les termes «lié au processeur» et «lié aux E / S»?
Réponses:
C'est assez intuitif:
Un programme est lié au CPU s'il irait plus vite si le CPU était plus rapide, c'est-à-dire qu'il passe la majorité de son temps à simplement utiliser le CPU (faire des calculs). Un programme qui calcule de nouveaux chiffres de π sera généralement lié au processeur, ce sont juste des chiffres croquants.
Un programme est lié aux E / S s'il irait plus vite si le sous-système d'E / S était plus rapide. Le système d'E / S exact auquel il est destiné peut varier; Je l'associe généralement au disque, mais bien sûr, la mise en réseau ou la communication en général est également courante. Un programme qui examine un énorme fichier pour certaines données peut devenir lié aux E / S, car le goulot d'étranglement est alors la lecture des données à partir du disque (en fait, cet exemple est peut-être un peu démodé de nos jours avec des centaines de Mo / s venant de SSD).
CPU Bound signifie que la vitesse à laquelle le processus progresse est limitée par la vitesse du CPU. Une tâche qui effectue des calculs sur un petit ensemble de nombres, par exemple en multipliant de petites matrices, est susceptible d'être liée au processeur.
I / O Bound signifie que la vitesse à laquelle un processus progresse est limitée par la vitesse du sous-système d'E / S. Une tâche qui traite des données à partir du disque, par exemple, compter le nombre de lignes dans un fichier est susceptible d'être liée aux E / S.
La mémoire liée signifie que la vitesse à laquelle un processus progresse est limitée par la quantité de mémoire disponible et la vitesse de cet accès à la mémoire. Une tâche qui traite de grandes quantités de données en mémoire, par exemple la multiplication de grandes matrices, est susceptible d'être liée à la mémoire.
Le cache lié signifie la vitesse à laquelle la progression d'un processus est limitée par la quantité et la vitesse du cache disponible. Une tâche qui traite simplement plus de données qu'il n'y en a dans le cache sera liée au cache.
I / O Bound serait plus lent que Memory Bound serait plus lent que Cache Bound serait plus lent que CPU Bound.
La solution pour être lié aux E / S n'est pas nécessairement d'obtenir plus de mémoire. Dans certaines situations, l'algorithme d'accès peut être conçu en fonction des limitations d'E / S, de mémoire ou de cache. Voir Cache Oblivious Algorithms .
Multi-threading
Dans cette réponse, j'examinerai un cas d'utilisation important de la distinction entre le travail limité CPU et IO: lors de l'écriture de code multi-thread.
Exemple lié aux E / S RAM: Vector Sum
Considérons un programme qui additionne toutes les valeurs d'un seul vecteur:
#define SIZE 1000000000
unsigned int is[SIZE];
unsigned int sum = 0;
size_t i = 0;
for (i = 0; i < SIZE; i++)
/* Each one of those requires a RAM access! */
sum += is[i]
Paralléliser cela en divisant la baie de manière égale pour chacun de vos cœurs est d'une utilité limitée sur les ordinateurs de bureau modernes courants.
Par exemple, sur mon ordinateur portable Ubuntu 19.04, Lenovo ThinkPad P51 avec processeur: processeur Intel Core i7-7820HQ (4 cœurs / 8 threads), RAM: 2x Samsung M471A2K43BB1-CRC (2x 16 Go), j'obtiens des résultats comme celui-ci:
Notez cependant qu'il y a beaucoup d'écart entre les exécutions. Mais je ne peux pas augmenter la taille du tableau beaucoup plus loin car je suis déjà à 8 Go, et je ne suis pas d'humeur à obtenir des statistiques sur plusieurs séries aujourd'hui. Cela semblait cependant être une course typique après avoir fait de nombreuses courses manuelles.
Code de référence:
pthread
Code source POSIX C utilisé dans le graphique.
Et voici une version C ++ qui produit des résultats analogues.
Je ne connais pas assez l'architecture informatique pour expliquer pleinement la forme de la courbe, mais une chose est claire: le calcul ne devient pas 8x plus rapide que prévu naïvement car j'utilise tous mes 8 threads! Pour une raison quelconque, 2 et 3 fils étaient optimaux, et en ajouter plus rend les choses beaucoup plus lentes.
Comparez cela au travail lié au processeur, qui devient en réalité 8 fois plus rapide: que signifient «réel», «utilisateur» et «sys» dans la sortie de time (1)?
La raison pour laquelle tous les processeurs partagent un bus de mémoire unique reliant à la RAM:
CPU 1 --\ Bus +-----+
CPU 2 ---\__________| RAM |
... ---/ +-----+
CPU N --/
donc le bus mémoire devient rapidement le goulot d'étranglement, pas le CPU.
Cela se produit car l'ajout de deux nombres prend un seul cycle CPU, les lectures en mémoire prennent environ 100 cycles CPU dans le matériel 2016.
Ainsi, le travail CPU effectué par octet de données d'entrée est trop petit, et nous appelons cela un processus lié aux E / S.
La seule façon d'accélérer encore ce calcul serait d'accélérer les accès à la mémoire individuelle avec un nouveau matériel de mémoire, par exemple une mémoire multicanal .
La mise à niveau vers une horloge CPU plus rapide par exemple ne serait pas très utile.
Autres exemples
la multiplication matricielle est liée au CPU sur la RAM et les GPU. L'entrée contient:
2 * N**2
chiffres, mais:
N ** 3
des multiplications sont faites, et cela suffit pour que la parallélisation en vaille la peine pour un grand N. pratique
C'est pourquoi des bibliothèques de multiplication de matrices CPU parallèles comme les suivantes existent:
L'utilisation du cache fait une grande différence dans la vitesse des implémentations. Voir par exemple cet exemple de comparaison de GPU didactique .
Voir également:
La mise en réseau est l'exemple prototypique lié aux entrées-sorties.
Même lorsque nous envoyons un seul octet de données, il faut encore beaucoup de temps pour atteindre sa destination.
La parallélisation de petites requêtes réseau comme les requêtes HTTP peut offrir d'énormes gains de performances.
Si le réseau est déjà à pleine capacité (par exemple en téléchargeant un torrent), la parallélisation peut encore augmenter améliorer la latence (par exemple, vous pouvez charger une page Web "en même temps").
Une opération factice liée au processeur C ++ qui prend un nombre et le craque beaucoup:
Le tri semble être basé sur le processeur sur la base de l'expérience suivante: les algorithmes parallèles C ++ 17 sont-ils déjà implémentés? qui a montré une amélioration des performances 4x pour le tri parallèle, mais je voudrais également avoir une confirmation plus théorique
Comment savoir si vous êtes lié au processeur ou aux E / S
IO non RAM lié comme disque, réseau ps aux
:, puis désactivez si CPU% / 100 < n threads
. Si oui, vous êtes lié aux E / S, par exemple les blocages read
n'attendent que des données et le planificateur ignore ce processus. Ensuite, utilisez d'autres outils comme sudo iotop
pour décider quel IO est le problème exactement.
Ou, si l'exécution est rapide et que vous paramétrez le nombre de threads, vous pouvez facilement le voir à partir du fait time
que les performances s'améliorent à mesure que le nombre de threads augmente pour le travail lié au processeur: que signifient «réel», «utilisateur» et «sys» dans la sortie du temps (1)?
RAM-IO lié: plus difficile à dire, car le temps d'attente RAM est inclus dans les CPU%
mesures, voir aussi:
Quelques options:
GPU
Les GPU ont un goulot d'étranglement d'E / S lorsque vous transférez pour la première fois les données d'entrée de la RAM lisible du processeur ordinaire vers le GPU.
Par conséquent, les GPU ne peuvent être meilleurs que les CPU pour les applications liées au CPU.
Cependant, une fois les données transférées vers le GPU, elles peuvent fonctionner sur ces octets plus rapidement que le processeur, car le GPU:
a plus de localisation de données que la plupart des systèmes CPU, et donc les données sont accessibles plus rapidement pour certains cœurs que pour d'autres
exploite le parallélisme des données et sacrifie la latence en sautant simplement toutes les données qui ne sont pas prêtes à être utilisées immédiatement.
Étant donné que le GPU doit fonctionner sur de grandes données d'entrée parallèles, il est préférable de simplement passer aux données suivantes qui pourraient être disponibles au lieu d'attendre que les données actuelles soient disponibles et de bloquer toutes les autres opérations comme le fait le CPU.
Par conséquent, le GPU peut être plus rapide qu'un processeur si votre application:
Ces choix de conception visaient à l'origine l'application du rendu 3D, dont les principales étapes sont celles illustrées dans Qu'est-ce que les shaders dans OpenGL et pourquoi en avons-nous besoin?
et nous concluons donc que ces applications sont liées au processeur.
Avec l'avènement du GPGPU programmable, nous pouvons observer plusieurs applications GPGPU qui servent d'exemples d'opérations liées au CPU:
Traitement d'image avec les shaders GLSL?
Les opérations de traitement d'images locales telles qu'un filtre de flou sont de nature très parallèle.
Tracé de graphiques de carte thermique si la fonction tracée est suffisamment complexe.
https://www.youtube.com/watch?v=fE0P6H8eK4I "Dynamique des fluides en temps réel: CPU vs GPU" par Jesús Martín Berlanga
Résolution d'équations différentielles partielles telles que l' équation de Navier Stokes de la dynamique des fluides:
Voir également:
CPython Global Intepreter Lock (GIL)
En guise d'étude de cas rapide, je tiens à souligner le verrouillage d'interpréteur global Python (GIL): Qu'est-ce que le verrouillage d'interpréteur global (GIL) dans CPython?
Ce détail d'implémentation CPython empêche plusieurs threads Python d'utiliser efficacement le travail lié au processeur. Les documents CPython disent:
Détail de l'implémentation de CPython: dans CPython, en raison du Global Interpreter Lock, un seul thread peut exécuter du code Python à la fois (même si certaines bibliothèques axées sur les performances peuvent surmonter cette limitation). Si vous souhaitez que votre application utilise mieux les ressources de calcul des machines multicœurs, il est conseillé d'utiliser
multiprocessing
ouconcurrent.futures.ProcessPoolExecutor
. Cependant, le thread est toujours un modèle approprié si vous souhaitez exécuter simultanément plusieurs tâches liées aux E / S.
Par conséquent, nous avons ici un exemple où le contenu lié au CPU ne convient pas et où le contenu lié aux E / S l'est.
Lié au processeur signifie que le programme est goulot d'étranglement par le processeur ou l'unité centrale de traitement, tandis que lié aux E / S signifie que le programme est goulot d'étranglement par les E / S ou les entrées / sorties, telles que la lecture ou l'écriture sur le disque, le réseau, etc.
En général, lors de l'optimisation des programmes informatiques, on essaie de rechercher le goulot d'étranglement et de l'éliminer. Savoir que votre programme est lié au processeur aide à éviter d'optimiser inutilement autre chose.
[Et par «goulot d'étranglement», je veux dire la chose qui rend votre programme plus lent qu'il ne l'aurait fait autrement.]
Une autre façon d'exprimer la même idée:
Si l'accélération du processeur n'accélère pas votre programme, il peut être lié aux E / S.
Si l'accélération des E / S (par exemple en utilisant un disque plus rapide) n'aide pas, votre programme est peut-être lié au processeur.
(J'ai utilisé "peut-être" car vous devez prendre en compte d'autres ressources. La mémoire en est un exemple.)
Lorsque votre programme attend des E / S (c'est-à-dire une lecture / écriture sur le disque ou une lecture / écriture sur le réseau, etc.), le processeur est libre d'effectuer d'autres tâches même si votre programme est arrêté. La vitesse de votre programme dépendra principalement de la rapidité avec laquelle les E / S peuvent se produire, et si vous voulez l'accélérer, vous devrez accélérer les E / S.
Si votre programme exécute de nombreuses instructions de programme et n'attend pas d'E / S, il est alors dit lié au processeur. Accélérer le CPU accélérera le programme.
Dans les deux cas, la clé pour accélérer le programme peut ne pas être d'accélérer le matériel, mais d'optimiser le programme pour réduire la quantité d'E / S ou de CPU dont il a besoin, ou de le faire faire des E / S tout en consommant beaucoup de CPU. des trucs.
Les E / S liées se réfèrent à une condition dans laquelle le temps nécessaire pour terminer un calcul est principalement déterminé par la période passée à attendre la fin des opérations d'entrée / sortie.
C'est l'opposé d'une tâche liée au processeur. Cette circonstance survient lorsque le taux auquel les données sont demandées est plus lent que le taux auquel elles sont consommées ou, en d'autres termes, plus de temps est passé à demander des données qu'à les traiter.
Découvrez ce que dit Microsoft.
Le cœur de la programmation asynchrone est les objets Task et Task, qui modélisent les opérations asynchrones. Ils sont pris en charge par l'async et attendent des mots clés. Le modèle est assez simple dans la plupart des cas:
Pour le code lié aux E / S, vous attendez une opération qui renvoie une tâche ou une tâche à l'intérieur d'une méthode asynchrone.
Pour le code lié au processeur, vous attendez une opération qui est démarrée sur un thread d'arrière-plan avec la méthode Task.Run.
Le mot-clé wait est l'endroit où la magie opère. Il cède le contrôle à l'appelant de la méthode exécutée en attente et permet finalement à une interface utilisateur d'être réactive ou à un service d'être élastique.
Exemple lié aux E / S: téléchargement de données à partir d'un service Web
private readonly HttpClient _httpClient = new HttpClient();
downloadButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI as the request
// from the web service is happening.
//
// The UI thread is now free to perform other work.
var stringData = await _httpClient.GetStringAsync(URL);
DoSomethingWithData(stringData);
};
Exemple lié au processeur: effectuer un calcul pour un jeu
private DamageResult CalculateDamageDone()
{
// Code omitted:
//
// Does an expensive calculation and returns
// the result of that calculation.
}
calculateButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI while CalculateDamageDone()
// performs its work. The UI thread is free to perform other work.
var damageResult = await Task.Run(() => CalculateDamageDone());
DisplayDamage(damageResult);
};
Les exemples ci-dessus ont montré comment utiliser async et attendre le travail lié aux E / S et au processeur. Il est essentiel que vous puissiez identifier lorsqu'un travail que vous devez effectuer est lié aux E / S ou lié au processeur, car cela peut affecter considérablement les performances de votre code et peut potentiellement conduire à une mauvaise utilisation de certaines constructions.
Voici deux questions que vous devez poser avant d'écrire un code:
Votre code "attendra" quelque chose, comme des données d'une base de données?
- Si votre réponse est «oui», votre travail est lié aux E / S.
Votre code effectuera-t-il un calcul très coûteux?
- Si vous avez répondu "oui", votre travail est lié au processeur.
Si le travail que vous avez est lié aux E / S, utilisez async et attendez sans Task.Run . Vous ne devez pas utiliser la bibliothèque parallèle de tâches. La raison de ceci est décrite dans l'article Async in Depth .
Si le travail que vous avez est lié au processeur et que vous vous souciez de la réactivité, utilisez async et attendez, mais générez le travail sur un autre thread avec Task.Run. Si le travail convient à la concurrence et au parallélisme, vous devez également envisager d'utiliser la bibliothèque parallèle de tâches .
Une application est liée au processeur lorsque les performances arithmétiques / logiques / à virgule flottante (A / L / FP) pendant l'exécution sont principalement proches des performances de pointe théoriques du processeur (données fournies par le fabricant et déterminées par les caractéristiques du processeur: nombre de cœurs, fréquence, registres, ALU, FPU, etc.).
La performance de coup d'oeil est très difficile à réaliser dans des applications du monde réel, pour ne pas dire impossible. La plupart des applications accèdent à la mémoire dans différentes parties de l'exécution et le processeur ne fait pas d'opérations A / L / FP pendant plusieurs cycles. C'est ce qu'on appelle la limitation Von Neumann en raison de la distance qui existe entre la mémoire et le processeur.
Si vous souhaitez être proche des performances maximales du processeur, une stratégie pourrait être d'essayer de réutiliser la plupart des données dans la mémoire cache afin d'éviter d'exiger des données de la mémoire principale. Un algorithme qui exploite cette fonctionnalité est la multiplication matrice-matrice (si les deux matrices peuvent être stockées dans la mémoire cache). Cela se produit car si les matrices sont de taille, n x n
vous devez effectuer des 2 n^3
opérations utilisant uniquement des 2 n^2
nombres FP de données. D'autre part, l'ajout de matrice, par exemple, est une application moins liée au processeur ou plus liée à la mémoire que la multiplication de la matrice, car elle ne nécessite que des n^2
FLOP avec les mêmes données.
Dans la figure suivante, les FLOP obtenus avec des algorithmes naïfs pour l'ajout de matrice et la multiplication de matrice dans un Intel i5-9300H, sont présentés:
Notez que, comme prévu, les performances de la multiplication matricielle sont supérieures à l'addition matricielle. Ces résultats peuvent être reproduits en exécutant test/gemm
et test/matadd
disponibles dans ce référentiel .
Je suggère également de voir la vidéo donnée par J. Dongarra sur cet effet.
Processus lié aux E / S: - Si la majeure partie de la durée de vie d'un processus est passée dans un état d'E / S, alors le processus est lié à un processus d'E / S. Exemple: -calculateur, Internet Explorer
Processus lié au CPU: - Si la majeure partie de la durée de vie du processus est passée en CPU, il s'agit d'un processus lié au CPU.