Testen der Linux-Codeleistung anhand von Beispielen

Als ich anfing, Java zu lernen, bestand eine der ersten Aufgaben, die ich zu lösen versuchte, darin, gerade / ungerade Zahlen zu bestimmen. Ich kannte verschiedene Möglichkeiten, um dies zu tun, entschied mich jedoch, im Internet nach dem „richtigen“ Weg zu suchen. Die Informationen auf allen gefundenen Links sagten mir über die einzig richtige Lösung der Form x% 2, um den Rest der Division zu erhalten. Wenn der Rest 0 ist, ist die Zahl gerade, wenn der Rest 1 ist, ist es ungerade.

Seit der Zeit von ZX Spectrum habe ich mich an einen anderen Weg erinnert, der mit der Darstellung von Zahlen im Binärsystem verbunden ist. Jede Zahl im Dezimalsystem kann als Summe der Zweierpotenzen geschrieben werden. Beispielsweise kann für ein Byte, und dies sind 8 Bits, eine beliebige Zahl im Dezimalsystem als die Summe der Zahlen 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 dargestellt werden.

Dies ist nur eine Folge von Zweierpotenzen. Wenn wir eine Zahl in das Binärsystem übersetzen und die Zahl berücksichtigen müssen, ist sie in der Binärdarstellung eins, wenn nicht erforderlich, ist sie 0.

Zum Beispiel:

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

Sie können sofort darauf achten, dass eine ungerade Zahl nur eine Zweierpotenz von Null mit einem Wert von 1 ergeben kann, alle anderen Zweierpotenzen sind per Definition gerade. Dies bedeutet automatisch, dass wir aus Sicht des Binärzahlensystems, wenn wir eine Zahl auf Parität prüfen möchten, nicht die ganze Zahl prüfen müssen, egal wie groß sie ist. Wir müssen nur das erste Bit (ganz rechts) überprüfen. Wenn es 0 ist, ist die Zahl gerade, da alle anderen Bits eine gerade Zahl ergeben, und umgekehrt, wenn es eine ganz rechts ist, ist die Zahl garantiert ungerade, da alle anderen Bits nur einen geraden Wert ergeben.
Um nur das richtige Bit in einer Zahl zu überprüfen, können Sie verschiedene Methoden verwenden. Eines davon ist binäres UND.

UND


Binäres UND (UND) funktioniert nach der folgenden Regel. Wenn Sie eine beliebige Zahl anwenden, nennen wir sie original, logisch UND mit der Zahl 0, dann ist das Ergebnis einer solchen Operation immer 0. Auf diese Weise können Sie die nicht benötigten Bits auf Null setzen. Wenn Sie sich für das Original 1 bewerben, erhalten Sie das Original.

In einem binären System ist es einfach, Folgendes zu schreiben:

0 UND 0 = 0 (Null das Original)
1 UND 0 = 0 (Null das Original)
0 UND 1 = 0 (Original nicht ändern)
1 UND 1 = 1 (Original nicht ändern)

Von hier aus einige einfache Regeln.

Wenn wir die UND-Verknüpfung aller Einheiten auf alle Zahlen anwenden (alle Bits sind eingeschaltet), erhalten wir dieselbe Anfangsnummer.

Wenn wir UND aller Nullen auf eine beliebige Zahl anwenden (alle Bits sind ausgeschaltet), erhalten wir 0.

Zum Beispiel:

Wenn wir AND 0 auf Byte 13 anwenden, erhalten wir 0. In Dezimalzahlen sieht es so aus, als ob 13 AND 0 = 0

Wenn wir AND 0 auf Byte 200 anwenden, erhalten wir 0 oder schreiben 200 AND 0 = 0 kurz auf.
Das Gleiche ist das Gegenteil. Wenden Sie auf 13 alle enthaltenen Bits an, für ein Byte sind es acht Einheiten, und wir erhalten das Original. Im binären System 00001101 UND 11111111 = 00001101 oder im Dezimalsystem 13 UND 255 = 13

Für 200 gibt es 11001000 UND 11111111 = 11001000 bzw. im Dezimalsystem 200 UND 255 = 200

Binäre Überprüfung


Um die Zahl auf Parität zu überprüfen, müssen wir nur das Bit ganz rechts überprüfen. Wenn es 0 ist, ist die Zahl gerade, wenn 1, dann ist es nicht gerade. Wenn wir wissen, dass wir mit AND einige Bits original lassen können und einige zurücksetzen können, können wir einfach alle Bits außer dem am weitesten rechts liegenden zurücksetzen. Zum Beispiel:

