Минимальные исполняемые примеры POSIX C
Чтобы сделать вещи более конкретными, я хочу привести в пример несколько крайних случаев time
с некоторыми минимальными программами тестирования на Си.
Все программы могут быть скомпилированы и запущены с:
gcc -ggdb3 -o main.out -pthread -std=c99 -pedantic-errors -Wall -Wextra main.c
time ./main.out
и были протестированы в Ubuntu 18.10, GCC 8.2.0, glibc 2.28, ядре Linux 4.18, ноутбуке ThinkPad P51, процессоре Intel Core i7-7820HQ (4 ядра / 8 потоков), 2x оперативной памяти Samsung M471A2K43BB1-CRC (2x 16 ГБ).
спать
Незанятого сон не учитывается ни в одном user
или sys
только real
.
Например, программа, которая спит на секунду:
#define _XOPEN_SOURCE 700
#include <stdlib.h>
#include <unistd.h>
int main(void) {
sleep(1);
return EXIT_SUCCESS;
}
GitHub вверх по течению .
выводит что-то вроде:
real 0m1.003s
user 0m0.001s
sys 0m0.003s
То же самое относится и к программам, заблокированным на IO.
Например, следующая программа ждет, когда пользователь введет символ и нажмет ввод:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
printf("%c\n", getchar());
return EXIT_SUCCESS;
}
GitHub вверх по течению .
И если вы подождете около одной секунды, он выдаст как пример сна что-то вроде:
real 0m1.003s
user 0m0.001s
sys 0m0.003s
По этой причине time
может помочь вам различать программы, связанные с процессором и вводом- выводом: что означают термины «привязка к процессору» и «привязка ввода-вывода»?
Несколько потоков
В следующем примере выполняются niters
итерации бесполезной, чисто связанной с ЦП, работы с nthreads
потоками:
#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 + сюжетный код .
Затем мы рисуем wall, user и sys как функцию от числа потоков для фиксированных 10 ^ 10 итераций на моем 8-процессорном процессоре с гиперпотоками:
Сюжет данных .
Из графика мы видим, что:
для одноядерного приложения, интенсивно использующего процессор, стена и пользователь примерно одинаковы
для 2 ядер пользователь примерно в 2 раза больше стены, что означает, что пользовательское время учитывается во всех потоках.
Пользователь в основном удвоился, и при этом стена осталась прежней.
это продолжается до 8 потоков, что соответствует моему числу гиперпотоков на моем компьютере.
После 8 стена также начинает увеличиваться, потому что у нас нет лишних процессоров, чтобы выполнять больше работы за данный промежуток времени!
Соотношение плато на данный момент.
Обратите внимание, что этот график только настолько ясен и прост, потому что работа связана только с процессором: если бы он был связан с памятью, то мы бы получили падение производительности намного раньше с меньшим количеством ядер, потому что доступ к памяти был бы узким местом, как показано в разделе Что означают термины «привязка к процессору» и «привязка ввода / вывода»?
Sys тяжелая работа с sendfile
Самая тяжелая рабочая нагрузка sys, которую я мог придумать, состояла в том sendfile
, чтобы использовать функцию, которая выполняет операцию копирования файла в пространстве ядра: копирование файла разумным, безопасным и эффективным способом.
Таким образом, я представлял, что это ядро memcpy
будет загружать процессор.
Сначала я инициализирую большой 10GiB случайный файл:
dd if=/dev/urandom of=sendfile.in.tmp bs=1K count=10M
Затем запустите код:
#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 вверх по течению .
что дает в основном системное время, как и ожидалось:
real 0m2.175s
user 0m0.001s
sys 0m1.476s
Мне также было любопытно посмотреть, time
будут ли различаться системные вызовы разных процессов, поэтому я попытался:
time ./sendfile.out sendfile.in1.tmp sendfile.out1.tmp &
time ./sendfile.out sendfile.in2.tmp sendfile.out2.tmp &
И результат был:
real 0m3.651s
user 0m0.000s
sys 0m1.516s
real 0m4.948s
user 0m0.000s
sys 0m1.562s
Время системы примерно одинаково для обоих процессов, но время задержки больше, поскольку процессы конкурируют за доступ к чтению с диска.
Таким образом, кажется, что он действительно учитывает, какой процесс начал работу с ядром.
Исходный код Bash
Когда вы используете только time <cmd>
Ubuntu, он использует ключевое слово Bash, как видно из:
type time
какие выводы:
time is a shell keyword
Итак, мы ищем исходный код в исходном коде Bash 4.19 для выходной строки:
git grep '"user\b'
что приводит нас к функции execute_cmd.ctime_command
, которая использует:
gettimeofday()
и getrusage()
если оба доступны
times()
в противном случае
все это системные вызовы Linux и функции POSIX .
Исходный код GNU Coreutils
Если мы называем это как:
/usr/bin/time
затем он использует реализацию GNU Coreutils.
Это немного сложнее, но соответствующий источник, кажется, находится в resuse.c, и он делает:
wait3
вызов не POSIX BSD, если он доступен
times
и gettimeofday
иначе