Exemplos mínimos de POSIX C executáveis
Para tornar as coisas mais concretas, quero exemplificar alguns casos extremos de time
alguns programas de teste C mínimos.
Todos os programas podem ser compilados e executados com:
gcc -ggdb3 -o main.out -pthread -std=c99 -pedantic-errors -Wall -Wextra main.c
time ./main.out
e foram testados no Ubuntu 18.10, GCC 8.2.0, glibc 2.28, Linux kernel 4.18, laptop ThinkPad P51, CPU Intel Core i7-7820HQ (4 núcleos / 8 threads), 2x Samsung M471A2K43BB1-CRC RAM (2x 16GiB).
dormir
O sono não ocupado não conta apenas em um user
ou em sys
apenas um real
.
Por exemplo, um programa que dorme por um segundo:
#define _XOPEN_SOURCE 700
#include <stdlib.h>
#include <unistd.h>
int main(void) {
sleep(1);
return EXIT_SUCCESS;
}
GitHub upstream .
produz algo como:
real 0m1.003s
user 0m0.001s
sys 0m0.003s
O mesmo vale para os programas bloqueados quando o IO está disponível.
Por exemplo, o seguinte programa aguarda o usuário digitar um caractere e pressione enter:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
printf("%c\n", getchar());
return EXIT_SUCCESS;
}
GitHub upstream .
E se você esperar cerca de um segundo, o resultado será igual ao exemplo de suspensão, algo como:
real 0m1.003s
user 0m0.001s
sys 0m0.003s
Por esse motivo, time
pode ajudá-lo a distinguir entre programas vinculados à CPU e E / S: O que significam os termos "vinculado à CPU" e "vinculado à E / S"?
Vários segmentos
O exemplo a seguir faz niters
iterações de trabalho inútil puramente vinculado à CPU em 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 upstream + código de plotagem .
Em seguida, plotamos wall, user e sys em função do número de threads para 10 iterações fixas de 10 ^ 10 na minha CPU hyperthread de 8:
Plotar dados .
A partir do gráfico, vemos que:
para um aplicativo de núcleo único intensivo de CPU, parede e usuário são praticamente os mesmos
para 2 núcleos, o usuário tem cerca de duas paredes, o que significa que o tempo do usuário é contado em todos os segmentos.
o usuário basicamente dobrou e, enquanto o muro permaneceu o mesmo.
isso continua até 8 threads, o que corresponde ao meu número de hyperthreads no meu computador.
Depois das 8, o muro começa a aumentar também, porque não temos CPUs extras para colocar mais trabalho em um determinado período de tempo!
A razão de platôs neste momento.
Observe que este gráfico é tão claro e simples porque o trabalho é puramente vinculado à CPU: se fosse vinculado à memória, teríamos uma queda no desempenho muito mais cedo com menos núcleos, porque os acessos à memória seriam um gargalo, como mostrado em O que os termos "CPU bound" e "I / O bound" significam?
Sys trabalho pesado com sendfile
A carga de trabalho mais pesada do sistema que eu pude criar foi usar o sendfile
, que executa uma operação de cópia de arquivo no espaço do kernel: Copie um arquivo de maneira sã, segura e eficiente
Então, imaginei que esse kernel memcpy
seria uma operação intensiva da CPU.
Primeiro, inicializo um arquivo aleatório grande de 10 GiB com:
dd if=/dev/urandom of=sendfile.in.tmp bs=1K count=10M
Em seguida, execute o código:
#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 upstream .
que fornece basicamente o tempo do sistema conforme o esperado:
real 0m2.175s
user 0m0.001s
sys 0m1.476s
Eu também estava curioso para ver se time
distinguiria os syscalls de diferentes processos, então tentei:
time ./sendfile.out sendfile.in1.tmp sendfile.out1.tmp &
time ./sendfile.out sendfile.in2.tmp sendfile.out2.tmp &
E o resultado foi:
real 0m3.651s
user 0m0.000s
sys 0m1.516s
real 0m4.948s
user 0m0.000s
sys 0m1.562s
O tempo do sistema é praticamente o mesmo para um único processo, mas o tempo da parede é maior porque os processos estão competindo pelo acesso de leitura do disco.
Portanto, parece que de fato explica qual processo iniciou um determinado trabalho do kernel.
Código-fonte Bash
Quando você faz apenas time <cmd>
no Ubuntu, ele usa a palavra-chave Bash, como pode ser visto em:
type time
quais saídas:
time is a shell keyword
Então nós grep source no código-fonte do Bash 4.19 para a string de saída:
git grep '"user\b'
o que nos leva à função execute_cmd.ctime_command
, que usa:
gettimeofday()
e getrusage()
se ambos estiverem disponíveis
times()
de outra forma
todas chamadas de sistema Linux e funções POSIX .
Código fonte GNU Coreutils
Se o chamarmos como:
/usr/bin/time
então ele usa a implementação GNU Coreutils.
Este é um pouco mais complexo, mas a fonte relevante parece estar em resuse.c e funciona:
- uma
wait3
chamada BSD não POSIX, se disponível
times
e de gettimeofday
outra forma