
strace で C / Go / Rust / Python / Node.js のシステムコールを覗いてみた。
人材育成室 育成メンバーチームで 研修中の はす です。
最近低レイヤーに興味を持ち始め、strace というコマンドを知りました。
なんとこれを使うと、プログラムの内部動作が見えるらしい。ということで、各言語の内部を覗いてみました。
今回はシンプルに比較するため、どの言語も Hello World を出力する処理にします。
strace コマンドについて
strace は System Call Trace の略で、つまりはシステムの呼び出しを追跡するということです。これを使うことで、ファイル操作やネットワーク通信、メモリ管理など、プログラムをトレースして、エラーの原因やプロセスの動きを確認することができます。
環境
| 項目 | バージョン |
|---|---|
| OS | Ubuntu 24.04 |
| C (gcc) | 13.3.0 |
| Go | 1.24.0 |
| Rust | 1.93.0 |
| Python | 3.14.3 |
| Node.js | 24.13.0 |
| strace | 6.8 |
検証
まずは、各言語で hello world を書いていきます。
#include <stdio.h>
int main() {
printf("Hello World\n");
return 0;
}
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello World")
}
fn main() {
println!("Hello World");
}
print("Hello World")
console.log("Hello World");
システムコールの統計を比較
strace に -cオプションをつけることで、各言語のシステムコール数の統計を出すことができます。
strace -c ./bin/hello_c > /dev/null
それでは各言語覗いてみます。
細かいことは後ほど解説するので、ぼんやり眺めてください。
$ strace -c ./bin/hello_c > /dev/null
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
45.38 0.000530 530 1 execve
16.70 0.000195 32 6 mmap
6.51 0.000076 19 4 mprotect
4.88 0.000057 19 3 munmap
4.62 0.000054 27 2 openat
4.37 0.000051 17 3 fstat
3.51 0.000041 13 3 brk
2.91 0.000034 17 2 close
1.97 0.000023 23 1 1 faccessat
1.37 0.000016 16 1 read
1.20 0.000014 14 1 write
1.20 0.000014 14 1 prlimit64
1.11 0.000013 13 1 1 ioctl
1.11 0.000013 13 1 set_robust_list
1.11 0.000013 13 1 getrandom
1.11 0.000013 13 1 rseq
0.94 0.000011 11 1 set_tid_address
------ ----------- ----------- --------- --------- ----------------
100.00 0.001168 35 33 2 total
$ strace -c ./bin/hello_go > /dev/null
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ------------------
38.64 0.001335 7 190 mmap
17.97 0.000621 621 1 execve
15.46 0.000534 3 165 1 rt_sigaction
9.81 0.000339 0 457 rt_sigprocmask
4.25 0.000147 7 20 1 futex
2.20 0.000076 15 5 munmap
2.14 0.000074 12 6 brk
1.51 0.000052 17 3 clone
1.04 0.000036 7 5 mprotect
0.98 0.000034 11 3 openat
0.93 0.000032 2 16 rt_sigreturn
0.69 0.000024 24 1 sysinfo
0.67 0.000023 5 4 read
0.64 0.000022 5 4 close
0.61 0.000021 7 3 lseek
0.49 0.000017 17 1 set_tid_address
0.46 0.000016 16 1 uname
0.29 0.000010 5 2 prlimit64
0.26 0.000009 9 1 1 faccessat
0.26 0.000009 9 1 madvise
0.14 0.000005 5 1 getgid
0.14 0.000005 2 2 getrandom
0.12 0.000004 4 1 getuid
0.12 0.000004 2 2 getegid
0.09 0.000003 3 1 sched_getaffinity
0.09 0.000003 1 2 geteuid
0.00 0.000000 0 6 fcntl
0.00 0.000000 0 1 write
0.00 0.000000 0 2 readv
0.00 0.000000 0 2 fstat
0.00 0.000000 0 2 gettid
------ ----------- ----------- --------- --------- ------------------
100.00 0.003455 3 911 3 total
$ strace -c ./bin/hello_rust > /dev/null
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ------------------
19.28 0.000139 19 7 mprotect
12.07 0.000087 7 11 mmap
9.43 0.000068 13 5 rt_sigaction
7.35 0.000053 13 4 openat
6.66 0.000048 12 4 read
5.96 0.000043 7 6 munmap
5.69 0.000041 13 3 sigaltstack
4.85 0.000035 8 4 close
4.16 0.000030 15 2 prlimit64
3.88 0.000028 9 3 brk
3.47 0.000025 6 4 fstat
2.77 0.000020 20 1 ppoll
2.50 0.000018 18 1 rseq
2.22 0.000016 16 1 write
2.22 0.000016 16 1 set_robust_list
1.94 0.000014 14 1 set_tid_address
1.94 0.000014 14 1 gettid
1.94 0.000014 14 1 getrandom
1.66 0.000012 12 1 sched_getaffinity
0.00 0.000000 0 1 1 faccessat
0.00 0.000000 0 1 execve
------ ----------- ----------- --------- --------- ------------------
100.00 0.000721 11 63 1 total
$ strace -c python3 python/hello.py > /dev/null
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ------------------
16.52 0.000352 12 29 8 openat
15.91 0.000339 14 24 mmap
11.22 0.000239 23 10 mprotect
9.90 0.000211 8 24 read
9.53 0.000203 9 21 close
6.52 0.000139 11 12 munmap
6.48 0.000138 5 26 fstat
6.43 0.000137 2 49 18 newfstatat
3.05 0.000065 6 10 brk
2.72 0.000058 3 17 2 lseek
1.88 0.000040 13 3 getrandom
1.36 0.000029 3 8 3 ioctl
1.17 0.000025 12 2 prlimit64
0.94 0.000020 10 2 getcwd
0.84 0.000018 3 6 4 readlinkat
0.66 0.000014 1 10 getdents64
0.66 0.000014 14 1 futex
0.66 0.000014 14 1 set_robust_list
0.61 0.000013 13 1 set_tid_address
0.61 0.000013 13 1 gettid
0.61 0.000013 13 1 rseq
0.56 0.000012 12 1 sched_getaffinity
0.28 0.000006 6 1 write
0.23 0.000005 1 4 fcntl
0.23 0.000005 0 66 rt_sigaction
0.14 0.000003 3 1 getgid
0.09 0.000002 2 1 getuid
0.09 0.000002 2 1 geteuid
0.09 0.000002 2 1 getegid
0.00 0.000000 0 1 1 faccessat
0.00 0.000000 0 1 execve
------ ----------- ----------- --------- --------- ------------------
100.00 0.002131 6 336 36 total
$ strace -c node node/hello.js > /dev/null
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ------------------
17.95 0.000738 11 64 mmap
9.41 0.000387 5 68 munmap
8.49 0.000349 20 17 2 openat
6.81 0.000280 280 1 execve
5.47 0.000225 9 23 read
5.40 0.000222 11 20 fstat
5.04 0.000207 7 27 mprotect
4.79 0.000197 3 63 rt_sigaction
4.26 0.000175 8 21 close
4.11 0.000169 4 37 futex
3.43 0.000141 5 26 14 fcntl
3.11 0.000128 4 27 madvise
3.09 0.000127 18 7 3 statx
2.65 0.000109 18 6 clone
2.29 0.000094 3 27 getpid
1.99 0.000082 4 17 brk
1.34 0.000055 3 18 2 ioctl
1.29 0.000053 3 15 capget
1.26 0.000052 3 15 getegid
1.14 0.000047 1 27 rt_sigprocmask
1.12 0.000046 3 15 geteuid
1.09 0.000045 3 15 getuid
1.07 0.000044 2 15 getgid
1.00 0.000041 41 1 1 faccessat
0.46 0.000019 2 8 prlimit64
0.36 0.000015 5 3 pipe2
0.34 0.000014 7 2 eventfd2
0.34 0.000014 7 2 epoll_create1
0.22 0.000009 4 2 2 io_uring_setup
0.15 0.000006 6 1 sched_getaffinity
0.12 0.000005 5 1 gettid
0.10 0.000004 4 1 prctl
0.10 0.000004 2 2 getrandom
0.07 0.000003 3 1 1 clone3
0.05 0.000002 0 4 epoll_ctl
0.05 0.000002 0 3 write
0.02 0.000001 1 1 getcwd
0.00 0.000000 0 3 epoll_pwait
0.00 0.000000 0 1 readlinkat
0.00 0.000000 0 1 set_tid_address
0.00 0.000000 0 1 set_robust_list
0.00 0.000000 0 1 rseq
------ ----------- ----------- --------- --------- ------------------
100.00 0.004111 6 610 25 total
ぱっと見で行数を比較すると、言語ごとにシステムコールの量が違うことがわかったと思います。
特に C が一番少なく、Node.js や Go が長いです。
また、今回着目している write システムコールの回数を見ると、Node.js 以外は全て 1回 であることがわかると思います。これは、Hello World を一回標準出力しているためです。
なぜ Node.js だけ多いかは次のセクションで解説します。
統計データの各列についての意味は以下になります。
| 項目 | 意味 |
|---|---|
| time | 全体の実行時間に対する割合、対象のシステムコールがどれだけの時間を使ったか。 |
| seconds | 実行にかかった合計時間(秒)、対象のシステムコールが費やした総時間。 |
| usecs / call | 1回あたりの平均時間(マイクロ秒)、対象のシステムコール1回の呼び出しにかかった時間 |
| errors | エラー回数、対象のシステムコールが失敗した回数 |
| syscall | システムコール数、実際に呼ばれたカーネル関数 |
これを踏まえてみると、数値としてシステムコール数の差に気づくと思います。
| 言語 | calls | seconds |
|---|---|---|
| C | 33 | 0.001168 秒 |
| Go | 911 | 0.003455 秒 |
| Rust | 63 | 0.000721 秒 |
| Python | 336 | 0.002131 秒 |
| Node.js | 610 | 0.004111 秒 |
なんと、Hello World を出すだけなのに、C は 33回 、Go は 911回 と約28倍 の差があることがわかりました。
なぜ違いが生まれるのかは後ほど解説します。
各言語ごとの write システムコールを見比べる
straceに-e オプションをつけることで指定したシステムコールのみ抽出することができます。
以下の場合は、C言語の実行ファイルから write システムコールのみ抽出しています。
strace -e write ./bin/hello_c > /dev/null
それでは、各言語ごとに出力していきます。
$ strace -e write ./bin/hello_c > /dev/null
write(1, "Hello World\n", 12) = 12
+++ exited with 0 +++
$ strace -e write ./bin/hello_go > /dev/null
write(1, "Hello World\n", 12) = 12
+++ exited with 0 +++
$ strace -e write ./bin/hello_rust > /dev/null
write(1, "Hello World\n", 12) = 12
+++ exited with 0 +++
$ strace -e write python3 python/hello.py > /dev/null
write(1, "Hello World\n", 12) = 12
+++ exited with 0 +++
$ strace -e write node node/hello.js > /dev/null
write(5, "*", 1) = 1
write(1, "Hello World\n", 12) = 12
write(12, "\1\0\0\0\0\0\0\0", 8) = 8
+++ exited with 0 +++
なんと、どの言語でも Hello World の書き込みは同じでした。
write(1, "Hello World\n", 12) = 12
なぜかというと、システムコールはカーネルが定義した窓口のようなもので、最終的にはどの言語も同じ窓口を通るためです。以下のようなイメージです。

