最小限の実行可能な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
。
たとえば、1秒間スリープするプログラム:
#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アップストリーム。
そして、約1秒待つと、スリープの例と同じように出力されます。
real 0m1.003s
user 0m0.001s
sys 0m0.003s
このためtime
、CPUとIOバウンドプログラムを区別するのに役立ちます。「CPUバウンド」と「I / Oバウンド」という用語はどういう意味ですか?
複数のスレッド
次の例はniters
、nthreads
スレッド上で純粋にCPUにバインドされた無用な作業を繰り返し実行します。
#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を、8つのハイパースレッドCPUで固定された10 ^ 10反復のスレッド数の関数としてプロットします。
データをプロットします。
グラフから、次のことがわかります。
CPU集中型のシングルコアアプリケーションの場合、壁とユーザーはほぼ同じです
2コアの場合、ユーザーは約2倍の壁です。つまり、ユーザー時間はすべてのスレッドでカウントされます。
ユーザーは基本的に2倍になり、壁は同じままでした。
これは最大8スレッドまで続き、これは私のコンピューターのハイパースレッドの数と一致します。
8時間後、一定の時間内により多くの作業を行うための追加のCPUがないため、壁も増加し始めます!
この時点で比率は安定しています。
それはメモリがバインドされた場合に示すように、メモリアクセスがボトルネックになるので、その後、私たちはずっと以前より少ないコアでパフォーマンスの低下を得るでしょう:仕事は純粋にCPUバウンドであるため、このグラフだけそれほど明確でシンプルであることに注意してくださいどのような「CPUバウンド」および「I / Oバウンド」という用語はどういう意味ですか?
Sys重い作業 sendfile
私が思いつくことができる最も重いsysワークロードsendfile
は、カーネルスペースでファイルコピー操作を実行するを使用することでした:正常で安全かつ効率的な方法でファイルをコピーします
そのため、このカーネル内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
、異なるプロセスのシステムコールを区別できるかどうかも知りたくて、試しました。
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ソースコードでソースをgrepします。
git grep '"user\b'
これは、execute_cmd.c functionにつながりtime_command
ます。
gettimeofday()
そして、getrusage()
の両方が利用可能な場合
times()
さもないと
これらはすべてLinuxシステムコールとPOSIX関数です。
GNU Coreutilsソースコード
それを次のように呼ぶと、
/usr/bin/time
次に、GNU Coreutils実装を使用します。
これはもう少し複雑ですが、関連するソースはresuse.cにあるようで、次のようになります。
- 非POSIX BSD
wait3
呼び出しが利用可能な場合
times
とgettimeofday
そうでない場合は