Pythonが遅い理由・C言語が速い理由をシステムコール呼び出し回数から考察する

Python

こんにちは。タンジェントです。

巷によく聞く、Pythonって遅いよね~~Cは速いよ!という会話。

「Pythonoはインタプリンタ言語で、C言語はコンパイラ言語だから」がよくある説明なのだが、こんな適当な説明だと、だからどうして?と思うだろう。

もうちょっと丁寧に言うと、

Python(インタプリタ言語)は一行一行機械語に翻訳して実行するから遅い、

C言語(コンパイル言語)は一度にすべて機械語に翻訳するから速い

が理由になる。

※他にも動的型付け言語であるとかいろいろ理由はありますが話を簡略化してます。

ここで、あーなるほどね!となればいいのだが好奇心旺盛な諸君に実際にどういう処理の中身になっているか説明することにしよう。

文章で説明するより実機で動かして、自分で確認したほうがわかりやすいと思うのでまずはLinuxの開発環境を整えよう。

  • Windows10
  • Virtual Box 6.1
  • Ubuntu 20.04

Linuxの環境であればいいのだが、WindowsにVirtual BoxでUbuntuを入れている。

Cygwinとかだと違うlog出力結果になるのでおすすめしない。

ソースコードの作成 Helloを出力するだけ

まずはソースコードを書くところからだ。

今回は単にHelloを出力するだけの処理にする。

C言語ではこのように書く。

ファイル名はhelloC.cとした。

#include<stdio.h>
int main(){
    printf("Hello\n")
    return 0;
}

いつものようにコンパイル→実行をしてみよう。

コンパイル。

$gcc helloC.c -o helloC

実行。

$./helloC

すると

Hello

と出力されるはずだ。ここで間違うと以降の説明のおじゃんになるのできちんと確認するように。

システムコール

え、これだけ?と思ったあなた、そんなに急ぐでない。

実行時に呼びされたシステムコールを見てみよう。

システムコールが何なのか簡単に説明すると、

我々が普段いじっているアプリケーションはCPUを直接いじれない。でも例えば画面にHelloという文字を出力するという処理はCPUが行っている。

じゃあどうやってCPUに命令を出すか。

我々はCPUをいじれないが、OSなら可能である。

Helloと出力するのはCPUであり、そのCPUを操作するのはOS。その実行依頼のことをシステムコールと言う。(ややこしい)

もっと分かりやすい説明がどこかにあると思うのであとはググってくださいな。

C言語のlogからシステムコールを確認

早速実践。

先ほどは

$./helloC

で実行したが、今回はstraceコマンドを使う。

$strace -o helloC.log ./helloC

helloC.logファイルが出力されるはずだ。

lessコマンドで中身を見てみよう。

$less helloC.log

こんな感じになるはずだ。

execve("./hello", ["./hello"], 0x7ffd94cb0030 /* 50 vars */) = 0
brk(NULL)                               = 0x5643d045b000
arch_prctl(0x3001 /* ARCH_??? */, 0x7fff69a01c00) = -1 EINVAL (無効な引数です)
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (そのようなファイルやディレ
クトリはありません)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=67717, ...}) = 0
mmap(NULL, 67717, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fa2244b2000
close(3)                                = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360q\2\0\0\0\0\0"..., 832) = 832
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
pread64(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32, 848) = 32
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0\t\233\222%\274\260\320\31\331\326\10\204\276X>\263"..., 68, 880) = 68
fstat(3, {st_mode=S_IFREG|0755, st_size=2029224, ...}) = 0
・
・
・

このlogファイルからhelloC実行時のシステムコールを見ることができる。

画面にHelloと出力するシステムコールはと言うと、ググれば出てくるのだが”write()”である。

write()を検索してみよう。先ほどのless helloC.log上で

/write

と入力してEnter。

・
・
・
arch_prctl(ARCH_SET_FS, 0x7fa2244b1540) = 0
mprotect(0x7fa2244a6000, 12288, PROT_READ) = 0
mprotect(0x5643ce6dd000, 4096, PROT_READ) = 0
mprotect(0x7fa2244f0000, 4096, PROT_READ) = 0
munmap(0x7fa2244b2000, 67717)           = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0), ...}) = 0
brk(NULL)                               = 0x5643d045b000
brk(0x5643d047c000)                     = 0x5643d047c000
write(1, "Hello\n", 6)                  = 6
exit_group(0)                           = ?
+++ exited with 0 +++
・
・
・

writeの部分がハイライトされたはずだ。

write(1, "Hello\n", 6)

Helloを画面出力するためにシステムコールwrite()が呼び出されたことがわかる。

ちなみに引数の1はシステムコール番号、6は文字数である。

lessから元のターミナルに戻りたければqと入力すればよい。

Pythonのlogからシステムコールを確認

お次はPythonの番。

正解を言ってしまえば、CもPythonも画面出力時はシステムコールwrite()が呼び出される。

ソース作成。ファイル名はhelloPython.pyとした。

print("Hello")

Pythonなので一行でおしまい。

実行するまでもなく、C言語のときとまったく同じ、Helloが出力された。

$python helloPython.py

次にstraceコマンドでlogファイルを出力しよう。

strace -o helloPython.log python helloPython.py

続いてlessコマンドでlogファイルの中身を確認。

$less helloPython.log

helloPython.py実行時のシステムコール一覧が見れるはずだ。

ここで見てほしいのは呼び出されたシステムコールの量だ。C言語のときと比べて圧倒的に多いことがわかる。

同じ”Hello”を出力するだけなのに、内部ではこれほどの違いがあることがわかる。

C言語と同様、lessの画面でwriteを検索しよう。

/write

と打つと、システムコールwrite()がハイライトされる。

・
・
・
read(3, "print(\"Hello\")\n", 4096)     = 15
lseek(3, 0, SEEK_SET)                   = 0
read(3, "print(\"Hello\")\n", 4096)     = 15
read(3, "", 4096)                       = 0
close(3)                                = 0
write(1, "Hello\n", 6)                  = 6
rt_sigaction(SIGINT, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7f615b595210}, {sa_handler=0x623ff0, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7f615b595210}, 8) = 0
sigaltstack(NULL, {ss_sp=0x27b1940, ss_flags=0, ss_size=16384}) = 0
sigaltstack({ss_sp=NULL, ss_flags=SS_DISABLE, ss_size=0}, NULL) = 0
exit_group(0)                           = ?
+++ exited with 0 +++
・
・
・

確かに、システムコールwrite()が呼び出されていることがわかる。C言語と一緒だ。

write(1, "Hello\n", 6)

もう一点。

write()から上3行目あたりを見てほしい。

read(3, "print(\"Hello\")\n", 4096) 

read()が呼び出されていることがわかる。

read()もwrite()と同様、システムコールだ。

Pythonはインタプリタ言語なので一行ずつ読んで実行している。

つまりread()で読んで、write()で出力しているということだ。

C言語のhelloC.logでreadを検索しても出てこない。システムコールread()を呼び出す必要がないからだ。

インタプリタ言語はシステムコール呼び出しが多い。read()とか。

PythonはC言語に比べてシステムコール呼び出しが多い。

例えば、Pythonでは”read(3, “print(\”Hello\”)\n”, 4096) “が呼び出されていた。

実行速度について、インタプリタ言語だから~~とかコンパイル言語だから~~という説明でももちろん正しいが、自分で手を動かして確認してみるというのも楽しいし理解が深まるだろう。

今回はシステムコール呼び出しという観点から実行速度に差が出る理由を考察した。

他の言語でも試してみるとよい。

コメント

タイトルとURLをコピーしました