Esempi di POSIX C eseguibili minimi
Per rendere le cose più concrete, voglio esemplificare alcuni casi estremi time
con alcuni programmi di test C minimi.
Tutti i programmi possono essere compilati ed eseguiti con:
gcc -ggdb3 -o main.out -pthread -std=c99 -pedantic-errors -Wall -Wextra main.c
time ./main.out
e sono stati testati in Ubuntu 18.10, GCC 8.2.0, glibc 2.28, kernel Linux 4.18, laptop ThinkPad P51, CPU Intel Core i7-7820HQ (4 core / 8 thread), 2x Samsung M471A2K43BB1-CRC RAM (2x 16GiB).
dormire
Il sonno non occupato non viene conteggiato in nessuno dei due user
o sys
, solo real
.
Ad esempio, un programma che dorme per un secondo:
#define _XOPEN_SOURCE 700
#include <stdlib.h>
#include <unistd.h>
int main(void) {
sleep(1);
return EXIT_SUCCESS;
}
GitHub a monte .
produce qualcosa come:
real 0m1.003s
user 0m0.001s
sys 0m0.003s
Lo stesso vale per i programmi bloccati su IO che diventano disponibili.
Ad esempio, il seguente programma attende che l'utente inserisca un carattere e premi Invio:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
printf("%c\n", getchar());
return EXIT_SUCCESS;
}
GitHub a monte .
E se aspetti circa un secondo, viene emesso proprio come nell'esempio sleep qualcosa come:
real 0m1.003s
user 0m0.001s
sys 0m0.003s
Per questo motivo time
può aiutarti a distinguere tra i programmi CPU e IO associati: che cosa significano i termini "CPU bound" e "I / O bound"?
Discussioni multiple
L'esempio seguente esegue niters
iterazioni di inutili lavori legati esclusivamente alla CPU sui nthreads
thread:
#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 + codice trama .
Quindi tracciamo wall, user e sys in funzione del numero di thread per 10 iterazioni fisse sulla mia 8 CPU hyperthread:
Traccia dati .
Dal grafico, vediamo che:
per un'applicazione single core ad uso intensivo di CPU, wall e user sono praticamente uguali
per 2 core, l'utente è circa 2x wall, il che significa che il tempo dell'utente viene conteggiato su tutti i thread.
l'utente ha praticamente raddoppiato, e mentre wall è rimasto lo stesso.
questo continua fino a 8 thread, che corrisponde al mio numero di hyperthread sul mio computer.
Dopo 8, anche il wall inizia ad aumentare, perché non abbiamo CPU extra per dedicare più lavoro in un determinato periodo di tempo!
Il rapporto plateau a questo punto.
Si noti che questo grafico è solo così chiaro e semplice perché il lavoro è puramente legato alla CPU: se fosse legato alla memoria, si otterrebbe un calo delle prestazioni molto prima con meno core perché gli accessi alla memoria sarebbero un collo di bottiglia, come mostrato in Cosa cosa significano i termini "CPU bound" e "I / O bound"?
Sys lavoro pesante con sendfile
Il carico di lavoro di sistema più pesante che potevo inventare era usare il sendfile
, che fa un'operazione di copia di file nello spazio del kernel: copiare un file in modo sano, sicuro ed efficiente
Quindi ho immaginato che questo in-kernel memcpy
sarà un'operazione intensiva per la CPU.
Per prima cosa inizializzo un grande file casuale da 10GiB con:
dd if=/dev/urandom of=sendfile.in.tmp bs=1K count=10M
Quindi eseguire il codice:
#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 a monte .
che dà sostanzialmente principalmente il tempo di sistema come previsto:
real 0m2.175s
user 0m0.001s
sys 0m1.476s
Ero anche curioso di vedere se time
distinguesse tra syscalls di diversi processi, quindi ho provato:
time ./sendfile.out sendfile.in1.tmp sendfile.out1.tmp &
time ./sendfile.out sendfile.in2.tmp sendfile.out2.tmp &
E il risultato è stato:
real 0m3.651s
user 0m0.000s
sys 0m1.516s
real 0m4.948s
user 0m0.000s
sys 0m1.562s
Il tempo di sistema è più o meno lo stesso sia per un singolo processo, ma il tempo di wall è maggiore perché i processi sono in competizione per l'accesso probabile alla lettura del disco.
Quindi sembra che in realtà spieghi quale processo ha avviato un determinato lavoro del kernel.
Codice sorgente di Bash
Quando lo fai solo time <cmd>
su Ubuntu, usa la parola chiave Bash come si può vedere da:
type time
che produce:
time is a shell keyword
Quindi grep source nel codice sorgente di Bash 4.19 per la stringa di output:
git grep '"user\b'
che ci porta alla funzione execute_cmd.ctime_command
, che utilizza:
gettimeofday()
e getrusage()
se entrambi sono disponibili
times()
altrimenti
tutte sono chiamate di sistema Linux e funzioni POSIX .
Codice sorgente GNU Coreutils
Se lo chiamiamo come:
/usr/bin/time
quindi utilizza l'implementazione GNU Coreutils.
Questo è un po 'più complesso, ma la fonte rilevante sembra essere resuse.c e lo fa:
- una
wait3
chiamata BSD non POSIX se disponibile
times
e gettimeofday
altrimenti