本記事の構成および論理分析にはAI(人工知能)を使用しています。情報の正確性は、システム管理者(UNIXユーザー)による手動検証済みです。
C言語で作るLC-3仮想マシン 第7回: 命令実行数を数える簡易プロファイル機能 | UNIX Cafe

前回は、VMに--traceオプションを追加し、命令を1つ実行するたびにPC、命令word、opcode、TRAP名、レジスタ、条件フラグを表示できるようにしました。
第7回では、1命令ずつの詳しい観察から一歩進んで、プログラム全体でどの種類の命令がどれくらい実行されたのかを数えます。Hello Worldのような小さいプログラムだけでなく、2048.objのような大きめのLC-3プログラムでも、実行の傾向を見られるようにします。
今回は、VMに--profileオプションを追加します。実行した命令数の合計とopcodeごとの実行回数を数え、プログラム終了後にprint_profile()でまとめて表示します。
GitHubでは lc3-vm-c という名前で公開しています。
この記事時点のコードは、GitHubのlessons/07-profileに完成版として置いています。リポジトリ直下のlc3.cは、連載の進行に合わせて最新版へ更新していきます。
今回作るプロファイル機能の全体像
第6回では、命令を1つ実行するたびにPC、命令word、opcode、レジスタを表示できるようにしました。
第7回では、その実行ループにカウンタを追加します。
第6回のVM
.objを読み込む
↓
fetch
PCが指す命令wordを読む
↓
decode
opcode = instr >> 12
↓
profile
total_instructionsを増やす
op_counts[opcode]を増やす
↓
trace
1命令ごとの状態を表示する
↓
execute
命令を実行する
プログラム終了後
print_profile()でまとめて表示する今回追加する主な機能は、以下の通りです。
OPCODE_COUNT: opcode数に合わせて集計配列の要素数を決めるtotal_instructions: 実行した命令数の合計を保存するop_counts[16]: opcodeごとの実行回数を保存するprint_profile(): プログラム終了後に集計結果を表示する--profileオプション: 命令実行数の集計を有効にする- 引数処理の拡張:
--traceと--profileを同時に指定できるようにする
第7回で作業する場所は、VM本体の中ではかなり限られています。
lc3.c
├─ include
│ └─ 追加なし
│
├─ enum
│ ├─ レジスタ定義
│ ├─ オペコード定義
│ ├─ 条件フラグ定義
│ ├─ MR_KBSR / MR_KBDR
│ └─ TRAPコード定義
│
├─ define
│ ├─ MEMORY_MAX
│ └─ OPCODE_COUNT を追加
│
├─ グローバル変数
│ ├─ memory[]
│ ├─ reg[]
│ ├─ total_instructions を追加
│ ├─ op_counts[OPCODE_COUNT] を追加
│ └─ original_tio
│
├─ 補助関数
│ ├─ disable_input_buffering()
│ ├─ restore_input_buffering()
│ ├─ check_key()
│ ├─ handle_interrupt()
│ ├─ sign_extend()
│ ├─ swap16()
│ ├─ update_flags()
│ ├─ read_image_file()
│ └─ read_image()
│
├─ メモリ操作
│ ├─ mem_write()
│ └─ mem_read()
│
├─ 表示用の補助関数
│ ├─ opcode_name() を再利用
│ ├─ trap_name()
│ ├─ cond_name()
│ ├─ trace_instruction()
│ ├─ trace_registers()
│ └─ print_profile() を追加
│
└─ main()
├─ 引数処理を変更
│ └─ --profile オプションを追加
├─ .objファイルを読み込む
├─ signal(SIGINT, handle_interrupt)
├─ disable_input_buffering()
├─ CONDをZに初期化する
├─ PCを x3000 に設定する
├─ while (running)
│ ├─ fetch
│ ├─ opcodeを取り出す
│ ├─ total_instructions を増やす処理を追加
│ ├─ op_counts[op] を増やす処理を追加
│ ├─ trace_instruction()
│ ├─ switch (op)
│ └─ trace_registers()
├─ restore_input_buffering()
└─ print_profile() 呼び出しを追加プログラム終了時に表示するプロファイル結果
前回は、VMに --trace オプションを追加しました。
これで、LC-3プログラムがどの番地から命令を読み、どのopcodeとして実行され、レジスタがどう変わるかを1命令ずつ見られるようになりました。
今回は、その流れをもう少し大きな単位で見ます。
VMに --profile オプションを追加し、プログラム終了時に次の情報を表示します。
実行した命令数の合計
opcodeごとの実行回数たとえば、次のような出力を目指します。
profile:
total instructions: 12345
BR 4200
ADD 3100
LD 1800
ST 950
TRAP 9今回作るのは、本格的な性能解析ツールではありません。
実行時間は測りませんし、関数単位の集計もしません。 まずは「どの種類の命令が何回実行されたか」を数えるだけにします。
それでも、Hello Worldのような小さいプログラムと、2048やRogueのような大きめのプログラムを比べると、VMが実行している命令の傾向が見えやすくなります。
実装する5つの作業
今回の変更は、前回作った --trace 機能よりも小さいです。
やることは大きく分けて5つです。
1. 実行回数を保存する変数を用意する
2. プロファイル結果を表示する関数を作る
3. --profile オプションを受け取れるようにする
4. fetchした命令のopcodeを数える
5. プログラム終了後に集計結果を表示する前回の --trace は、命令を実行するたびに情報を表示しました。
今回の --profile は、命令を実行するたびにカウンタだけを増やし、最後にまとめて表示します。
trace:
1命令ごとに表示する
profile:
1命令ごとに数える
最後にまとめて表示する通常実行では、これまでどおり余計な情報は表示しません。
./lc3 programs/hello.objプロファイルを取りたいときだけ、--profile を付けます。
./lc3 --profile programs/hello.obj前回の --trace と同時に指定できるようにもしておきます。
./lc3 --trace --profile programs/hello.objこの記事では、コードブロックの直前に役割を書きます。確認するコードは読むだけの抜粋、追加するコードは書き足す部分、変更後のコードは既存コードを置き換えたあとの形です。
今回は、各パートで少しずつコードを追加していきます。ただし、途中では実行確認を挟みません。プロファイル機能はカウンタ、引数処理、終了後の表示がそろってから動かすほうが分かりやすいので、必要な変更を最後まで入れてから、--profile を付けてまとめて確認します。
命令の種類ごとに実行回数を数える
LC-3の命令は16bitで、上位4bitがopcodeでした。opcodeは、命令の種類を表す番号です。
ここで数えたいのは、プログラム中に書かれている行数ではなく、VMが実行中に取り出した命令の数です。同じ命令でも、ループの中にあれば何度も実行されます。逆に、分岐で通らなかった命令は、プログラムに書かれていても実行回数には入りません。
たとえば、ADD 命令を1回実行したら ADD のカウンタを1つ増やします。BR 命令を1回実行したら BR のカウンタを1つ増やします。このように、fetchした命令をopcodeで分類して、「どの種類の命令が何回動いたか」を数えます。
確認するコード:
uint16_t op = instr >> 12;opcodeは0から15までの値になります。
第2回で定義したopcode enumは、次のようになっていました。
確認するコード:
enum
{
OP_BR = 0,
OP_ADD,
OP_LD,
OP_ST,
OP_JSR,
OP_AND,
OP_LDR,
OP_STR,
OP_RTI,
OP_NOT,
OP_LDI,
OP_STI,
OP_JMP,
OP_RES,
OP_LEA,
OP_TRAP
};つまり、opcodeごとの実行回数は、要素数16の配列で持てます。
op_counts[OP_BR] BR命令の実行回数
op_counts[OP_ADD] ADD命令の実行回数
op_counts[OP_LD] LD命令の実行回数
...
op_counts[OP_TRAP] TRAP命令の実行回数命令全体の実行回数も別に持ちます。
total_instructionsop_counts の合計と同じ値になりますが、合計値を毎回計算するより、命令をfetchするたびに1つ増やすほうが単純です。
命令数を保存する変数を追加する
まず、ファイル先頭のincludeを確認します。
前回のコードでは、strcmp() を使うために string.h を追加しました。
今回は、大きな実行回数を扱えるように uint64_t を使います。 stdint.h はすでに入っているので、includeの追加は不要です。
確認するコード:
#include <stdio.h>
#include <stdint.h>
#include <signal.h>
#include <string.h>次に、メモリとレジスタ配列の近くに、プロファイル用の変数を追加します。
追加するコード:
#define OPCODE_COUNT 16
static uint64_t total_instructions;
static uint64_t op_counts[OPCODE_COUNT];total_instructions は、実行した命令数の合計です。
op_counts は、opcodeごとの実行回数です。
uint64_t にしているのは、2048やRogueのように長く動かすプログラムでも、すぐに桁あふれしないようにするためです。
今回の用途なら unsigned long でも動くことが多いですが、幅がはっきりしている uint64_t を使います。
プロファイル結果を表示する関数を作る
前回、opcode番号を命令名へ変換する opcode_name() を作りました。
確認するコード:
static const char *opcode_name(uint16_t op)今回はこの関数を再利用します。
opcodeごとのカウントを表示するときに、OP_ADD のような番号ではなく、ADD のような名前で表示したいからです。
追加するコード:
static void print_profile(void)
{
printf("\nprofile:\n");
printf("total instructions: %llu\n\n",
(unsigned long long)total_instructions);
for (uint16_t op = 0; op < OPCODE_COUNT; ++op) {
if (op_counts[op] == 0) {
continue;
}
printf("%-6s %10llu\n",
opcode_name(op),
(unsigned long long)op_counts[op]);
}
}uint64_t を printf() で表示する方法はいくつかあります。
本来は inttypes.h の PRIu64 を使うと型に厳密ですが、今回は記事のコードを増やしすぎないため、unsigned long long にキャストして %llu で表示します。
(unsigned long long)total_instructionsop_counts[op] == 0 のopcodeは表示しません。
Hello Worldでは LEA と TRAP しか実行されないので、未使用の命令まで全部並べると少し読みにくくなります。
必要なら、0回のopcodeも表示する形に変えてもかまいません。
--profile を引数で受け取れるようにする
前回の引数処理は、通常実行と --trace 実行だけに対応していました。
確認するコード:
int trace_enabled = 0;
const char *image_path = NULL;
if (argc == 2) {
image_path = argv[1];
} else if (argc == 3 && strcmp(argv[1], "--trace") == 0) {
trace_enabled = 1;
image_path = argv[2];
} else {
fprintf(stderr, "usage: %s [--trace] image.obj\n", argv[0]);
return 2;
}今回は、--trace と --profile の両方を受け取れるようにします。
引数の順番に強く依存しないように、最後の引数を .obj ファイル、それより前をオプションとして読む形にします。
./lc3 image.obj
./lc3 --trace image.obj
./lc3 --profile image.obj
./lc3 --trace --profile image.obj
./lc3 --profile --trace image.obj変更後のコード:
int trace_enabled = 0;
int profile_enabled = 0;
const char *image_path = NULL;
for (int i = 1; i < argc; ++i) {
if (strcmp(argv[i], "--trace") == 0) {
trace_enabled = 1;
} else if (strcmp(argv[i], "--profile") == 0) {
profile_enabled = 1;
} else if (image_path == NULL) {
image_path = argv[i];
} else {
fprintf(stderr, "usage: %s [--trace] [--profile] image.obj\n", argv[0]);
return 2;
}
}
if (image_path == NULL) {
fprintf(stderr, "usage: %s [--trace] [--profile] image.obj\n", argv[0]);
return 2;
}この書き方なら、--trace と --profile の順番はどちらでもかまいません。
ただし、今回の簡易パーサでは、未知の --xxx もファイル名として扱われる可能性があります。 もう少し厳密にするなら、argv[i][0] == '-' のときは未知のオプションとしてエラーにしてもかまいません。
今回は連載用の小さいVMなので、まずは読みやすさを優先します。
入力ミスしたオプションをエラーにする
とはいえ、--profiel のようなtypoをファイル名として扱うと、エラーメッセージが分かりにくくなります。
そこで、- で始まる未知の引数はエラーにしておきます。
変更後のコード:
int trace_enabled = 0;
int profile_enabled = 0;
const char *image_path = NULL;
for (int i = 1; i < argc; ++i) {
if (strcmp(argv[i], "--trace") == 0) {
trace_enabled = 1;
} else if (strcmp(argv[i], "--profile") == 0) {
profile_enabled = 1;
} else if (argv[i][0] == '-') {
fprintf(stderr, "unknown option: %s\n", argv[i]);
fprintf(stderr, "usage: %s [--trace] [--profile] image.obj\n", argv[0]);
return 2;
} else if (image_path == NULL) {
image_path = argv[i];
} else {
fprintf(stderr, "usage: %s [--trace] [--profile] image.obj\n", argv[0]);
return 2;
}
}
if (image_path == NULL) {
fprintf(stderr, "usage: %s [--trace] [--profile] image.obj\n", argv[0]);
return 2;
}これで、次のような間違いはすぐ分かります。
./lc3 --profiel programs/hello.obj出力です。
unknown option: --profiel
usage: ./lc3 [--trace] [--profile] image.obj命令を読み込んだ直後にカウントする
命令数を増やす場所は、fetchの直後にします。fetchは、PCが指しているメモリから次に実行する命令wordを読み込む処理です。
前回のfetch周辺は、次のようになっていました。
確認するコード:
uint16_t pc_before = reg[R_PC];
uint16_t instr = mem_read(reg[R_PC]++);
uint16_t op = instr >> 12;
if (trace_enabled) {
trace_instruction(pc_before, instr, op);
}ここにプロファイル用のカウント処理を追加します。
変更後のコード:
uint16_t pc_before = reg[R_PC];
uint16_t instr = mem_read(reg[R_PC]++);
uint16_t op = instr >> 12;
if (profile_enabled) {
++total_instructions;
++op_counts[op];
}
if (trace_enabled) {
trace_instruction(pc_before, instr, op);
}これで、VMが1命令fetchするたびに、合計命令数とopcodeごとの命令数が増えます。
op は4bitの値なので、0から15の範囲に収まります。 そのため、op_counts[op] でそのまま配列を引けます。
プログラム終了後に結果を表示する
プロファイル結果は、プログラムが終わってから表示します。
実行ループの最後は、前回まで次のようになっていました。
確認するコード:
while (running)
{
...
}
restore_input_buffering();
return 0;while ループを抜けたあと、端末設定を戻してから print_profile() を呼びます。
変更後のコード:
while (running)
{
...
}
restore_input_buffering();
if (profile_enabled) {
print_profile();
}
return 0;restore_input_buffering() のあとにしているのは、端末設定を先に戻したいからです。
第3回で、LC-3プログラムのキー入力を扱うために、端末を非カノニカルモードにしました。 プロファイル表示そのものは通常の printf() なので、最後に端末を戻してから表示するほうが扱いやすいです。
Hello Worldで3つの命令を確認する
まずは、前回まで使ってきたHello Worldで確認します。
./lc3 --profile lc3as-lab/build/hello-minias.obj出力例です。
Hello, World!
HALT
profile:
total instructions: 3
LEA 1
TRAP 2この3つの命令は、第4回の「同じ内容をアセンブリとして書く」で書いたHello Worldのアセンブリ本体にあたります。
また、第5回の「Pass 1を実装する」では、これが x3000 から x3002 に配置され、その後ろの x3003 から文字列データが続くことを確認しました。
つまり、ここで数える「命令部分」は .ORIG、.STRINGZ、.END などの疑似命令や文字列データではなく、VMが実際に実行する次の3つの命令を指しています。
LEA R0, HELLO
PUTS
HALTPUTSとHALTがどちらもTRAPになる理由
PUTS と HALT は、アセンブリ上では別の命令のように見えます。
しかしLC-3の機械語として見ると、どちらもopcodeは TRAP になります。違いは、命令wordの下位8bitに入っているTRAPベクタ番号です。
PUTS -> TRAP x22
HALT -> TRAP x25今回の --profile は、命令wordの上位4bitにあるopcodeだけを数えています。そのため、TRAP x22 と TRAP x25 はどちらも同じ TRAP として集計されます。
LEA 1回
TRAP 2回つまり、この結果は「PUTSが何回、HALTが何回」ではなく、「TRAP opcodeの命令が何回実行されたか」を表しています。
もし PUTS と HALT を分けて数えたい場合は、opcodeだけでなく、instr & 0xFF で取り出せるTRAPベクタ番号も別に集計する必要があります。
今回はまず、VMが実行した命令をopcode単位で見るところまでで十分です。
--trace で順番、--profile で合計を見る
--trace と --profile は同時に使えます。
./lc3 --trace --profile lc3as-lab/build/hello-minias.obj出力例です。
PC=3000 INSTR=E002 OP=LEA
R0=3003 R1=0000 R2=0000 R3=0000 R4=0000 R5=0000 R6=0000 R7=0000 PC=3001 COND=P
PC=3001 INSTR=F022 OP=TRAP PUTS
Hello, World!
R0=3003 R1=0000 R2=0000 R3=0000 R4=0000 R5=0000 R6=0000 R7=3002 PC=3002 COND=P
PC=3002 INSTR=F025 OP=TRAP HALT
HALT
R0=3003 R1=0000 R2=0000 R3=0000 R4=0000 R5=0000 R6=0000 R7=3003 PC=3003 COND=P
profile:
total instructions: 3
LEA 1
TRAP 2trace は、VMが命令を実行する流れを時系列で見るためのものです。PCがどの番地を指し、どの命令wordをfetchし、実行後にレジスタや条件フラグがどう変わったかを、1命令ずつ確認できます。
一方、profile は、プログラム全体を実行した結果として、どのopcodeが何回実行されたかを見るためのものです。途中の順番やレジスタの変化は追わず、最後に合計だけを表示します。
Hello Worldのように3つの命令で終わるプログラムなら
Hello Worldのように3つの命令で終わるプログラムなら、両方を同時に使うと分かりやすいです。最初に --trace の出力で LEA、TRAP PUTS、TRAP HALT の順に実行されたことを確認し、最後に --profile の出力で LEA が1回、TRAP が2回だったことを確認できます。
ただし、大きなプログラムに --trace を付けると出力が非常に多くなります。画面描画や入力待ちを含むプログラムでは、同じような命令が何千回、何万回も実行されるため、ログを読むだけで大変になります。
2048やRogueでは、まず --profile だけで全体の傾向を見るほうが扱いやすいです。気になる命令や処理の場所が見えてきたら、必要な範囲に絞って --trace で詳しく追う、という使い方が向いています。
2048で大きいプログラムの傾向を見る
次に、少し大きいLC-3プログラムで確認します。
./lc3 --profile programs/2048.obj2048は対話的なゲームなので、Hello Worldのようにすぐ終了するわけではありません。起動すると、まずANSI端末かどうかを聞かれます。
Are you on an ANSI terminal (y/n)?通常のmacOS/Linuxターミナルなら y を入力します。そのあとは、第3回で確認したときと同じように W、A、S、D でタイルを動かします。
W 上
A 左
S 下
D 右--profile の結果は、LC-3プログラムが終了して、VMの実行ループを抜けたあとに表示されます。2048の場合は、盤面が埋まり、上下左右どこにも動かせなくなるとゲームオーバーになります。
You lost :(
Would you like to play again (y/n)?ここで y を入力すると、もう一度2048が始まります。プロファイル結果を見たい場合は、n を入力してゲームを終了します。LC-3プログラムが通常終了すると、そのあとでプロファイル結果が続けて表示されます。
途中でやめたい場合は Ctrl-C で止められます。ただし、今回の実装では Ctrl-C は割り込み終了なので、端末設定は戻りますが、通常終了時のようなプロファイル結果は表示されません。集計結果を見たい場合は、ゲームオーバー後の確認で n を入力し、LC-3プログラムを終了させます。
出力例です。
profile:
total instructions: 987654
BR 220001
ADD 185432
LD 84210
ST 54120
JSR 32010
AND 77222
LDR 140998
STR 65012
NOT 4210
LDI 1320
STI 870
JMP 31640
LEA 14230
TRAP 2379この数字は一例です。
ゲームは入力や遊んだ時間によって実行命令数が変わるので、毎回同じにはなりません。
見るべきなのは、正確な数字そのものよりも傾向です。
分岐命令が多い
メモリ読み書きが多い
TRAPは画面出力や入力のタイミングで出るこのくらいの粒度でも、Hello Worldとはかなり違うプログラムであることが分かります。
今回のプロファイル機能で分かること
今回作った機能は、かなり単純なプロファイルです。本格的な性能解析ツールというより、VMの中でどの種類の命令がどれくらい実行されているかを見るための観察用の機能です。
数えているのは、次の2つだけです。
実行した命令数の合計
opcodeごとの実行回数命令をfetchするたびに、合計カウンタとopcode別カウンタを1つ増やしています。そのため、結果は「このプログラムは全体としてどんな命令を多く使ったか」を見るためのものになります。
Hello Worldなら LEA が1回、TRAP が2回。2048なら、分岐、演算、メモリ読み書き、画面出力や入力に関わる TRAP がたくさん出てきます。この差を見るだけでも、小さい確認用プログラムと対話的なゲームでは、VMが実行している仕事の量も種類も大きく違うことが分かります。
一方で、一般的なプロファイラは、もっと多くの情報を扱います。
たとえば、次のようなものです。
どの関数で時間を使っているか
どの行が何回実行されたか
キャッシュミスがどれくらいあるか
どの処理が待ち時間を発生させているか今回のVMでは、そこまではしません。今回のカウンタでは、命令の実行時間は測っていません。どのサブルーチンで多く実行されたかも分かりません。同じ TRAP の中で、PUTS と HALT と GETC を分けることもしていません。
これは手抜きというより、今回の目的に合わせて範囲を絞っています。LC-3の .obj には、Cの関数名やソースコードの行番号のような情報が基本的に入っていません。まずは、命令をfetchし、decodeし、executeするVMの流れの中で、どこに観察用の処理を差し込めばよいかを理解することが目的です。
ただ、命令数を数えるだけでも、次のような観察はできます。
小さいプログラムと大きいプログラムの違い
BRやJMPが多いプログラム
LDやSTが多いプログラム
TRAPをどれくらい呼んでいるかこれくらいなら、VMの中に少しコードを足すだけで実現できます。しかも、--trace のように大量のログを出さなくても、プログラム全体の傾向を最後にまとめて見られます。
今回の結論としては、プロファイル機能は「速い・遅い」を厳密に測るためのものではありません。VMが実行した命令を数え、プログラムの性格をざっくり見るための道具として使います。このくらいの小さい機能でも、VMをただ動かすだけの段階から、VMの中で何が起きているかを観察する段階に進めます。
今回の完成コード
ここまでの変更をまとめると、次の形になります。
完成コード: lc3.c の全文です。今回追加・変更した場所には、// 追加:や// 変更:のコメントを付けています。実行確認は、このコードを入れ終わってから行います。
#include <stdio.h>
#include <stdint.h>
#include <signal.h>
#include <string.h>
/* unix only */
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/termios.h>
#include <sys/mman.h>
enum
{
R_R0 = 0,
R_R1,
R_R2,
R_R3,
R_R4,
R_R5,
R_R6,
R_R7,
R_PC, /* program counter */
R_COND,
R_COUNT
};
enum
{
FL_POS = 1 << 0, /* P */
FL_ZRO = 1 << 1, /* Z */
FL_NEG = 1 << 2, /* N */
};
enum
{
OP_BR = 0, /* branch */
OP_ADD, /* add */
OP_LD, /* load */
OP_ST, /* store */
OP_JSR, /* jump register */
OP_AND, /* bitwise and */
OP_LDR, /* load registar */
OP_STR, /* store registar */
OP_RTI, /* unused */
OP_NOT, /* bitwise not */
OP_LDI, /* load indirect */
OP_STI, /* store indirect */
OP_JMP, /* jump */
OP_RES, /* reserved (unused) */
OP_LEA, /* load effective address */
OP_TRAP /* execute trap */
};
enum
{
MR_KBSR = 0xFE00, /* keyboard status */
MR_KBDR = 0xFE02 /* keyboard data */
};
enum
{
TRAP_GETC = 0x20, /* get character from keyboard, not echoed onto the terminal */
TRAP_OUT = 0x21, /* output a character */
TRAP_PUTS = 0x22, /* output a word string */
TRAP_IN = 0x23, /* get character from keyboard, echoed onto the terminal */
TRAP_PUTSP = 0x24, /* output a byte string */
TRAP_HALT = 0x25 /* halt the program */
};
#define MEMORY_MAX (1 << 16)
// 追加: opcodeは4bitなので、集計用の配列は16要素にする
#define OPCODE_COUNT 16
uint16_t memory[MEMORY_MAX]; /* 65536 locations */
uint16_t reg[R_COUNT];
// 追加: プロファイル結果を保持するカウンタ
static uint64_t total_instructions;
static uint64_t op_counts[OPCODE_COUNT];
struct termios original_tio;
void disable_input_buffering(void)
{
tcgetattr(STDIN_FILENO, &original_tio);
struct termios new_tio = original_tio;
new_tio.c_lflag &= ~ICANON & ~ECHO;
tcsetattr(STDIN_FILENO, TCSANOW, &new_tio);
}
void restore_input_buffering(void)
{
tcsetattr(STDIN_FILENO, TCSANOW, &original_tio);
}
uint16_t check_key(void)
{
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds);
struct timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec = 0;
return select(1, &readfds, NULL, NULL, &timeout) != 0;
}
void handle_interrupt(int signal)
{
(void)signal;
restore_input_buffering();
printf("\n");
exit(-2);
}
uint16_t sign_extend(uint16_t x, int bit_count)
{
if ((x >> (bit_count - 1)) & 1) {
x |= (0xFFFF << bit_count);
}
return x;
}
uint16_t swap16(uint16_t x)
{
return (x << 8) | (x >> 8);
}
void update_flags(uint16_t r)
{
if (reg[r] == 0)
{
reg[R_COND] = FL_ZRO;
}
else if (reg[r] >> 15) /* a 1 in the left-most bit indicates negative */
{
reg[R_COND] = FL_NEG;
}
else
{
reg[R_COND] = FL_POS;
}
}
void read_image_file(FILE* file)
{
/* the origin tells us where in memory to place the image */
uint16_t origin;
fread(&origin, sizeof(origin), 1, file);
origin = swap16(origin);
/* we know the maximum file size so we only need one fread */
uint16_t max_read = MEMORY_MAX - origin;
uint16_t* p = memory + origin;
size_t read = fread(p, sizeof(uint16_t), max_read, file);
/* swap to little endian */
while (read-- > 0)
{
*p = swap16(*p);
++p;
}
}
int read_image(const char* image_path)
{
FILE* file = fopen(image_path, "rb");
if (!file) { return 0; }
read_image_file(file);
fclose(file);
return 1;
}
void mem_write(uint16_t address, uint16_t val)
{
memory[address] = val;
}
uint16_t mem_read(uint16_t address)
{
if (address == MR_KBSR)
{
if (check_key())
{
memory[MR_KBSR] = (1 << 15);
memory[MR_KBDR] = getchar();
}
else
{
memory[MR_KBSR] = 0;
}
}
return memory[address];
}
static const char *opcode_name(uint16_t op)
{
switch (op) {
case OP_BR: return "BR";
case OP_ADD: return "ADD";
case OP_LD: return "LD";
case OP_ST: return "ST";
case OP_JSR: return "JSR";
case OP_AND: return "AND";
case OP_LDR: return "LDR";
case OP_STR: return "STR";
case OP_RTI: return "RTI";
case OP_NOT: return "NOT";
case OP_LDI: return "LDI";
case OP_STI: return "STI";
case OP_JMP: return "JMP";
case OP_RES: return "RES";
case OP_LEA: return "LEA";
case OP_TRAP: return "TRAP";
default: return "UNKNOWN";
}
}
static const char *trap_name(uint16_t trapvect)
{
switch (trapvect) {
case TRAP_GETC: return "GETC";
case TRAP_OUT: return "OUT";
case TRAP_PUTS: return "PUTS";
case TRAP_IN: return "IN";
case TRAP_PUTSP: return "PUTSP";
case TRAP_HALT: return "HALT";
default: return "UNKNOWN";
}
}
static const char *cond_name(uint16_t cond)
{
switch (cond) {
case FL_POS: return "P";
case FL_ZRO: return "Z";
case FL_NEG: return "N";
default: return "?";
}
}
static void trace_instruction(uint16_t pc_before, uint16_t instr, uint16_t op)
{
printf("PC=%04X INSTR=%04X OP=%s", pc_before, instr, opcode_name(op));
if (op == OP_TRAP) {
uint16_t trapvect = instr & 0xFF;
printf(" %s", trap_name(trapvect));
}
printf("\n");
}
static void trace_registers(void)
{
printf("R0=%04X R1=%04X R2=%04X R3=%04X ", reg[R_R0], reg[R_R1], reg[R_R2], reg[R_R3]);
printf("R4=%04X R5=%04X R6=%04X R7=%04X ", reg[R_R4], reg[R_R5], reg[R_R6], reg[R_R7]);
printf("PC=%04X COND=%s\n", reg[R_PC], cond_name(reg[R_COND]));
}
// 追加: プロファイル結果を最後にまとめて表示する
static void print_profile(void)
{
printf("\nprofile:\n");
printf("total instructions: %llu\n\n",
(unsigned long long)total_instructions);
for (uint16_t op = 0; op < OPCODE_COUNT; ++op) {
if (op_counts[op] == 0) {
continue;
}
printf("%-6s %10llu\n",
opcode_name(op),
(unsigned long long)op_counts[op]);
}
}
int main(int argc, const char* argv[])
{
// 変更: 引数処理を走査式にして、--traceと--profileを同時指定できるようにする
int trace_enabled = 0;
int profile_enabled = 0;
const char *image_path = NULL;
for (int i = 1; i < argc; ++i) {
if (strcmp(argv[i], "--trace") == 0) {
trace_enabled = 1;
} else if (strcmp(argv[i], "--profile") == 0) {
profile_enabled = 1;
} else if (argv[i][0] == '-') {
fprintf(stderr, "unknown option: %s\n", argv[i]);
fprintf(stderr, "usage: %s [--trace] [--profile] image.obj\n", argv[0]);
return 2;
} else if (image_path == NULL) {
image_path = argv[i];
} else {
fprintf(stderr, "usage: %s [--trace] [--profile] image.obj\n", argv[0]);
return 2;
}
}
if (image_path == NULL) {
fprintf(stderr, "usage: %s [--trace] [--profile] image.obj\n", argv[0]);
return 2;
}
if (!read_image(image_path))
{
printf("failed to load image: %s\n", image_path);
exit(1);
}
signal(SIGINT, handle_interrupt);
disable_input_buffering();
/* since exactly one condition flag should be set at any given time, set the Z flag */
reg[R_COND] = FL_ZRO;
/* set the PC to starting position */
/* 0x3000 is the default */
enum { PC_START = 0x3000 };
reg[R_PC] = PC_START;
int running = 1;
while (running)
{
/* FETCH */
uint16_t pc_before = reg[R_PC];
uint16_t instr = mem_read(reg[R_PC]++);
uint16_t op = instr >> 12;
// 追加: 1命令fetchするたびに合計とopcode別の回数を増やす
if (profile_enabled) {
++total_instructions;
++op_counts[op];
}
if (trace_enabled) {
trace_instruction(pc_before, instr, op);
}
switch (op)
{
case OP_ADD:
{
/* destination register (DR) */
uint16_t r0 = (instr >> 9) & 0x7;
/* first operand (SR1) */
uint16_t r1 = (instr >> 6) & 0x7;
/* whether we are in immediate mode */
uint16_t imm_flag = (instr >> 5) & 0x1;
if (imm_flag)
{
uint16_t imm5 = sign_extend(instr & 0x1F, 5);
reg[r0] = reg[r1] + imm5;
}
else
{
uint16_t r2 = instr & 0x7;
reg[r0] = reg[r1] + reg[r2];
}
update_flags(r0);
}
break;
case OP_AND:
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;
uint16_t imm_flag = (instr >> 5) & 0x1;
if (imm_flag)
{
uint16_t imm5 = sign_extend(instr & 0x1F, 5);
reg[r0] = reg[r1] & imm5;
}
else
{
uint16_t r2 = instr & 0x7;
reg[r0] = reg[r1] & reg[r2];
}
update_flags(r0);
}
break;
case OP_NOT:
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;
reg[r0] = ~reg[r1];
update_flags(r0);
}
break;
case OP_BR:
{
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
uint16_t cond_flag = (instr >> 9) & 0x7;
if (cond_flag & reg[R_COND])
{
reg[R_PC] += pc_offset;
}
}
break;
case OP_JMP:
{
/* Also handles RET */
uint16_t r1 = (instr >> 6) & 0x7;
reg[R_PC] = reg[r1];
}
break;
case OP_JSR:
{
uint16_t long_flag = (instr >> 11) & 1;
reg[R_R7] = reg[R_PC];
if (long_flag)
{
uint16_t long_pc_offset = sign_extend(instr & 0x7FF, 11);
reg[R_PC] += long_pc_offset; /* JSR */
}
else
{
uint16_t r1 = (instr >> 6) & 0x7;
reg[R_PC] = reg[r1]; /* JSRR */
}
}
break;
case OP_LD:
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
reg[r0] = mem_read(reg[R_PC] + pc_offset);
update_flags(r0);
}
break;
case OP_LDI:
{
/* destination registar (DR) */
uint16_t r0 = (instr >> 9) & 0x7;
/* PCoffset 9 */
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
/* add pc_offset to the current PC, look at that memory location to get the final address */
reg[r0] = mem_read(mem_read(reg[R_PC] + pc_offset));
update_flags(r0);
}
break;
case OP_LDR:
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;
uint16_t offset = sign_extend(instr & 0x3F, 6);
reg[r0] = mem_read(reg[r1] + offset);
update_flags(r0);
}
break;
case OP_LEA:
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
reg[r0] = reg[R_PC] + pc_offset;
update_flags(r0);
}
break;
case OP_ST:
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
mem_write(reg[R_PC] + pc_offset, reg[r0]);
}
break;
case OP_STI:
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
mem_write(mem_read(reg[R_PC] + pc_offset), reg[r0]);
}
break;
case OP_STR:
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;
uint16_t offset = sign_extend(instr & 0x3F, 6);
mem_write(reg[r1] + offset, reg[r0]);
}
break;
case OP_TRAP:
reg[R_R7] = reg[R_PC];
switch (instr & 0xFF)
{
case TRAP_GETC:
/* read a single ASCII char */
reg[R_R0] = (uint16_t)getchar();
update_flags(R_R0);
break;
case TRAP_OUT:
putc((char)reg[R_R0], stdout);
fflush(stdout);
break;
case TRAP_PUTS:
{
/* one char per word */
uint16_t* c = memory + reg[R_R0];
while (*c)
{
putc((char)*c, stdout);
++c;
}
fflush(stdout);
}
break;
case TRAP_IN:
{
printf("Enter a character: ");
char c = getchar();
putc(c, stdout);
fflush(stdout);
reg[R_R0] = (uint16_t)c;
update_flags(R_R0);
}
break;
case TRAP_PUTSP:
{
/* one char per byte (two bytes per word)
here we need to swap back to
big endian format */
uint16_t* c = memory + reg[R_R0];
while (*c)
{
char char1 = (*c) & 0xFF;
putc(char1, stdout);
char char2 = (*c) >> 8;
if (char2) putc(char2, stdout);
++c;
}
fflush(stdout);
}
break;
case TRAP_HALT:
puts("HALT");
fflush(stdout);
running = 0;
break;
}
break;
case OP_RES:
case OP_RTI:
default:
abort();
break;
}
if (trace_enabled) {
trace_registers();
printf("\n");
}
}
restore_input_buffering();
// 追加: プログラム終了後にプロファイル結果を表示する
if (profile_enabled) {
print_profile();
}
return 0;
}今回できたこと
今回は、LC-3 VMに簡易プロファイル機能を追加しました。
--profileオプションを追加しました- 実行した命令数の合計を数えました
- opcodeごとの実行回数を数えました
opcode_name()を再利用して、命令名付きで表示しました--traceと--profileを同時に指定できるようにしました- Hello Worldでは
LEAが1回、TRAPが2回実行されることを確認しました
トレース機能では、1命令ずつの流れを見ることができました。
プロファイル機能では、プログラム全体でどの命令が多く実行されたかを見られます。
この2つを使い分けると、VMの動きを少し違う角度から観察できます。
次回はステップ実行を追加する
命令数は分かるようになりました。
ただし、トレースは流れ続けるログなので、じっくり確認したい場所があるときには少し忙しいです。
次回は、--step オプションを追加します。
1命令実行するたびに停止し、Enterで次へ進めるようにして、PCやレジスタの変化を手元で確認できるようにします。
