Exemples POSIX C exécutables au minimum
Pour rendre les choses plus concrètes, je veux illustrer quelques cas extrêmes de time
quelques programmes de test C minimum.
Tous les programmes peuvent être compilés et exécutés avec:
gcc -ggdb3 -o main.out -pthread -std=c99 -pedantic-errors -Wall -Wextra main.c
time ./main.out
et ont été testés dans Ubuntu 18.10, GCC 8.2.0, glibc 2.28, noyau Linux 4.18, ordinateur portable ThinkPad P51, CPU Intel Core i7-7820HQ (4 cœurs / 8 threads), 2x RAM Samsung M471A2K43BB1-CRC (2x 16GiB).
dormir
Sommeil non occupé ne compte pas dans les deux user
ou sys
seulement real
.
Par exemple, un programme qui dort une seconde:
#define _XOPEN_SOURCE 700
#include <stdlib.h>
#include <unistd.h>
int main(void) {
sleep(1);
return EXIT_SUCCESS;
}
GitHub en amont .
génère quelque chose comme:
real 0m1.003s
user 0m0.001s
sys 0m0.003s
Il en va de même pour les programmes bloqués lorsque les entrées-sorties deviennent disponibles.
Par exemple, le programme suivant attend que l'utilisateur entre un caractère et appuyez sur Entrée:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
printf("%c\n", getchar());
return EXIT_SUCCESS;
}
GitHub en amont .
Et si vous attendez environ une seconde, il produit exactement comme l'exemple de sommeil quelque chose comme:
real 0m1.003s
user 0m0.001s
sys 0m0.003s
Pour cette raison, vous time
pouvez faire la distinction entre les programmes liés au processeur et aux E / S: que signifient les termes "lié au processeur" et "lié aux E / S"?
Plusieurs threads
L'exemple suivant effectue des niters
itérations de travaux inutiles purement liés au processeur sur les nthreads
threads:
#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 en amont + code de tracé .
Ensuite, nous traçons wall, user et sys en fonction du nombre de threads pour une itération fixe de 10 ^ 10 sur mon 8 CPU hyperthread:
Tracer des données .
Du graphique, nous voyons que:
pour une application monocœur gourmande en CPU, le mur et l'utilisateur sont à peu près les mêmes
pour 2 cœurs, l'utilisateur est environ 2x mur, ce qui signifie que le temps de l'utilisateur est compté sur tous les threads.
l'utilisateur a pratiquement doublé, et le mur est resté le même.
cela continue jusqu'à 8 threads, ce qui correspond à mon nombre d'hyperthreads dans mon ordinateur.
Après 8, le mur commence également à augmenter, car nous n'avons pas de CPU supplémentaires pour mettre plus de travail dans un laps de temps donné!
Le ratio plateaux à ce stade.
Notez que ce graphique n'est clair et simple que parce que le travail est purement lié au processeur: s'il était lié à la mémoire, nous obtiendrions une baisse des performances beaucoup plus tôt avec moins de cœurs car les accès à la mémoire seraient un goulot d'étranglement, comme indiqué dans What signifient les termes «lié au processeur» et «lié aux E / S»?
Sys travail lourd avec sendfile
La charge de travail sys la plus lourde que j'ai pu trouver était d'utiliser le sendfile
, qui effectue une opération de copie de fichiers sur l'espace du noyau: Copiez un fichier de manière saine, sûre et efficace
J'ai donc imaginé que ce noyau memcpy
serait une opération gourmande en CPU.
D'abord, j'initialise un grand fichier aléatoire de 10 Go avec:
dd if=/dev/urandom of=sendfile.in.tmp bs=1K count=10M
Exécutez ensuite le code:
#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 en amont .
ce qui donne essentiellement du temps système comme prévu:
real 0m2.175s
user 0m0.001s
sys 0m1.476s
J'étais également curieux de voir si cela time
ferait la distinction entre les appels système de différents processus, alors j'ai essayé:
time ./sendfile.out sendfile.in1.tmp sendfile.out1.tmp &
time ./sendfile.out sendfile.in2.tmp sendfile.out2.tmp &
Et le résultat a été:
real 0m3.651s
user 0m0.000s
sys 0m1.516s
real 0m4.948s
user 0m0.000s
sys 0m1.562s
Le temps sys est à peu près le même pour les deux que pour un seul processus, mais le temps du mur est plus grand car les processus sont probablement en concurrence pour l'accès en lecture sur disque.
Il semble donc que cela explique en fait quel processus a démarré un travail de noyau donné.
Code source Bash
Lorsque vous ne faites que time <cmd>
sur Ubuntu, il utilise le mot-clé Bash comme on peut le voir sur:
type time
qui génère:
time is a shell keyword
Nous avons donc grep source dans le code source Bash 4.19 pour la chaîne de sortie:
git grep '"user\b'
ce qui nous amène à la fonction execute_cmd.ctime_command
, qui utilise:
gettimeofday()
et getrusage()
si les deux sont disponibles
times()
autrement
qui sont tous des appels système Linux et des fonctions POSIX .
Code source de GNU Coreutils
Si nous l'appelons comme:
/usr/bin/time
puis il utilise l'implémentation GNU Coreutils.
Celui-ci est un peu plus complexe, mais la source pertinente semble être à resuse.c et il le fait:
- un
wait3
appel BSD non POSIX s'il est disponible
times
et gettimeofday
sinon