Мінімальні приклади, які можна виконувати 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 16GiB).
спати
Незайнятого сон не враховується ні в одному 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, які стають доступними.
Наприклад, наступна програма чекає, коли користувач введе символ і натисне клавішу Enter:
#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 вище за течією + код ділянки .
Тоді ми побудуємо графік wall, user та sys як функція від кількості потоків для фіксованих 10 ^ 10 ітерацій на моєму 8 процесорі гіперпотоків:
Дані графіку .
З графіка ми бачимо, що:
для інтенсивного однопроцесорного додатка, стіни та користувача приблизно однакові
для 2 ядер, користувач становить приблизно 2х стінок, що означає, що час користувача обчислюється в усіх потоках.
Користувач в основному збільшився вдвічі, і поки стіна залишилася колишньою.
це триває до 8 потоків, що відповідає моїй кількості гіперточок у моєму комп’ютері.
Після 8 стіна також починає збільшуватися, тому що у нас немає додаткових процесорів, щоб помістити більше роботи за певний час!
Співвідношення плато в цій точці.
Зауважте, що цей графік настільки чіткий і простий, оскільки робота суто пов'язана з процесором: якби це було пов'язано з пам'яттю, ми б отримали падіння продуктивності набагато раніше з меншими ядрами, оскільки доступ до пам'яті був би вузьким місцем, як показано на Що чи означають терміни "пов'язані з процесором" та "пов'язані введення / виведення"?
Сис важку роботу з sendfile
Найбільш важке завантаження системи, яке я міг придумати, - це використовувати функцію sendfile
, яка виконує операцію копіювання файлу в просторі ядра: Скопіюйте файл надійним, безпечним та ефективним способом
Тож я уявив, що це в ядрі memcpy
буде інтенсивним процесором.
Спочатку я ініціалізую великий випадковий файл 10 Гбіт із:
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
Час sys приблизно однаковий для обох процесів, але час стіни більший, оскільки процеси конкурують за ймовірний доступ до читання дисків.
Тож здається, що насправді це враховує, для якого процесу розпочато роботу ядра.
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, і це:
- не-POSIX BSD
wait3
виклик який не є якщо такий доступний
times
і в gettimeofday
іншому випадку