ตัวอย่าง POSIX C ที่รันได้น้อยที่สุด
เพื่อทำให้สิ่งต่าง ๆ เป็นรูปธรรมมากขึ้นฉันต้องการยกตัวอย่างกรณีที่รุนแรงที่สุดtime
ด้วยโปรแกรมทดสอบ C ขั้นต่ำ
โปรแกรมทั้งหมดสามารถรวบรวมและรันด้วย:
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 CPU (4 คอร์ / 8 เธรด), 2x Samsung M471A2K43BB1-CRC RAM (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
สามารถช่วยคุณแยกแยะความแตกต่างระหว่าง CPU และโปรแกรมที่เชื่อมโยงกับ IO: คำว่า "CPU bound" และ "I / O bound" หมายถึงอะไร?
หลายกระทู้
ตัวอย่างต่อไปนี้ทำการniters
วนซ้ำของ CPU-bound ที่ไม่ได้ใช้งานบน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 รหัสพล็อตต้นน้ำ
จากนั้นเราพล็อตวอลล์ผู้ใช้และ sys เป็นฟังก์ชันของจำนวนเธรดสำหรับการทำซ้ำ 10 ^ 10 บนซีพียูไฮเปอร์เธรด 8 ของฉัน:
ข้อมูลที่พล็อต
จากกราฟเราจะเห็นว่า:
สำหรับแอพพลิเคชั่นที่ใช้ซีพียูแบบ Single Core แบบ Wall และผู้ใช้ก็เหมือนกัน
สำหรับ 2 คอร์ผู้ใช้อยู่ที่ 2x wall ซึ่งหมายความว่าเวลาของผู้ใช้จะถูกนับในทุกเธรด
ผู้ใช้เพิ่มเป็นสองเท่าโดยทั่วไปและในขณะที่กำแพงยังคงเหมือนเดิม
สิ่งนี้ดำเนินต่อไปถึง 8 เธรดซึ่งตรงกับจำนวนไฮเปอร์เธรดในคอมพิวเตอร์ของฉัน
หลังจาก 8 กำแพงก็เริ่มเพิ่มขึ้นเช่นกันเพราะเราไม่มีซีพียูพิเศษใด ๆ ที่จะเพิ่มการทำงานในเวลาที่กำหนด!
อัตราส่วนจะมีอยู่ ณ จุดนี้
โปรดทราบว่ากราฟนี้เป็นเพียงเพื่อให้ง่ายและชัดเจนเพราะการทำงานเป็นอย่างหมดจด CPU-bound: ถ้ามันถูกหน่วยความจำที่ถูกผูกไว้แล้วเราจะได้รับการตกอยู่ในการปฏิบัติงานมากก่อนหน้านี้มีแกนน้อยลงเพราะการเข้าถึงหน่วยความจำจะเป็นคอขวดตามที่แสดงอะไร คำว่า "CPU bound" และ "I / O bound" หมายถึงอะไร
Sys ทำงานหนักกับ sendfile
ภาระงานที่หนักที่สุดที่ฉันจะได้รับคือการใช้งานsendfile
ซึ่งจะทำการคัดลอกไฟล์ในพื้นที่เคอร์เนล:คัดลอกไฟล์ด้วยวิธีที่ปลอดภัยและมีประสิทธิภาพ
ดังนั้นฉันจึงจินตนาการว่า in-kernel memcpy
นี้จะเป็นการทำงานที่ใช้ CPU มาก
ก่อนอื่นฉันเริ่มต้นไฟล์สุ่มขนาดใหญ่ 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
จะแยกแยะระหว่าง syscalls ของกระบวนการที่แตกต่างกันหรือไม่ดังนั้นฉันจึงลอง:
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 มีค่าเท่ากันสำหรับทั้งกระบวนการเดียว แต่เวลาผนังใหญ่ขึ้นเนื่องจากกระบวนการแข่งขันกันเพื่อเข้าถึงการอ่านดิสก์
ดังนั้นดูเหมือนว่าในความเป็นจริงบัญชีที่กระบวนการเริ่มต้นทำงานเคอร์เนลที่กำหนด
รหัสแหล่งทุบตี
เมื่อคุณทำtime <cmd>
บน Ubuntu เพียงใช้คำหลัก Bash ดังที่เห็นได้จาก:
type time
ผลลัพธ์ใด:
time is a shell keyword
ดังนั้นเราจึง grep แหล่งที่มาในรหัสที่มา Bash 4.19 สำหรับสตริงออก:
git grep '"user\b'
ซึ่งทำให้เรามีฟังก์ชันexecute_cmd.ctime_command
ซึ่งใช้:
gettimeofday()
และgetrusage()
ถ้าทั้งสองมีอยู่
times()
มิฉะนั้น
ซึ่งทั้งหมดเป็นลินุกซ์สายระบบและฟังก์ชั่น POSIX
Coreutils ซอร์สโค้ด GNU
ถ้าเราเรียกมันว่า:
/usr/bin/time
จากนั้นจะใช้การดำเนินการ GNU Coreutils
อันนี้ซับซ้อนกว่าเล็กน้อย แต่แหล่งข้อมูลที่เกี่ยวข้องดูเหมือนจะเป็นresuse.cและมันจะ:
- การเรียกที่ไม่ใช่ POSIX BSD
wait3
หากมีให้
times
และgettimeofday
อื่น ๆ