Minimal ausführbare POSIX C-Beispiele
Um die Dinge konkreter zu machen, möchte ich einige extreme Fälle time
mit einigen minimalen C-Testprogrammen veranschaulichen .
Alle Programme können kompiliert und ausgeführt werden mit:
gcc -ggdb3 -o main.out -pthread -std=c99 -pedantic-errors -Wall -Wextra main.c
time ./main.out
und wurden in Ubuntu 18.10, GCC 8.2.0, glibc 2.28, Linux-Kernel 4.18, ThinkPad P51-Laptop, Intel Core i7-7820HQ-CPU (4 Kerne / 8 Threads), 2x Samsung M471A2K43BB1-CRC-RAM (2x 16GiB) getestet.
Schlaf
Nicht beschäftigter Schlaf zählt weder in user
noch sys
nur real
.
Zum Beispiel ein Programm, das eine Sekunde lang schläft:
#define _XOPEN_SOURCE 700
#include <stdlib.h>
#include <unistd.h>
int main(void) {
sleep(1);
return EXIT_SUCCESS;
}
GitHub stromaufwärts .
gibt so etwas aus wie:
real 0m1.003s
user 0m0.001s
sys 0m0.003s
Gleiches gilt für Programme, die auf E / A blockiert sind und verfügbar werden.
Das folgende Programm wartet beispielsweise darauf, dass der Benutzer ein Zeichen eingibt und die Eingabetaste drückt:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
printf("%c\n", getchar());
return EXIT_SUCCESS;
}
GitHub stromaufwärts .
Und wenn Sie ungefähr eine Sekunde warten, wird genau wie im Schlafbeispiel Folgendes ausgegeben:
real 0m1.003s
user 0m0.001s
sys 0m0.003s
Aus diesem Grund time
können Sie zwischen CPU- und E / A-gebundenen Programmen unterscheiden: Was bedeuten die Begriffe "CPU-gebunden" und "E / A-gebunden"?
Mehrere Threads
Im folgenden Beispiel werden niters
Iterationen nutzloser, rein CPU-gebundener Arbeit an nthreads
Threads ausgeführt:
#define _XOPEN_SOURCE 700
#include <assert.h>
#include <inttypes.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
uint64_t niters;
void* my_thread(void *arg) {
uint64_t *argument, i, result;
argument = (uint64_t *)arg;
result = *argument;
for (i = 0; i < niters; ++i) {
result = (result * result) - (3 * result) + 1;
}
*argument = result;
return NULL;
}
int main(int argc, char **argv) {
size_t nthreads;
pthread_t *threads;
uint64_t rc, i, *thread_args;
/* CLI args. */
if (argc > 1) {
niters = strtoll(argv[1], NULL, 0);
} else {
niters = 1000000000;
}
if (argc > 2) {
nthreads = strtoll(argv[2], NULL, 0);
} else {
nthreads = 1;
}
threads = malloc(nthreads * sizeof(*threads));
thread_args = malloc(nthreads * sizeof(*thread_args));
/* Create all threads */
for (i = 0; i < nthreads; ++i) {
thread_args[i] = i;
rc = pthread_create(
&threads[i],
NULL,
my_thread,
(void*)&thread_args[i]
);
assert(rc == 0);
}
/* Wait for all threads to complete */
for (i = 0; i < nthreads; ++i) {
rc = pthread_join(threads[i], NULL);
assert(rc == 0);
printf("%" PRIu64 " %" PRIu64 "\n", i, thread_args[i]);
}
free(threads);
free(thread_args);
return EXIT_SUCCESS;
}
GitHub Upstream + Plotcode .
Dann zeichnen wir wall, user und sys als Funktion der Anzahl der Threads für feste 10 ^ 10 Iterationen auf meiner 8-Hyperthread-CPU:
Plotdaten .
Aus der Grafik sehen wir Folgendes:
Bei einer CPU-intensiven Single-Core-Anwendung sind Wand und Benutzer ungefähr gleich
Bei 2 Kernen beträgt der Benutzer ungefähr 2x Wand, was bedeutet, dass die Benutzerzeit über alle Threads hinweg gezählt wird.
Benutzer im Grunde verdoppelt, und während Wand gleich blieb.
Dies setzt bis zu 8 Threads fort, was meiner Anzahl von Hyperthreads in meinem Computer entspricht.
Nach 8 beginnt auch die Wand zu wachsen, da wir keine zusätzlichen CPUs haben, um in einer bestimmten Zeit mehr Arbeit zu leisten!
Das Verhältnis Plateaus an diesem Punkt.
Beachten Sie, dass dieses Diagramm nur so klar und einfach ist, weil die Arbeit rein CPU-gebunden ist: Wenn es speichergebunden wäre, würden wir mit weniger Kernen viel früher einen Leistungsabfall erzielen, da die Speicherzugriffe einen Engpass darstellen würden, wie unter Was gezeigt bedeuten die Begriffe "CPU-gebunden" und "E / A-gebunden"?
Sys schwere Arbeit mit sendfile
Die schwerste Systemauslastung, die ich finden konnte, war die Verwendung von sendfile
, die einen Dateikopiervorgang im Kernelbereich ausführt : Kopieren Sie eine Datei auf eine vernünftige, sichere und effiziente Weise
Also stellte ich mir vor, dass dieser In-Kernel memcpy
eine CPU-intensive Operation sein wird.
Zuerst initialisiere ich eine große 10GiB-Zufallsdatei mit:
dd if=/dev/urandom of=sendfile.in.tmp bs=1K count=10M
Führen Sie dann den Code aus:
#define _GNU_SOURCE
#include <assert.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv) {
char *source_path, *dest_path;
int source, dest;
struct stat stat_source;
if (argc > 1) {
source_path = argv[1];
} else {
source_path = "sendfile.in.tmp";
}
if (argc > 2) {
dest_path = argv[2];
} else {
dest_path = "sendfile.out.tmp";
}
source = open(source_path, O_RDONLY);
assert(source != -1);
dest = open(dest_path, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
assert(dest != -1);
assert(fstat(source, &stat_source) != -1);
assert(sendfile(dest, source, 0, stat_source.st_size) != -1);
assert(close(source) != -1);
assert(close(dest) != -1);
return EXIT_SUCCESS;
}
GitHub stromaufwärts .
was im Grunde meistens Systemzeit wie erwartet gibt:
real 0m2.175s
user 0m0.001s
sys 0m1.476s
Ich war auch neugierig, ob ich time
zwischen Systemaufrufen verschiedener Prozesse unterscheiden würde, also versuchte ich:
time ./sendfile.out sendfile.in1.tmp sendfile.out1.tmp &
time ./sendfile.out sendfile.in2.tmp sendfile.out2.tmp &
Und das Ergebnis war:
real 0m3.651s
user 0m0.000s
sys 0m1.516s
real 0m4.948s
user 0m0.000s
sys 0m1.562s
Die Systemzeit ist für beide ungefähr gleich wie für einen einzelnen Prozess, aber die Wandzeit ist größer, da die Prozesse wahrscheinlich um den Lesezugriff auf die Festplatte konkurrieren.
Es scheint also tatsächlich zu berücksichtigen, welcher Prozess eine bestimmte Kernelarbeit gestartet hat.
Bash-Quellcode
Wenn Sie nur time <cmd>
unter Ubuntu arbeiten, verwenden Sie das Schlüsselwort Bash, wie aus folgendem ersichtlich ist:
type time
welche Ausgänge:
time is a shell keyword
Also grep wir die Quelle im Bash 4.19-Quellcode für die Ausgabezeichenfolge:
git grep '"user\b'
Dies führt uns zur Funktion execute_cmd.ctime_command
, die Folgendes verwendet:
gettimeofday()
und getrusage()
wenn beide verfügbar sind
times()
Andernfalls
All dies sind Linux-Systemaufrufe und POSIX-Funktionen .
GNU Coreutils Quellcode
Wenn wir es nennen als:
/usr/bin/time
Dann wird die Implementierung von GNU Coreutils verwendet.
Dieser ist etwas komplexer, aber die relevante Quelle scheint bei resuse.c zu sein und es tut:
- ein Nicht-POSIX-BSD-
wait3
Aufruf, falls verfügbar
times
und gettimeofday
sonst