Test de performance du code Linux avec des exemples

Quand j'ai commencé à apprendre Java, l'une des premières tâches que j'ai essayé de résoudre était de déterminer les nombres pairs / impairs. Je connaissais plusieurs façons de procéder, mais j'ai décidé de chercher la «bonne» manière sur Internet. Les informations sur tous les liens trouvés m'ont indiqué la seule solution correcte du formulaire x% 2, afin d'obtenir le reste de la division. Si le reste est 0, le nombre est pair; si le reste est 1, il est impair.

Depuis l'époque de ZX Spectrum, je me suis souvenu d'une autre manière et elle est associée à la représentation des nombres dans le système binaire. Tout nombre dans le système décimal peut être écrit comme la somme des puissances de deux. Par exemple, pour un octet, et cela fait 8 bits, tout nombre dans le système décimal peut être représenté comme la somme des nombres 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1.

Ce n'est qu'une séquence de pouvoirs de deux. Lors de la traduction d'un nombre dans le système binaire, si nous devons tenir compte du nombre, dans la représentation binaire, ce sera un, sinon nécessaire, ce sera 0.

Par exemple:

10 = 1010 (8 + 0 + 2 + 0)
13 = 1101 (8 + 4 + 0 + 1)
200 = 11001000 (128 + 64 + 0 + 0 + 8 + 0 + 0 + 0)

Vous pouvez immédiatement faire attention au fait qu'un nombre impair ne peut donner qu'une puissance nulle de deux avec une valeur de 1, toutes les autres puissances de deux seront égales par définition. Cela signifie automatiquement que du point de vue du système de nombres binaires, si nous voulons vérifier la parité d'un nombre, nous n'avons pas besoin de vérifier le nombre entier, quelle que soit sa taille. Nous devons vérifier uniquement le premier bit (le plus à droite). S'il est égal à 0, alors le nombre est pair, puisque tous les autres bits donnent un nombre pair, et vice versa, s'il est un dans le bit le plus à droite, alors le nombre est garanti d'être impair, car tous les autres bits ne donnent qu'une valeur paire.
Pour vérifier uniquement le bon bit dans un nombre, vous pouvez utiliser plusieurs méthodes. L'un d'eux est ET binaire.

ET


ET binaire (ET) fonctionne selon la règle suivante. Si vous appliquez à n'importe quel nombre, appelons-le original, ET logique avec le nombre 0, alors le résultat d'une telle opération est toujours 0. Ainsi, vous pouvez mettre à zéro les bits dont vous n'avez pas besoin. Si vous postulez à l'original 1, vous obtenez l'original.

Dans un système binaire, il est facile d'écrire ceci:

