Minimalne uruchamialne przykłady POSIX C.
Żeby było bardziej konkretnie, chcę zilustrować kilka ekstremalnych przypadków time
z minimalnymi programami testowymi C.
Wszystkie programy można kompilować i uruchamiać za pomocą:
gcc -ggdb3 -o main.out -pthread -std=c99 -pedantic-errors -Wall -Wextra main.c
time ./main.out
i zostały przetestowane w Ubuntu 18.10, GCC 8.2.0, glibc 2.28, jądro Linux 4.18, laptop ThinkPad P51, procesor Intel Core i7-7820HQ (4 rdzenie / 8 wątków), 2x RAM Samsung M471A2K43BB1-CRC (2x 16GiB).
sen
Non-zajęty sen nie liczyć na jeden user
lub sys
tylko real
.
Na przykład program, który śpi przez sekundę:
#define _XOPEN_SOURCE 700
#include <stdlib.h>
#include <unistd.h>
int main(void) {
sleep(1);
return EXIT_SUCCESS;
}
GitHub w górę .
wyprowadza coś takiego:
real 0m1.003s
user 0m0.001s
sys 0m0.003s
To samo dotyczy programów zablokowanych przy IO, które stają się dostępne.
Na przykład następujący program czeka na wprowadzenie znaku przez użytkownika i naciśnięcie enter:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
printf("%c\n", getchar());
return EXIT_SUCCESS;
}
GitHub w górę .
A jeśli zaczekasz około jednej sekundy, wyświetli się tak jak w przykładzie uśpienia, na przykład:
real 0m1.003s
user 0m0.001s
sys 0m0.003s
Z tego powodu time
może pomóc ci rozróżnić programy związane z procesorem i operacjami we / wy : Co oznaczają pojęcia „związane z procesorem” i „związane z operacjami we / wy”?
Wiele wątków
Poniższy przykład wykonuje niters
iteracje bezużytecznych prac związanych wyłącznie z procesorem w nthreads
wątkach:
#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 w górę + kod fabuły .
Następnie wykreślamy ścianę, użytkownika i system jako funkcję liczby wątków dla stałej iteracji 10 ^ 10 na moim 8 procesorze hyperthread:
Wykres danych .
Z wykresu widzimy, że:
w przypadku aplikacji jednordzeniowych intensywnie wykorzystujących procesor, ściana i użytkownik są mniej więcej takie same
dla 2 rdzeni użytkownik ma około 2x ściany, co oznacza, że czas użytkownika jest liczony we wszystkich wątkach.
użytkownik w zasadzie podwoił się, a ściana pozostała bez zmian.
kontynuuje to do 8 wątków, co odpowiada mojej liczbie hiperwątków na moim komputerze.
Po 8 ściana zaczyna się również zwiększać, ponieważ nie mamy żadnych dodatkowych procesorów, aby włożyć więcej pracy w danym czasie!
Stosunki plateau w tym punkcie.
Zauważ, że ten wykres jest tylko tak przejrzysty i prosty, ponieważ praca jest ściśle związana z procesorem: gdyby była związana z pamięcią, osiągnęlibyśmy spadek wydajności znacznie wcześniej z mniejszą liczbą rdzeni, ponieważ dostęp do pamięci byłby wąskim gardłem, jak pokazano w What oznaczają terminy „związany z procesorem” i „związany z I / O”?
Sys ciężka praca z sendfile
Najcięższym obciążeniem systemowym, jakie mogłem wymyślić, było użycie narzędzia sendfile
, które wykonuje operację kopiowania plików w przestrzeni jądra: Kopiuj plik w rozsądny, bezpieczny i wydajny sposób
Więc wyobrażałem sobie, że to jądro memcpy
będzie wymagało dużej mocy obliczeniowej procesora.
Najpierw inicjuję duży losowy plik 10GiB za pomocą:
dd if=/dev/urandom of=sendfile.in.tmp bs=1K count=10M
Następnie uruchom kod:
#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 w górę .
co daje w zasadzie głównie czas systemowy zgodnie z oczekiwaniami:
real 0m2.175s
user 0m0.001s
sys 0m1.476s
Byłem również ciekawy, czy time
rozróżniam systemy różnych procesów, więc spróbowałem:
time ./sendfile.out sendfile.in1.tmp sendfile.out1.tmp &
time ./sendfile.out sendfile.in2.tmp sendfile.out2.tmp &
Rezultatem było:
real 0m3.651s
user 0m0.000s
sys 0m1.516s
real 0m4.948s
user 0m0.000s
sys 0m1.562s
Czas sys jest mniej więcej taki sam dla obu jak dla pojedynczego procesu, ale czas ściany jest dłuższy, ponieważ procesy rywalizują o dostęp do odczytu dysku.
Wygląda więc na to, że faktycznie bierze pod uwagę proces, który rozpoczął dane działanie jądra.
Kod źródłowy Bash
Kiedy robisz to tylko time <cmd>
na Ubuntu, użyj słowa kluczowego Bash, jak widać z:
type time
które wyjścia:
time is a shell keyword
Dlatego grepujemy źródło w kodzie źródłowym Bash 4.19 dla ciągu wyjściowego:
git grep '"user\b'
co prowadzi nas do funkcji execute_cmd.ctime_command
, która wykorzystuje:
gettimeofday()
i getrusage()
jeśli oba są dostępne
times()
Inaczej
wszystkie to wywołania systemowe Linux i funkcje POSIX .
Kod źródłowy GNU Coreutils
Jeśli nazwiemy to:
/usr/bin/time
następnie wykorzystuje implementację GNU Coreutils.
Ten jest nieco bardziej złożony, ale wydaje się, że odpowiednim źródłem jest resuse.c i robi:
wait3
wywołanie BSD inne niż POSIX, jeśli jest dostępne
times
i gettimeofday
inaczej