13 im Binärsystem ist 1101. Wenden wir AND 0001 darauf an (wir setzen alle Bits zurück, das letzte bleibt das Original). Im Jahr 1101 ändern wir alle Bits bis auf das letzte auf 0 und erhalten 0001. Wir haben nur das letzte Bit von unserer ursprünglichen Nummer erhalten. Im Dezimalsystem sieht es aus wie 13 UND 1 = 1.

Das Gleiche gilt für die Zahl 200 in binärer Form 11001000. Wir wenden AND 00000001 an, indem wir nach demselben Schema alle Bits auf Null setzen, das letzte so lassen, wie es ist, 00000000 erhalten und die linken 7 Nullen mit AND zurücksetzen und die letzte 0 zurücksetzen von der ursprünglichen Nummer. Im Dezimalsystem sieht es so aus, als ob 200 UND 1 = 0.

Wenn wir also den Befehl UND 1 auf eine beliebige Zahl anwenden, erhalten wir entweder 0 oder 1. Wenn das Ergebnis 0 ist, ist die Zahl gerade. Bei 1 ist die Zahl ungerade.

In Java wird das binäre UND als & geschrieben. Dementsprechend ist 200 & 1 = 0 (gerade) und 13 & 1 = 1 (ungerade).

Dies impliziert mindestens zwei Methoden zur Bestimmung gerader Zahlen.

X% 2 - durch den Rest der Division durch zwei
X & 1 - durch binäres UND

Binäroperationen wie OR, AND, XOR werden vom Prozessor in kürzester Zeit verarbeitet. Die Teilungsoperation ist jedoch eine nicht triviale Aufgabe, und um sie auszuführen, muss der Prozessor viele Anweisungen verarbeiten und im Wesentlichen das gesamte Programm ausführen. Es gibt jedoch binäre Links- und Rechtsverschiebungsoperationen, mit denen beispielsweise eine Zahl schnell durch 2 geteilt werden kann. Die Frage ist, ob Compiler diese Optimierung verwenden und ob es einen Unterschied zwischen diesen beiden Vergleichen gibt, die tatsächlich dasselbe tun.

Codierung


Wir werden ein Programm schreiben, das 9.000.000.000 Zahlen in einem Zyklus der Reihe nach verarbeitet und ihre Zugehörigkeit zu gerade / ungerade bestimmt, indem wir den Rest der Division bestimmen.

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);
        }
}

Und wir werden genau das Gleiche schreiben, aber buchstäblich zwei Zeichen ändern und dasselbe durch binäres UND überprüfen.

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);

Jetzt müssen wir diese beiden Programme irgendwie vergleichen.

Ressourcen unter Linux. Zentralprozessor


Es wurde sehr viel Zeit für die Erstellung eines Betriebssystems aufgewendet, insbesondere für die gerechte Verteilung der Ressourcen zwischen den Programmen. Dies ist einerseits gut, da Sie sicher sein können, dass zwei Programme parallel ausgeführt werden. Wenn Sie jedoch die Leistung eines Programms überprüfen müssen, ist es äußerst wichtig, den externen Einfluss anderer auf das Programm zu begrenzen oder zumindest zu verringern Programme und Betriebssystem.

Das erste, was herauszufinden ist, ist der Prozessor. Das Linux-Betriebssystem speichert für jeden Prozess eine Bitmaske, die angibt, welche Kernel von der Anwendung verwendet werden können und welche nicht. Sie können diese Maske mit dem Task-Set-Befehl anzeigen und ändern.

Sehen wir uns zum Beispiel die Anzahl der Kerne in meinem Prozessor an:

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

Mein Computer hat einen Prozessor mit 4 Kernen. Das ist gut, weil ich einen von ihnen meinen Bedürfnissen zuordnen werde.

Mal sehen, ob alle derzeit mit dem Befehl top verwendet werden:

[user@localhost]# top

Drücken Sie "1", um Informationen zu jedem Kern separat anzuzeigen:

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
....

Hier sehen wir, dass alle Kerne ungefähr gleich verwendet werden. (us- und sy- und id-Indikatoren sind für jeden Kern ungefähr gleich).

Versuchen wir nun, dasselbe mit dem Task-Befehl zu sehen.

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

Die Bitmaske "F" im Hexadezimalsystem bedeutet 15 in Dezimalzahl oder 1111 in Binär (8 + 4 + 2 + 1). Alle Bits sind aktiviert, was bedeutet, dass alle Kerne von einem Prozess mit PID 1 verwendet werden.
Unter Linux wird die Bitmaske zum Zeitpunkt des Klonens vom übergeordneten Prozess kopiert, wenn ein Prozess einen anderen mit einem Klonsystemaufruf erzeugt. Das heißt, wenn wir diese Maske für unseren Init-Prozess ändern (in meinem Fall ist es systemd), wird dieser neue Prozess beim Starten eines neuen Prozesses über systemd bereits mit einer neuen Maske gestartet.