0 et 0 = 0 (zéro l'original)
1 et 0 = 0 (zéro l'original)
0 ET 1 = 0 (ne pas modifier l'original)
1 ET 1 = 1 (ne change pas l'original) A

partir de là quelques simples règles.

Si nous appliquons l'opération ET de toutes les unités à tous les nombres (tous les bits sont activés), nous obtenons le même nombre initial.

Si nous appliquons ET de tous les zéros à n'importe quel nombre (tous les bits sont désactivés), nous obtenons 0.

Par exemple:

Si nous appliquons AND 0 à l'octet 13, nous obtiendrons 0. En décimal, cela ressemble à 13 AND 0 = 0

Si nous appliquons AND 0 à l'octet 200, nous obtiendrons 0, ou notons brièvement 200 AND 0 = 0.
Il en va de même pour le contraire, appliquer à 13 tous les bits inclus, pour un octet ce sera huit unités, et nous obtenons l'original. Dans le système binaire 00001101 ET 11111111 = 00001101 ou dans le système décimal 13 ET 255 = 13

Pour 200, il y aura respectivement 11001000 ET 11111111 = 11001000, ou dans le système décimal 200 ET 255 = 200

VĂ©rification binaire


Pour vérifier la parité du nombre, il suffit de vérifier le bit le plus à droite. Si c'est 0, alors le nombre est pair; si 1, alors ce n'est pas pair. Sachant qu'avec AND, nous pouvons laisser certains bits d'origine et certains que nous pouvons réinitialiser, nous pouvons simplement réinitialiser tous les bits sauf le plus à droite. Par exemple:

13 dans le système binaire est 1101. Appliquons ET 0001 à lui (nous remettons à zéro tous les bits, le dernier reste l'original). Nous changeons le nombre 1101 tous les bits en 0 sauf le dernier et obtenons 0001. Nous n'avons obtenu que le dernier bit de notre nombre d'origine. Dans le système décimal, il ressemblera à 13 ET 1 = 1.

La même chose avec le nombre 200, sous forme binaire 11001000. Nous lui appliquons ET 00000001, selon le même schéma, remettons à zéro tous les bits, laissons le dernier tel quel, nous obtenons 00000000, et nous remettons à zéro les 7 zéros de gauche avec AND, et nous avons obtenu le dernier 0 du numéro d'origine. Dans le système décimal, cela ressemble à 200 AND 1 = 0

Ainsi, en appliquant la commande AND 1 à n'importe quel nombre, nous obtenons 0 ou 1. Et si le résultat est 0, alors le nombre est pair. Lorsque 1, le nombre est impair.

En Java, le binaire ET s'écrit &. En conséquence, 200 & 1 = 0 (pair) et 13 & 1 = 1 (impair).

Cela implique au moins deux méthodes pour déterminer les nombres pairs.

X% 2 - par le reste de la division par deux
X & 1 - par ET binaire

Les opérations binaires telles que OR, AND, XOR sont traitées par le processeur en un minimum de temps. Mais l'opération de division est une tâche non triviale, et pour l'exécuter, le processeur doit traiter un grand nombre d'instructions, essentiellement exécuter le programme entier. Cependant, il existe des opérations de décalage gauche et droite binaires qui permettent, par exemple, de diviser rapidement un nombre par 2. La question est de savoir si les compilateurs utilisent cette optimisation et s'il existe une différence entre ces deux comparaisons, qui en fait font de même.

Codage


Nous écrirons un programme qui traitera 9 000 000 000 de nombres dans un cycle dans l'ordre, et déterminerons leur appartenance à pair / impair en déterminant le reste de la division.

public class OddEvenViaMod {
        public static void main (String[] args) {
                long i=0;
                long odds=0;
                long evens=0;
                do {
                if ((i % 2) == 0) {
                        evens++;
                        }
                else {
                        odds++;
                        }
                i++;
                } while (i<9000000000L);
                System.out.println("Odd " + odds);
                System.out.println("Even " + evens);
        }
}

Et nous écrirons exactement la même chose, mais changerons littéralement deux caractères, en vérifiant la même chose via ET binaire.

public class OddEvenViaAnd {
        public static void main (String[] args) {
                long i=0;
                long odds=0;
                long evens=0;
                do {
                if ((i & 1) == 0) {
                        evens++;
                        }
                else {
                        odds++;
                        }
                i++;
                } while (i<9000000000L);
                System.out.println("Odd " + odds);
                System.out.println("Even " + evens);

Maintenant, nous devons en quelque sorte comparer ces deux programmes.

Ressources sur Linux. CPU


Des heures considérables ont été consacrées à la création de tout système d'exploitation, en particulier à une répartition équitable des ressources entre les programmes. D'une part, c'est bien, puisque l'exécution de deux programmes, vous pouvez être sûr qu'ils fonctionneront en parallèle, mais d'autre part, lorsque vous devez vérifier les performances d'un programme, il est extrêmement nécessaire de limiter ou au moins de réduire l'impact externe sur le programme des autres programmes et système d'exploitation.

La première chose à comprendre est le processeur. Le système d'exploitation Linux pour chaque processus stocke un masque de bits, qui indique quels noyaux peuvent être utilisés par l'application et lesquels ne le sont pas. Vous pouvez afficher et modifier ce masque avec la commande tasket.

Par exemple, voyons le nombre de cœurs dans mon processeur:

[user@localhost]# grep -c processor /proc/cpuinfo
4

Mon ordinateur possède un processeur avec 4 cœurs. C'est bien, car je vais en affecter un à mes besoins.

Voyons si tous sont actuellement utilisés avec la commande top:

[user@localhost]# top

Appuyez sur "1" pour afficher séparément les informations sur chaque cœur:

top - 13:44:11 up 1 day, 23:26,  7 users,  load average: 1.48, 2.21, 2.02
Tasks: 321 total,   1 running, 320 sleeping,   0 stopped,   0 zombie
%Cpu0  :  7.7 us,  6.8 sy,  0.0 ni, 85.5 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu1  :  9.2 us,  4.2 sy,  0.0 ni, 86.6 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu2  :  7.6 us,  3.4 sy,  0.0 ni, 89.1 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu3  :  8.4 us,  4.2 sy,  0.0 ni, 87.4 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem : 16210820 total,   296972 free, 10072092 used,  5841756 buff/cache
KiB Swap: 16777212 total, 16777212 free,        0 used.  5480568 avail Mem
....

Ici, nous voyons que tous les cœurs sont utilisés à peu près de la même manière. (les indicateurs us et sy et id sont approximativement égaux pour chaque noyau).

Essayons maintenant de voir la mĂŞme chose avec la commande tasket.

[user@localhost]# taskset -p 1
pid 1's current affinity mask: f

Le masque binaire "F" dans le système hexadécimal signifie 15 en décimal, ou 1111 en binaire (8 + 4 + 2 + 1). Tous les bits sont activés, ce qui signifie que tous les cœurs sont utilisés par un processus avec PID 1.
Sous Linux, lorsqu'un processus en génère un autre avec un appel système clone, le masque binaire est copié à partir du parent au moment du clonage. Cela signifie que si nous changeons ce masque pour notre processus d'initialisation (dans mon cas, c'est systemd), alors lors du démarrage de tout nouveau processus via systemd, ce nouveau processus sera déjà lancé avec un nouveau masque.

Vous pouvez modifier le masque du processus à l'aide de la même commande, en répertoriant le nombre de cœurs de processeur que nous voulons laisser utilisés pour le processus. Supposons que nous voulons laisser le noyau 0.2.3 pour notre processus, et nous voulons désactiver le noyau 1 pour notre processus systemd. Pour ce faire, nous devons exécuter la commande:

[user@localhost]#  taskset -pc 0,2,3 1
pid 1's current affinity list: 0-3
pid 1's new affinity list: 0,2,3

Nous vérifions:

[user@localhost]# taskset -p 1
pid 1's current affinity mask: d

Le masque a changé en "D" dans la notation hexadécimale, qui est 13 en décimal et 1101 en binaire (8 + 4 + 0 + 1).

Désormais, tout processus qui sera cloné par le processus systemd aura automatiquement un masque 1101 d'utilisation CPU, ce qui signifie que le noyau numéro 1 ne sera pas utilisé.

Nous interdisons l'utilisation du noyau Ă  tous les processus


Empêcher le processus Linux principal d'utiliser un seul noyau n'affectera que les nouveaux processus créés par ce processus. Mais dans mon système, il n'y a déjà pas un processus, mais toute une multitude, comme crond, sshd, bash et autres. Si je dois interdire à tous les processus d'utiliser un seul cœur, je dois exécuter la commande tasket pour chaque processus en cours d'exécution.

Pour obtenir une liste de tous les processus, nous utiliserons l'API que le noyau nous donne, à savoir le système de fichiers / proc.

Plus loin dans la boucle, nous regardons le PID de chaque processus en cours d'exécution et changeons le masque pour lui et tous les threads:

[user@localhost]# cd /proc; for i in `ls -d [0-9]*`; do taskset -a -pc 0,2,3 $i; done
pid 1's current affinity list: 0,2,3
pid 1's new affinity list: 0,2,3
...

Étant donné que pendant l'exécution du programme, certains processus peuvent avoir le temps de générer d'autres processus, il est préférable d'exécuter cette commande plusieurs fois.

Vérifiez le résultat de notre travail avec la commande top:

[user@localhost]# top
top - 14:20:46 up 2 days, 3 min,  7 users,  load average: 0.19, 0.27, 0.57
Tasks: 324 total,   4 running, 320 sleeping,   0 stopped,   0 zombie
%Cpu0  :  8.9 us,  7.7 sy,  0.0 ni, 83.4 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu1  :  0.0 us,  0.0 sy,  0.0 ni,100.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu2  :  9.5 us,  6.0 sy,  0.0 ni, 84.5 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu3  :  8.4 us,  6.6 sy,  0.0 ni, 85.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem : 16210820 total,   285724 free, 10142548 used,  5782548 buff/cache
KiB Swap: 16777212 total, 16777212 free,        0 used.  5399648 avail Mem

Comme vous pouvez le voir, l'image a un peu changé, maintenant pour le noyau 0.2.3 les paramètres moyens us, sy, id sont les mêmes pour nous, et pour le noyau 1 notre consommation de base dans l'espace utilisateur et sys est de 0, et le noyau est inactif à 100% (100 inactif) ) Le noyau 1 n'est plus utilisé par nos applications et un très faible pourcentage est actuellement utilisé par le noyau.

Désormais, la tâche de tester les performances se réduit à démarrer notre processus sur un cœur libre.

MĂ©moire


La mémoire physique allouée à un processus peut être facilement extraite de n'importe quel processus. Ce mécanisme est appelé swap. Si Linux a une place pour l'échange, il le fera de toute façon. La seule façon d'empêcher le système d'exploitation de prendre de la mémoire de notre processus, comme tout autre processus, est de désactiver complètement la section swap, ce que nous ferons:

[user@localhost]$ sudo swapoff -a
[user@localhost]$ free -m
              total        used        free      shared  buff/cache   available
Mem:          15830        7294        1894         360        6641        7746
Swap:             0           0           0

Nous avons alloué 1 cœur de processeur, qui n'est pas utilisé, et nous avons également supprimé la possibilité d'échanger de la mémoire du noyau Linux.

Disque


Afin de réduire l'impact du disque sur le lancement de notre processus, créez un disque en mémoire et copiez tous les fichiers nécessaires sur ce disque.

Créez un répertoire et montez le système de fichiers:

[user@localhost]$ sudo mkdir /mnt/ramdisk;
[user@localhost]$ mount -t tmpfs -o size=512m tmpfs /mnt/ramdisk
[user@localhost]$ chown user: /mnt/ramdisk

Nous devons maintenant déterminer quoi et comment nous prévoyons de le lancer. Afin d'exécuter notre programme, nous devons d'abord compiler notre code:

[user@localhost]$ javac OddEvenViaMod.java

Ensuite, vous devez l'exécuter:

[user@localhost]$ java OddEvenViaMod

Mais dans notre cas, nous voulons exécuter le processus sur le cœur du processeur qui n'est utilisé par aucun autre processus. Par conséquent, exécutez-le via l'ensemble de tâches:

[user@localhost]# taskset -c 1 time java OddEvenViaMod

Dans nos tests, nous devons mesurer le temps, donc notre ligne de lancement se transforme en

taskset -c 1 time java OddEvenViaMod

Linux OS prend en charge plusieurs formats de fichiers exécutables, et le plus courant d'entre eux est le format ELF. Ce format de fichier vous permet de dire au système d'exploitation de ne pas exécuter votre fichier, mais d'exécuter un autre fichier. À première vue, cela ne semble pas très logique et compréhensible. Imaginez que je lance le jeu Démineur et que le jeu Mario démarre pour moi - il ressemble à un virus. Mais c'est la logique. Si mon programme nécessite une bibliothèque dynamique, par exemple libc, ou toute autre, cela signifie que le système d'exploitation doit d'abord charger cette bibliothèque en mémoire, puis charger et exécuter mon programme. Et il semble logique de placer une telle fonctionnalité dans le système d'exploitation lui-même, mais le système d'exploitation fonctionne dans une zone protégée de mémoire et devrait contenir le moins de fonctionnalités possible et nécessaire.Par conséquent, le format ELF offre la possibilité de dire au système d'exploitation que nous voulons télécharger un autre programme, et cet "autre" programme téléchargera toutes les bibliothèques nécessaires et notre programme et démarrera le tout.

Donc, nous devons exécuter 3 fichiers, c'est le jeu de tâches, le temps, java.

VĂ©rifiez le premier d'entre eux:

[user@localhost]$ whereis taskset
taskset: /usr/bin/taskset /usr/share/man/man1/taskset.1.gz

Bash exécutera le fichier / usr / bin / taskset, vérifiez ce qu'il contient:

[user@localhost]$ file /usr/bin/taskset
/usr/bin/taskset: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=7a2fd0779f64aa9047faa00f498042f0f0c5dc60, stripped

Il s'agit du fichier ELF dont j'ai parlé plus haut. Dans le fichier ELF, en plus du programme lui-même, il existe différents en-têtes. En lançant ce fichier, le système d'exploitation vérifie ses en-têtes et si l'en-tête «Requesting program interpreter» existe dans le fichier, le système d'exploitation lancera le fichier à partir de cet en-tête et transmettra le fichier initialement lancé comme argument.

VĂ©rifiez si cet en-tĂŞte existe dans notre fichier ELF:

[user@localhost]$ readelf -a /usr/bin/taskset  | grep -i interpreter
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]

L'en-tête existe, ce qui signifie qu'en lançant le fichier / usr / bin / tasket nous exécutons réellement /lib64/ld-linux-x86-64.so.2.

VĂ©rifiez ce qu'est ce fichier:

[user@localhost]$ ls -lah /lib64/ld-linux-x86-64.so.2
lrwxrwxrwx 1 root root 10 May 21  2019 /lib64/ld-linux-x86-64.so.2 -> ld-2.17.so

Il s'agit d'un lien sim vers le fichier /lib64/ld-2.17.so. VĂ©rifiez-le:

[user@localhost]$ file /lib64/ld-2.17.so
/lib64/ld-2.17.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=a527fe72908703c5972ae384e78d1850d1881ee7, not stripped

Comme vous pouvez le voir, il s'agit d'un autre fichier ELF que le système d'exploitation exécutera. Nous regardons les en-têtes:

[user@localhost]$ readelf -a /lib64/ld-2.17.so  | grep -i interpreter
[user@localhost]$

Nous voyons que ce fichier ELF n'a pas un tel en-tête, donc le système d'exploitation exécutera ce fichier et lui transférera le contrôle. Et déjà ce fichier ouvrira notre fichier / usr / bin / tasket, lisez à partir de là des informations sur toutes les bibliothèques nécessaires. La liste des bibliothèques requises se trouve également dans les en-têtes du fichier ELF. On peut regarder cette liste avec la commande ldd ou readelf, qui est la même chose:

[user@localhost]$ ldd /usr/bin/taskset
	linux-vdso.so.1 =>  (0x00007ffc4c1df000)
	libc.so.6 => /lib64/libc.so.6 (0x00007f4a24c4e000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f4a2501b000)

[user@localhost]$ readelf -a /usr/bin/taskset  | grep NEEDED
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]

VDSO est un morceau de mémoire lié qui n'est pas lié aux bibliothèques, il est donc absent du fichier ELF en tant que bibliothèque nécessaire.

De cela, il est clair que le programme /lib64/ld-2.17.so est responsable de l'exécution de tous les programmes qui en ont besoin, et ce sont tous des programmes avec des bibliothèques liées dynamiquement.

Si nous exécutons / usr / bin / taskset, c'est exactement la même chose que nous exécutons /lib64/ld-2.17.so avec l'argument / usr / bin / taskset.

Nous revenons au problème de l'influence du disque sur nos tests. Maintenant, nous savons que si nous voulons charger notre programme de la mémoire, nous devons copier non pas un fichier, mais plusieurs:

[user@localhost]$ cp /lib64/libc-2.17.so /mnt/ramdisk/
[user@localhost]$ cp /lib64/ld-2.17.so /mnt/ramdisk/
[user@localhost]$ cp /usr/bin/taskset /mnt/ramdisk/

Nous faisons de même pour le temps, les exigences de la bibliothèque sont exactement les mêmes (nous avons déjà copié ld et libc).

[user@localhost]$ cp /usr/bin/time /mnt/ramdisk/

Pour java, les choses sont un peu plus compliquées, car java nécessite de nombreuses bibliothèques différentes qui peuvent être copiées pendant longtemps. Pour simplifier un peu ma vie, je vais copier tout le répertoire de mon openjdk java sur un disque en mémoire et créer un lien sim. Bien sûr, les accès disque resteront dans ce cas, mais il y en aura moins.

[user@localhost]$ cp -R /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.222.b10-0.el7_6.x86_64 /mnt/ramdisk/

Renommez l'ancien répertoire en y ajoutant le .default final

[user@localhost]$ sudo mv /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.222.b10-0.el7_6.x86_64{,.default}

Et créez un lien symbolique:

[user@localhost]$ sudo ln -s /mnt/ramdisk/java-1.8.0-openjdk-1.8.0.222.b10-0.el7_6.x86_64 /usr/lib/jvm/

Nous savons déjà comment exécuter un fichier binaire via l'argument du fichier /lib64/ld-2.17.so, qui démarre réellement. Mais comment faire pour que le programme /lib64/ld-2.17.so charge les bibliothèques chargées à partir du répertoire que nous avons spécifié? man ld pour nous aider, d'où nous apprenons que si vous déclarez la variable d'environnement LD_LIBRARY_PATH, le programme ld chargera les bibliothèques à partir des répertoires que nous spécifions. Nous avons maintenant toutes les données pour préparer la ligne de lancement de l'application Java.

Nous commençons plusieurs fois de suite et vérifions:

[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so java OddEvenViaMod
Odd 4500000000
Even 4500000000
10.66user 0.01system 0:10.67elapsed 99%CPU (0avgtext+0avgdata 20344maxresident)k
0inputs+64outputs (0major+5229minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so java OddEvenViaMod
Odd 4500000000
Even 4500000000
10.65user 0.01system 0:10.67elapsed 99%CPU (0avgtext+0avgdata 20348maxresident)k
0inputs+64outputs (0major+5229minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so java OddEvenViaMod
Odd 4500000000
Even 4500000000
10.66user 0.01system 0:10.68elapsed 99%CPU (0avgtext+0avgdata 20348maxresident)k
0inputs+64outputs (0major+5229minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so java OddEvenViaMod
Odd 4500000000
Even 4500000000
10.65user 0.01system 0:10.67elapsed 99%CPU (0avgtext+0avgdata 20348maxresident)k
0inputs+96outputs (0major+5234minor)pagefaults 0swaps
[user@localhost ramdisk]$

Pendant l'exécution du programme, nous pouvons exécuter top et nous assurer que le programme s'exécute sur le noyau CPU correct.

[user@localhost ramdisk]$ top
...
%Cpu0  : 19.7 us, 11.7 sy,  0.0 ni, 68.6 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu1  :100.0 us,  0.0 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu2  :  9.8 us,  9.1 sy,  0.0 ni, 81.1 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu3  : 14.0 us,  9.0 sy,  0.0 ni, 77.1 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 s
...

Comme vous pouvez le voir, les résultats dans la plupart des cas sont similaires. Malheureusement, nous ne pouvons pas supprimer complètement l'influence du système d'exploitation sur le cœur du processeur, donc le résultat dépend toujours des tâches spécifiques à l'intérieur du noyau Linux au moment du lancement. Par conséquent, il est préférable d'utiliser la médiane des valeurs de plusieurs démarrages.

Dans notre cas, nous voyons que le programme java traite 9 000 000 000 avec parité à travers le reste de la division en 10,65 secondes sur un cœur du CPU.

Faisons le même test avec notre deuxième programme, qui fait la même chose via AND binaire.

[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so java OddEvenViaAnd
Odd 4500000000
Even 4500000000
4.02user 0.00system 0:04.03elapsed 99%CPU (0avgtext+0avgdata 20224maxresident)k
0inputs+64outputs (0major+5197minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so java OddEvenViaAnd
Odd 4500000000
Even 4500000000
4.01user 0.00system 0:04.03elapsed 99%CPU (0avgtext+0avgdata 20228maxresident)k
0inputs+64outputs (0major+5199minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so java OddEvenViaAnd
Odd 4500000000
Even 4500000000
4.01user 0.01system 0:04.03elapsed 99%CPU (0avgtext+0avgdata 20224maxresident)k
0inputs+64outputs (0major+5198minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so java OddEvenViaAnd
Odd 4500000000
Even 4500000000
4.02user 0.00system 0:04.03elapsed 99%CPU (0avgtext+0avgdata 20224maxresident)k
0inputs+64outputs (0major+5198minor)pagefaults 0swaps

Maintenant, nous pouvons dire avec confiance que la comparaison de la parité via ET binaire prend 4,02 secondes, ce qui signifie que par rapport au contrôle dans le reste de la division, cela fonctionne 2,6 fois plus rapidement, au moins sur openjdk version 1.8.0.

Oracle Java vs Openjdk


J'ai téléchargé et décompressé java jdk sur le site Web oracle dans le répertoire /mnt/ramdisk/jdk-13.0.2.

Compile:

[user@localhost ramdisk]$ /mnt/ramdisk/jdk-13.0.2/bin/javac OddEvenViaAnd.java

Nous lançons:

[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/jdk-13.0.2/bin/java OddEvenViaAnd
Odd 4500000000
Even 4500000000
10.39user 0.01system 0:10.41elapsed 99%CPU (0avgtext+0avgdata 24260maxresident)k
0inputs+64outputs (0major+6979minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/jdk-13.0.2/bin/java OddEvenViaAnd
Odd 4500000000
Even 4500000000
10.40user 0.01system 0:10.42elapsed 99%CPU (0avgtext+0avgdata 24268maxresident)k
0inputs+64outputs (0major+6985minor)pagefaults 0swaps

Nous compilons le deuxième programme:

[user@localhost ramdisk]$ /mnt/ramdisk/jdk-13.0.2/bin/javac OddEvenViaMod.java

Nous lançons:

[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/jdk-13.0.2/bin/java OddEvenViaMod
Odd 4500000000
Even 4500000000
10.39user 0.01system 0:10.40elapsed 99%CPU (0avgtext+0avgdata 24324maxresident)k
0inputs+96outputs (0major+7003minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/jdk-13.0.2/bin/java OddEvenViaMod
Odd 4500000000
Even 4500000000
10.40user 0.00system 0:10.41elapsed 99%CPU (0avgtext+0avgdata 24316maxresident)k
0inputs+64outputs (0major+6992minor)pagefaults 0swaps

Le temps d'exécution des mêmes sources dans oracle jdk est le même pour le reste de la division et AND binaire, ce qui semble normal, mais cette fois est également mauvais, ce qui a été affiché dans openjdk sur le reste de la division.

Python


Essayons de comparer la mĂŞme chose en Python. Tout d'abord, l'option avec le reste de la division par 2:

odd=0
even=0
for i in xrange(100000000):
	if i % 2 == 0:
		even += 1
	else:
		odd += 1
print "even", even
print "odd", odd

Nous lançons:

[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/python2.7 odd_mod.py
even 50000000
odd 50000000
11.69user 0.00system 0:11.69elapsed 99%CPU (0avgtext+0avgdata 4584maxresident)k
0inputs+0outputs (0major+1220minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/python2.7 odd_mod.py
even 50000000
odd 50000000
11.67user 0.00system 0:11.67elapsed 99%CPU (0avgtext+0avgdata 4584maxresident)k
0inputs+0outputs (0major+1220minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/python2.7 odd_mod.py
even 50000000
odd 50000000
11.66user 0.00system 0:11.66elapsed 99%CPU (0avgtext+0avgdata 4584maxresident)k
0inputs+0outputs (0major+1220minor)pagefaults 0swaps

Maintenant, la mĂŞme chose avec ET binaire:

odd=0
even=0
for i in xrange(100000000):
	if i & 1 == 0:
		even += 1
	else:
		odd += 1
print "even", even
print "odd", odd

Nous lançons:

[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/python2.7 odd_and.py
even 50000000
odd 50000000
10.41user 0.00system 0:10.41elapsed 99%CPU (0avgtext+0avgdata 4588maxresident)k
0inputs+0outputs (0major+1221minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/python2.7 odd_and.py
even 50000000
odd 50000000
10.43user 0.00system 0:10.43elapsed 99%CPU (0avgtext+0avgdata 4588maxresident)k
0inputs+0outputs (0major+1222minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/python2.7 odd_and.py
even 50000000
odd 50000000
10.43user 0.00system 0:10.43elapsed 99%CPU (0avgtext+0avgdata 4584maxresident)k
0inputs+0outputs (0major+1222minor)pagefaults 0swaps

Les résultats montrent que ET est plus rapide.

Sur Internet, il a été écrit à plusieurs reprises que les variables globales en Python sont plus lentes. J'ai décidé de comparer le temps d'exécution du dernier programme avec ET et exactement le même, mais enveloppé dans une fonction:

def main():
	odd=0
	even=0
	for i in xrange(100000000):
		if i & 1 == 0:
			even += 1
		else:
			odd += 1
	print "even", even
	print "odd", odd

main()

Exécutez la fonction:

[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/python2.7 odd_and_func.py
even 50000000
odd 50000000
5.08user 0.00system 0:05.08elapsed 99%CPU (0avgtext+0avgdata 4592maxresident)k
0inputs+0outputs (0major+1222minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/python2.7 odd_and_func.py
even 50000000
odd 50000000
5.08user 0.00system 0:05.09elapsed 99%CPU (0avgtext+0avgdata 4592maxresident)k
0inputs+0outputs (0major+1223minor)pagefaults 0swaps

Comme vous pouvez le voir, la même comparaison de parité en Python via ET binaire dans une fonction traite 100000000 nombres sur un seul cœur de processeur en ~ 5 secondes, la même comparaison via AND sans fonction prend ~ 10 secondes et la comparaison sans fonction via le reste de la division prend ~ 11 secondes

La raison pour laquelle un programme Python dans une fonction fonctionne plus rapidement que sans lui a déjà été décrite plus d'une fois et est liée à la portée des variables.

Python a la capacité de désassembler un programme en fonctions internes que Python utilise lors de l'interprétation d'un programme. Voyons quelles fonctions Python utilise pour la variante avec la fonction odd_and_func.py:

[user@localhost ramdisk]# python
Python 2.7.5 (default, Jun 20 2019, 20:27:34)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-36)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> def main():
...     odd=0
...     even=0
...     for i in xrange(100000000):
...             if i & 1 == 0:
...                     even += 1
...             else:
...                     odd += 1
...     print "even", even
...     print "odd", odd
...
>>> import dis
>>> dis.dis(main)
  2           0 LOAD_CONST               1 (0)
              3 STORE_FAST               0 (odd)

  3           6 LOAD_CONST               1 (0)
              9 STORE_FAST               1 (even)

  4          12 SETUP_LOOP              59 (to 74)
             15 LOAD_GLOBAL              0 (xrange)
             18 LOAD_CONST               2 (100000000)
             21 CALL_FUNCTION            1
             24 GET_ITER
        >>   25 FOR_ITER                45 (to 73)
             28 STORE_FAST               2 (i)

  5          31 LOAD_FAST                2 (i)
             34 LOAD_CONST               3 (1)
             37 BINARY_AND
             38 LOAD_CONST               1 (0)
             41 COMPARE_OP               2 (==)
             44 POP_JUMP_IF_FALSE       60

  6          47 LOAD_FAST                1 (even)
             50 LOAD_CONST               3 (1)
             53 INPLACE_ADD
             54 STORE_FAST               1 (even)
             57 JUMP_ABSOLUTE           25

  8     >>   60 LOAD_FAST                0 (odd)
             63 LOAD_CONST               3 (1)
             66 INPLACE_ADD
             67 STORE_FAST               0 (odd)
             70 JUMP_ABSOLUTE           25
        >>   73 POP_BLOCK

  9     >>   74 LOAD_CONST               4 ('even')
             77 PRINT_ITEM
             78 LOAD_FAST                1 (even)
             81 PRINT_ITEM
             82 PRINT_NEWLINE

 10          83 LOAD_CONST               5 ('odd')
             86 PRINT_ITEM
             87 LOAD_FAST                0 (odd)
             90 PRINT_ITEM
             91 PRINT_NEWLINE
             92 LOAD_CONST               0 (None)
             95 RETURN_VALUE

Et vérifiez la même chose sans utiliser la fonction dans notre code:

>>> f=open("odd_and.py","r")
>>> l=f.read()
>>>
>>> l
'odd=0\neven=0\nfor i in xrange(100000000):\n\tif i & 1 == 0:\n\t\teven += 1\n\telse:\n\t\todd += 1\nprint "even", even\nprint "odd", odd\n'
>>> k=compile(l,'l','exec')
>>> k
<code object <module> at 0x7f2bdf39ecb0, file "l", line 1>
>>> dis.dis(k)
  1           0 LOAD_CONST               0 (0)
              3 STORE_NAME               0 (odd)

  2           6 LOAD_CONST               0 (0)
              9 STORE_NAME               1 (even)

  3          12 SETUP_LOOP              59 (to 74)
             15 LOAD_NAME                2 (xrange)
             18 LOAD_CONST               1 (100000000)
             21 CALL_FUNCTION            1
             24 GET_ITER
        >>   25 FOR_ITER                45 (to 73)
             28 STORE_NAME               3 (i)

  4          31 LOAD_NAME                3 (i)
             34 LOAD_CONST               2 (1)
             37 BINARY_AND
             38 LOAD_CONST               0 (0)
             41 COMPARE_OP               2 (==)
             44 POP_JUMP_IF_FALSE       60

  5          47 LOAD_NAME                1 (even)
             50 LOAD_CONST               2 (1)
             53 INPLACE_ADD
             54 STORE_NAME               1 (even)
             57 JUMP_ABSOLUTE           25

  7     >>   60 LOAD_NAME                0 (odd)
             63 LOAD_CONST               2 (1)
             66 INPLACE_ADD
             67 STORE_NAME               0 (odd)
             70 JUMP_ABSOLUTE           25
        >>   73 POP_BLOCK

  8     >>   74 LOAD_CONST               3 ('even')
             77 PRINT_ITEM
             78 LOAD_NAME                1 (even)
             81 PRINT_ITEM
             82 PRINT_NEWLINE

  9          83 LOAD_CONST               4 ('odd')
             86 PRINT_ITEM
             87 LOAD_NAME                0 (odd)
             90 PRINT_ITEM
             91 PRINT_NEWLINE
             92 LOAD_CONST               5 (None)
             95 RETURN_VALUE

Comme vous pouvez le voir, dans la variante avec la fonction déclarée, Python utilise des fonctions internes avec le suffixe FAST, par exemple, STORE_FAST, LOAD_FAST, et dans la variante sans la déclaration de la fonction, Python utilise les fonctions internes STORE_NAME et LOAD_NAME.

Cet article a peu de sens pratique et vise davantage à comprendre certaines des fonctionnalités de Linux et des compilateurs.

Bon Ă  tous!

All Articles