書いたコードがどのようにCPUで実行されるかここ1年くらいでやっとイメージ出来るようになったので書いてみる。
ちなみにJVMとかインタプリタ言語がどうやって実行されているのかはまだ良くわかってない。
コードがコンパイルされてマシンコードになり、実行ファイルになる
コードはまずアセンブリに変換される。
アセンブリはほぼほぼCPU命令と1:1の人が読みやすい言語。
ほぼほぼCPU命令なので、CPUアーキテクチャ毎(x86_64, arm64など)に使えるアセンブリは異なる。
コンパイラは指定されたCPUアーキテクチャでプログラムコードをアセンブリに変換する。
その後、アセンブリからマシンコード(CPU命令そのもの)に変換され、(外部ライブラリコードを結合されたりもして)実行ファイルとして出力される。
実行ファイルにはマシンコードだけではなく、実行時に必要な情報(エントリーポイントや確保メモリサイズなど?)なども含まれたものになっている。
実行ファイルも色々形式があって、Windows用とLinux用で形式が異なったりする。(だからCPUアーキテクチャが同じでもWindowsの実行ファイルをLinuxで実行できない)
コンパイラは適切な実行ファイル形式で実行ファイルを作成する。(どの形式で実行ファイルを作成するか指定できたりするのかも)
OSから実行ファイルを実行し、プロセスになる
参考)
OSから実行ファイルを実行すると、マシンコードを含むファイル内容がメモリ上に読み込まれる。
OSはこの実行状態になったプログラムを「プロセス」として管理し、CPUを効率よく使うようにスケジューリングしながらプロセスをCPUに割り当てていく。
具体的には、プロセスはCPUに割り当てられて一定時間立つとCPUから外され次の割り当て機会を待つ。
その間、CPUには別のプロセスが割り当てられている。
シングルコアCPUの場合、このように正確にはある時間では1つのプロセスしか実行していないが、短い時間で切り替えながら複数のプロセスを処理するため、人間の体感的には複数のプロセスが同時に動いているように感じる。
マルチコアCPUの場合には複数のCPUがこれを同時に行うため、実際に複数のプロセスが同時に処理されている。
CPUがマシンコードを実行する
参考)
CPUはメモリ上にあるマシンコードを順次読み込み実行していくのみである。
CPU上にプログラムカウンタというレジスタ(レジスタはCPU上にある保存領域で容量は小さいがCPUからのアクセスが超早い。CPUのワーキング領域みたいな感じ)にメモリ上のマシンコードの実行したい部分のメモリアドレスが保持されている。
CPUはプログラムカウンタの指すメモリアドレスからマシンコードを読み込み、デコーダーでその命令を実行できるデジタル回路を有効化(という表現がいいのかよくわからないが、とにかく命令が正しく実行できる回路を利用できるように調整する)して、その回路を使って命令を実行する。
CPUにとっての「命令の実行」とは、対象のデジタル回路へ入力し、出力を得る事である。 例えば加算命令の場合、加算用のデジタル回路の入力へ1と2を入れると、出力から3が得られる。
そういう感じで、CPUは様々な命令に対応するデジタル回路を持っていて、マシンコードをデコーダーに通すことでどのデジタル回路を利用するかを調整し、適切なデジタル回路を使って結果を得て、また次のマシンコードをデコーダーに通し、、を繰り返している。
(ついでに)CPUの実行速度効率化
参考)
CPUの世界から見るとメモリの場所はかなり遠く、読み書きに時間がかかるので、CPUは利用するデータをできるだけ近くに配置する。
一番近く一番速いのがレジスタで、クロックサイクルでいう1サイクルで読み書き可能。
CPUとメモリの間にはキャシュが存在している(L1キャッシュ、L2キャッシュ, L3キャッシュなど)
これらはレジスタよりも遠いが、メモリよりもかなり近い。
なので、このキャッシュに収まるデータ量で収まるようなプログラムであれば実行速度が期待できるが、逆に収まらないとキャッシュがうまく使えず期待した実行速度がでないかもしれない。
CPUはできるだけ無駄な待ち時間が無いように動くように設計されている。
たとえば、ある命令が実行されている間、デコーダーが何もしないのはもったいないので、次の命令のデコードを同時に行う。
また、ある命令でメモリからのデータ取得を待っている間(メモリからの読み込みは時間がかかる)、何もしないのは勿体ないので次の命令を先に実行してしまっておく。
他にも色々ある。
分岐の無い真っすぐなプログラムの場合、上記のような工夫を使って最大限効率化されて実行される(GPUがまさにこのケースで最適化されているらしい)が、実際のプログラムには分岐処理などがあってなかなか効率化できないシーンがある。
分岐結果を予め予測し、空き時間の間に先行して分岐先の命令を実行しておく投機的実行も行われたりするが、分岐結果予測を外した場合にはそれらが無駄になってしまうなど、いろいろ面白い。
また、マルチコアで並列で実行されているゆえに効率化できないシーン(CPUキャッシュの同期が必要になる)などもある。
これらを理解しても、なかなかプログラムコードのレイヤーからそれらを制御することは難しいのでコーディングに活きるかはわからないが、面白かった。