自作CPUの面積削減の試行をしている間、どこかの段階でサイクル性能がデグレードしてしまった。 デグレードする前のリビジョンを特定したので、デグレード前とデグレード後の実行ログは取得できたのだが、Dhrystoneの実行ログは非常に長いので、どこが原因なのか詳細を把握できない。
もう少し、LSUパイプラインがどのような理由でリプレイ状態に入っているのかを統計的に取ってみる。
特定のコードで問題が発生することがつかめてきた。具体的には、printf()
を実行する以下のコードだ。
static void vprintfmt(void (*putch)(int, void**), void **putdat, const char *fmt, va_list ap) { register const char* p; const char* last_fmt; register int ch, err; unsigned long long num; int base, lflag, width, precision, altflag; char padc; while (1) { while ((ch = *(unsigned char *) fmt) != '%') { if (ch == '\0') return; fmt++; putch(ch, putdat); } fmt++;
さらにputch()
も続く。
int putchar(int ch) { static __thread char buf[64] __attribute__((aligned(64))); static __thread int buflen = 0; buf[buflen++] = ch; if (ch == '\n' || buflen == sizeof(buf)) { syscall(SYS_write, 1, (uintptr_t)buf, buflen); buflen = 0; } return 0; }
STQの使われ方を波形で見てみる。性能が落ちているバージョンは、STQがストアデータを受け取る際のパスを省略している。 その結果、各STQエントリの寿命が長くなっており、大きなストールにつながっているようだ。
ポイントは、同じアドレスを更新し続けているbuflenポインタのアップデートだと思う。これは、static
で宣言されておりメモリにマップされている、と思う。
static __thread int buflen = 0; buf[buflen++] = ch;
下記のアセンブリコードがひたすらbuflenの値をアップデートしている。
04022e03 lw t3, 64(tp) 001e031b addiw t1, t3, 1 04622023 sw t1, 64(tp) 01fe0023 sb t6, 0(t3)
今のパイプラインだと、lwとswのベースアドレスは、ループ内で変化がないので非常に速く決定できる。
しかし、sw
命令がaddiw
命令の結果を取得するのが少し遅れると、この小さなループは一気にハザードを発生し始める。
これは、STQがaddiwの結果を取得可能になったことを探知してからデータを取得するまでに時間がかかり、その間に小ループは次のロード命令が進んでしまうからである。
ここで発生しているハザードは、「STQ内で同じアドレスへのアクセスする命令が存在しており、フォワードが必要なのは分かっているが、データrs2が揃っていないので待たなければならない」ハザードである。
これを解消するには、MDP(Memory Dependence Predictor)などを追加して、次のイタレーションのLWが投機的に実行されてしまうのを防ぐことが考えられる。 しかしMDPの挿入は結果的にストールを発生させているのと等しいので、大きく性能が向上するとも考えられない。