Sie können die Maske für den Prozess mit demselben Befehl ändern und die Anzahl der CPU-Kerne auflisten, die für den Prozess verwendet werden sollen. Angenommen, wir möchten den Kernel 0.2.3 für unseren Prozess belassen und Kernel 1 für unseren systemd-Prozess deaktivieren. Dazu müssen wir den folgenden Befehl ausführen:

[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

Wir überprüfen:

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

Die Maske wurde in der hexadezimalen Notation in "D" geändert, dh 13 in Dezimal und 1101 in Binär (8 + 4 + 0 + 1).

Von nun an hat jeder Prozess, der vom systemd-Prozess geklont wird, automatisch eine Maske 1101 der CPU-Auslastung, was bedeutet, dass Kernel Nummer 1 nicht verwendet wird.

Wir verbieten die Verwendung des Kernels für alle Prozesse


Das Verhindern, dass der Linux-Hauptprozess einen einzelnen Kernel verwendet, wirkt sich nur auf neue Prozesse aus, die durch diesen Prozess erstellt wurden. Aber in meinem System gibt es bereits nicht einen Prozess, sondern eine ganze Menge, wie z. B. crond, sshd, bash und andere. Wenn ich verhindern möchte, dass alle Prozesse einen Kern verwenden, muss ich den Task-Set-Befehl für jeden ausgeführten Prozess ausführen.

Um eine Liste aller Prozesse zu erhalten, verwenden wir die vom Kernel bereitgestellte API, nämlich das Dateisystem / proc.

Weiter in der Schleife sehen wir uns die PID jedes laufenden Prozesses an und ändern die Maske für ihn und alle 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
...

Da einige Prozesse während der Ausführung des Programms Zeit haben könnten, andere Prozesse zu erzeugen, ist es besser, diesen Befehl mehrmals auszuführen.

Überprüfen Sie das Ergebnis unserer Arbeit mit dem Befehl 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

Wie Sie sehen können, hat sich das Bild ein wenig geändert. Jetzt sind für den Kernel 0.2.3 die durchschnittlichen Parameter us, sy, id für uns gleich, und für Kernel 1 beträgt unser Kernverbrauch in Userspace und sys 0, und der Kernel ist zu 100% im Leerlauf (Leerlauf 100) ) Kernel 1 wird von unseren Anwendungen nicht mehr verwendet, und ein sehr kleiner Prozentsatz wird derzeit vom Kernel verwendet.

Jetzt ist das Testen der Leistung darauf beschränkt, unseren Prozess auf einem freien Kern zu starten.

Erinnerung


Der einem Prozess zugewiesene physische Speicher kann problemlos aus jedem Prozess entnommen werden. Dieser Mechanismus wird als Swap bezeichnet. Wenn Linux einen Platz zum Tauschen hat, wird es es trotzdem tun. Die einzige Möglichkeit, das Betriebssystem daran zu hindern, wie bei jedem anderen Prozess Speicher aus unserem Prozess zu entnehmen, besteht darin, den Swap-Abschnitt vollständig zu deaktivieren.