しかし、Node.js だけ追加の write が存在しています。
これは何なのかというと libuv(非同期I/O) の内部処理によるもので、深くなってしまうため割愛します。
なぜ違いが生まれるのか
各言語の統計を調べた時に出た疑問ですね。
どの言語も最終的には同じ write システムコールなのに、なぜ総コール数に差が出るのか。
それはランタイムの違いにあるようです。
ランタイムの違い
| 言語 | ランタイムの特徴 | 結果 |
|---|---|---|
| C | 直接機械語にコンパイルされるため、余計な処理が発生しません。 | 最小限(33回) |
| Go | Go はランタイム起動時に goroutine 管理のための準備を行うため、シグナル関連(rt_sigprocmask 457回、rt_sigaction 165回)やメモリ確保(mmap 190回)が多く発生しています。 | シグナル設定が多い(911回) |
| Rust | C と同様にランタイムは最小限。しかし標準ライブラリの初期化処理で mmap(メモリ確保)などが発生します。 |
C にほぼ近い(63回) |
| Python | インタプリタ言語のため、実行前に CPython の起動と標準ライブラリの読み込みが必要。ファイル関連のシステムコールが多く発生しています。 | ファイルアクセスが多い(336回) |
| Node.js | V8エンジン(JavaScript 実行) と libuv(非同期I/O)で構成されている。V8起動時のメモリ確保、libuv のワーカースレッドの生成がおこなわれます。 | メモリの確保が多い(610回) |
※各言語のランタイムの詳細については割愛します。
まとめ
どの言語も最終的には同じシステムコール(write) を呼んでいる。
しかし、そこに至るまでのシステムコール数は、言語によって大きく異なり、この差が 「速い」 「軽い」 と言われる正体の一部だということがわかりました。
実務ではなかなか触れない領域ではありますが、低レイヤーを学ぶことで自分のコードが内部でどう動いているのかが見えて面白いです。ぜひ皆さんも strace で覗いてみてください。
サンプルリポジトリ
参考資料
system call
node.js
write でパイプに通知する実装コード