[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

Wir haben 1 Prozessorkern zugewiesen, der nicht verwendet wird, und die Möglichkeit zum Austauschen von Speicher aus dem Linux-Kernel entfernt.

Platte


Um die Auswirkungen der Festplatte auf den Start unseres Prozesses zu verringern, erstellen Sie eine Festplatte im Speicher und kopieren Sie alle erforderlichen Dateien auf diese Festplatte.

Erstellen Sie ein Verzeichnis und hängen Sie das Dateisystem ein:

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

Jetzt müssen wir herausfinden, was und wie wir es starten wollen. Um unser Programm auszuführen, müssen wir zuerst unseren Code kompilieren:

[user@localhost]$ javac OddEvenViaMod.java

Dann müssen Sie es ausführen:

[user@localhost]$ java OddEvenViaMod

In unserem Fall möchten wir den Prozess jedoch auf dem Prozessorkern ausführen, der von keinem anderen Prozess verwendet wird. Führen Sie es daher über das Task-Set aus:

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

In unseren Tests müssen wir die Zeit messen, damit aus unserer Startlinie wird

taskset -c 1 time java OddEvenViaMod

Das Linux-Betriebssystem unterstützt verschiedene Formate ausführbarer Dateien, von denen das ELF-Format am häufigsten verwendet wird. Mit diesem Dateiformat können Sie das Betriebssystem anweisen, Ihre Datei nicht auszuführen, sondern eine andere Datei auszuführen. Auf den ersten Blick klingt es nicht sehr logisch und verständlich. Stellen Sie sich vor, ich starte das Minesweeper-Spiel und das Mario-Spiel startet für mich - es sieht aus wie ein Virus. Aber das ist die Logik. Wenn für mein Programm eine dynamische Bibliothek erforderlich ist, z. B. libc oder eine andere, bedeutet dies, dass das Betriebssystem diese Bibliothek zuerst in den Speicher laden und anschließend mein Programm laden und ausführen muss. Und es scheint logisch, solche Funktionen im Betriebssystem selbst zu platzieren, aber das Betriebssystem arbeitet in einem geschützten Speicherbereich und sollte so wenig Funktionen wie möglich und notwendig enthalten.Daher bietet das ELF-Format die Möglichkeit, dem Betriebssystem mitzuteilen, dass wir ein anderes Programm herunterladen möchten, und dieses "andere" Programm lädt alle erforderlichen Bibliotheken und unser Programm herunter und startet das Ganze.

Wir müssen also 3 Dateien ausführen, dies ist Task-Set, Zeit, Java.

Überprüfen Sie die erste von ihnen:

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

Bash führt die Datei / usr / bin / Task-Set aus und überprüft, was sich darin befindet:

[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

Dies ist die ELF-Datei, über die ich oben geschrieben habe. In der ELF-Datei gibt es neben dem Programm selbst verschiedene Header. Durch das Starten dieser Datei überprüft das Betriebssystem seine Header. Wenn der Header "Requesting Program Interpreter" in der Datei vorhanden ist, startet das Betriebssystem die Datei über diesen Header und übergibt die ursprünglich gestartete Datei als Argument.

Überprüfen Sie, ob dieser Header in unserer ELF-Datei vorhanden ist:

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

Der Header ist vorhanden, was bedeutet, dass wir durch Starten der Datei / usr / bin / Task-Set tatsächlich /lib64/ld-linux-x86-64.so.2 ausführen.

Überprüfen Sie, was diese Datei ist:

[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

Dies ist ein Sim-Link zur Datei /lib64/ld-2.17.so. Hör zu:

[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

Wie Sie sehen können, ist dies eine weitere ELF-Datei, die vom Betriebssystem ausgeführt wird. Wir schauen uns die Überschriften an:

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

Wir sehen, dass diese ELF-Datei keinen solchen Header hat, daher wird das Betriebssystem diese Datei ausführen und die Kontrolle darauf übertragen. Und schon öffnet diese Datei unsere Datei / usr / bin / Task-Set und liest von dort Informationen zu allen notwendigen Bibliotheken. Die Liste der erforderlichen Bibliotheken befindet sich auch in den Headern der ELF-Datei. Wir können diese Liste mit dem Befehl ldd oder readelf betrachten, was dasselbe ist:

[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 ist ein verknüpfter Speicher, der nicht mit Bibliotheken verknüpft ist. Daher fehlt er in der ELF-Datei als erforderliche Bibliothek.

Dies macht deutlich, dass das Programm /lib64/ld-2.17.so für die Ausführung aller Programme verantwortlich ist, die dies erfordern, und dies sind alles Programme mit dynamisch verknüpften Bibliotheken.

Wenn wir / usr / bin / Task-Set ausführen, ist dies genau das gleiche wie wir /lib64/ld-2.17.so mit dem Argument / usr / bin / Task-Set ausführen.

Wir kehren zum Problem des Einflusses der Festplatte auf unsere Tests zurück. Jetzt wissen wir, dass wir, wenn wir unser Programm aus dem Speicher laden möchten, nicht eine Datei, sondern mehrere kopieren müssen:

[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/

Wir machen das gleiche für die Zeit, deren Bibliotheksanforderungen genau gleich sind (wir haben bereits ld und libc kopiert).

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

Für Java sind die Dinge etwas komplizierter, da Java viele verschiedene Bibliotheken benötigt, die für eine lange Zeit kopiert werden können. Um mein Leben ein wenig zu vereinfachen, kopiere ich das gesamte Verzeichnis von meinem Java OpenJDK auf eine Festplatte im Speicher und erstelle einen Sim-Link. Natürlich bleiben in diesem Fall Festplattenzugriffe erhalten, aber es wird weniger geben.

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

Benennen Sie das alte Verzeichnis um und fügen Sie die Endung .default hinzu

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

Und erstellen Sie einen Symlink:

[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/

Wir wissen bereits, wie eine Binärdatei über das Argument zur Datei /lib64/ld-2.17.so ausgeführt wird, die tatsächlich gestartet wird. Aber wie kann das Programm /lib64/ld-2.17.so geladene Bibliotheken aus dem von uns angegebenen Verzeichnis laden? man ld hilft uns, woraus wir erfahren, dass das ld-Programm die Bibliotheken aus den von uns angegebenen Verzeichnissen lädt, wenn Sie die Umgebungsvariable LD_LIBRARY_PATH deklarieren. Jetzt haben wir alle Daten, um die Startlinie der Java-Anwendung vorzubereiten.

Wir beginnen mehrmals hintereinander und prüfen:

[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]$

Während der Ausführung des Programms können wir top ausführen und sicherstellen, dass das Programm auf dem richtigen CPU-Kern ausgeführt wird.

[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
...

Wie Sie sehen können, sind die Ergebnisse in den meisten Fällen ähnlich. Leider können wir den Einfluss des Betriebssystems auf den CPU-Kern nicht vollständig beseitigen, sodass das Ergebnis immer noch von den spezifischen Aufgaben im Linux-Kernel zum Zeitpunkt des Starts abhängt. Daher ist es besser, den Median der Werte mehrerer Starts zu verwenden.

In unserem Fall sehen wir, dass das Java-Programm 9.000.000.000 mit Parität durch den Rest der Division in 10,65 Sekunden auf einem CPU-Kern verarbeitet.

Machen wir den gleichen Test mit unserem zweiten Programm, das dasselbe durch binäres UND macht.

[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

Jetzt können wir mit Zuversicht sagen, dass der Vergleich für die Parität durch binäres UND 4,02 Sekunden dauert, was bedeutet, dass er im Vergleich zum Überprüfen des Restes der Division zumindest bei openjdk Version 1.8.0 2,6-mal schneller funktioniert.

Oracle Java gegen Openjdk


Ich habe Java JDK von der Oracle-Website in das Verzeichnis /mnt/ramdisk/jdk-13.0.2 heruntergeladen und entpackt.

Kompilieren:

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

Wir starten:

[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

Wir kompilieren das zweite Programm:

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

Wir starten:

[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

Die Ausführungszeit derselben Quellen in oracle jdk ist für den Rest der Division und das binäre AND gleich, was normal aussieht, aber diese Zeit ist ebenso schlecht, was in openjdk für den Rest der Division gezeigt wurde.

Python


Versuchen wir, dasselbe in Python zu vergleichen. Erstens die Option mit dem Rest der Division durch 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

Wir starten:

[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

Nun das gleiche mit binärem UND:

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

Wir starten:

[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

Die Ergebnisse zeigen, dass AND schneller ist.

Im Internet wurde oft geschrieben, dass globale Variablen in Python langsamer sind. Ich habe beschlossen, die Ausführungszeit des letzten Programms mit AND zu vergleichen und genau dieselbe, aber in eine Funktion eingeschlossen:

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()

Führen Sie die folgende Funktion aus:

[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

Wie Sie sehen können, verarbeitet der gleiche Paritätsvergleich in Python über binäres UND in einer Funktion 100000000 Zahlen auf einem einzelnen CPU-Kern in ~ 5 Sekunden, der gleiche Vergleich über UND ohne Funktion dauert ~ 10 Sekunden und der Vergleich ohne Funktion durch den Rest der Division dauert ~ 11 Sekunden

Warum ein Python-Programm in einer Funktion schneller als ohne funktioniert, wurde bereits mehrmals beschrieben und hängt mit dem Umfang der Variablen zusammen.

Python hat die Möglichkeit, ein Programm in interne Funktionen zu zerlegen, die Python bei der Interpretation eines Programms verwendet. Mal sehen, welche Funktionen Python für die Variante mit der Funktion odd_and_func.py verwendet:

[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

Und überprüfen Sie dasselbe, ohne die Funktion in unserem Code zu verwenden:

>>> 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

Wie Sie sehen können, verwendet Python in der Variante mit der deklarierten Funktion interne Funktionen mit dem FAST-Postfix, z. B. STORE_FAST, LOAD_FAST, und in der Variante ohne Deklaration der Funktion verwendet Python die internen Funktionen STORE_NAME und LOAD_NAME.

Dieser Artikel hat wenig praktische Bedeutung und zielt eher darauf ab, einige der Funktionen von Linux und Compilern zu verstehen.

Gut zu allen!

All Articles