目次: GCC
前回(2020年3月6日の日記参照)はレジスタ制約(register_constraints)を追加しました。これだけでは何もできませんので、今回はベクトルレジスタの定義を追加してみます。長そうなので分割して書きます。
RISC-Vには汎用レジスタ(GP_REGS)と浮動小数点レジスタ(FP_REGS)が既に定義されているため、それらを参考にします。
変更するファイルはgcc/config/riscv/riscv.c, riscv.hです。FP_REGくらいで検索すると、下記の関数、マクロに名前が見当たりますので、真似して追加します。(詳細は パッチファイルもご覧ください、内容の正しさは全く保証できませんけど)
// gcc/config/riscv/riscv.c
riscv_regno_to_class[FIRST_PSEUDO_REGISTER] //32個レジスタを足す
riscv_hard_regno_nregs //どのマシンモードでもレジスタを1つだけ使う、よくわからん、また今度調べる
riscv_hard_regno_mode_ok //どのマシンモードでも許可する、よくわからん、また今度調べる
riscv_class_max_nregs //どのクラスでもレジスタを1つだけ使う、よくわからん、また今度調べる
// gcc/config/riscv/riscv.h
FIRST_PSEUDO_REGISTER //32個分ずれてもらう
FIXED_REGISTERS //32個足す、今回は0にした、固定された役目(スタックポインタなど)はない
CALL_USED_REGISTERS //32個足す、今回は0にした(関数呼び出しにより内容を破壊されない、s0 - s11と同じ扱い)
enum reg_class
#define REG_CLASS_NAMES //新たなレジスタクラスを足す
#define REG_CLASS_CONTENTS //後述する
#define REG_ALLOC_ORDER //レジスタの割当順、レジスタ番号で指定する
#define REGISTER_NAMES //レジスタの名前
#define ADDITIONAL_REGISTER_NAMES
初歩の初歩的な変更の割に必要な変更点はかなり多いです。どの変更が何に効くか完全にわかっていないので、合っているかわかりませんし、説明し難い変更もあります。後日、要調査ですね。
変更した中のriscv_regno_to_classをみるとFIRST_PSEUDO_REGISTERというマクロが出てきます。GCCはレジスタを2種類使い分けていて、レジスタ番号で区別できます。
正式な名前がわからない(※)ので、名付けは適当です。GCCはRTLのフェーズで命令の引数にレジスタを割り当てます。その際、いきなりメモリや物理レジスタを割り当てるのではなく、まず疑似レジスタを割り当てます。
疑似レジスタには数の制限がないので、最初の方の最適化パスで必要なだけ割り当てます。その後の最適化パスで物理レジスタや、メモリにうまく割り当てを考える二段構成になっています。
今回は32個の物理レジスタを足そうとしているので、FIRST_PSEUDO_REGISTERにも32個分だけズレてもらう必要があります。
今回の変更の要はREG_CLASS_CONTENTSです。このマクロの効き目についてはまた今度。
(※)GCCのヘンテコなマクロの意味を調べる際、GCC Internals(HTML版へのリンク)が大変参考になるのですが、この文書は用語の説明がイマイチ甘くて、正式な用語がわかりません。いつも困ります……。
目次: GCC
レジスタ追加の変更の要はREG_CLASS_CONTENTSです。このマクロは32ビット整数の配列で、各レジスタ番号がどのレジスタの仲間(enum reg_class)に属するかを指定するテーブルです。こんな風に変更します。
#define REG_CLASS_CONTENTS \
{ \
- { 0x00000000, 0x00000000, 0x00000000 }, /* NO_REGS */ \
- { 0xf003fcc0, 0x00000000, 0x00000000 }, /* SIBCALL_REGS */ \
- { 0xffffffc0, 0x00000000, 0x00000000 }, /* JALR_REGS */ \
- { 0xffffffff, 0x00000000, 0x00000000 }, /* GR_REGS */ \
- { 0x00000000, 0xffffffff, 0x00000000 }, /* FP_REGS */ \
- { 0x00000000, 0x00000000, 0x00000003 }, /* FRAME_REGS */ \
- { 0xffffffff, 0xffffffff, 0x00000003 } /* ALL_REGS */ \
+ { 0x00000000, 0x00000000, 0x00000000, 0x00000000 }, /* NO_REGS */ \
+ { 0xf003fcc0, 0x00000000, 0x00000000, 0x00000000 }, /* SIBCALL_REGS */ \
+ { 0xffffffc0, 0x00000000, 0x00000000, 0x00000000 }, /* JALR_REGS */ \
+ { 0xffffffff, 0x00000000, 0x00000000, 0x00000000 }, /* GR_REGS */ \
+ { 0x00000000, 0xffffffff, 0x00000000, 0x00000000 }, /* FP_REGS */ \
+ { 0x00000000, 0x00000000, 0xffffffff, 0x00000000 }, /* VP_REGS */ \
+ { 0x00000000, 0x00000000, 0x00000000, 0x00000003 }, /* FRAME_REGS */ \
+ { 0xffffffff, 0xffffffff, 0xffffffff, 0x00000003 } /* ALL_REGS */ \
}
↑ここの3列目を足した
行方向は、ビットフィールドになっており非常にわかりにくいです。0要素目の0ビット目、0要素目の1ビット目、…という順に見ます。整数内では右から左(右が上位ビット)、要素間では左から右(左が0要素目)に見ます。
列方向はenum reg_classの整数値と一致しますのでさほど難しくはないでしょう。
行と列の意味 →→ 行方向、レジスタ番号(0〜FIRST_PSEUDO_REGISTER - 1まで) ↓ ↓ 列方向、enum reg_classを整数に直したもの 行方向の見方 例えば3行目(GR_REGS)がこうなっていたとすると、 { 0x0000000f, 0x0000000c, }, - 0要素目(レジスタ番号0〜31のクラス): 0x0000000f - 0, 1, 2, 3ビット目が1 = レジスタ番号0〜3はGR_REGS - 他のレジスタについては言及しない - 1要素目(レジスタ番号32〜63のクラス): 0x0000000c - 2, 3ビット目が1 = レジスタ番号34〜35はGR_REGS - 他のレジスタについては言及しない
ALL_REGSは全レジスタに1をセットしますので、ビットフィールドのルールがわかりやすいと思います。今回はレジスタが98本なので、3要素(32 * 3 = 96)+ 最後の要素は2ビット分だけ1にセットしています。
今回はVR_REGSという新たなレジスタクラスを足したいので、行が一つ増えます。レジスタの総数も増えるので、列方向も増えます。ちょうど良いことに新規に追加するレジスタは32本なので、整数1要素分を増やすだけです。
このマクロは直接使用されるわけではなく、別の配列にコピーされます。
// gcc/reginfo.c
static const unsigned int_reg_class_contents[N_REG_CLASSES][N_REG_INTS]
= REG_CLASS_CONTENTS;
...
/* Function called only once per target_globals to initialize the
target_hard_regs structure. Once this is done, various switches
may override. */
void
init_reg_sets (void)
{
int i, j;
/* First copy the register information from the initial int form into
the regsets. */
for (i = 0; i < N_REG_CLASSES; i++)
{
CLEAR_HARD_REG_SET (reg_class_contents[i]);
/* Note that we hard-code 32 here, not HOST_BITS_PER_INT. */
for (j = 0; j < FIRST_PSEUDO_REGISTER; j++)
if (int_reg_class_contents[i][j / 32] //★★ここで参照している
& ((unsigned) 1 << (j % 32)))
SET_HARD_REG_BIT (reg_class_contents[i], j);
}
// gcc/reginfo.c
struct target_hard_regs default_target_hard_regs;
// gcc/hard-reg-set.h
#if SWITCHABLE_TARGET //★★x86, ARM, MIPSなどはSWITCHABLE_TARGET = 1, RISC-Vは0のようだ
extern struct target_hard_regs *this_target_hard_regs;
#else
#define this_target_hard_regs (&default_target_hard_regs)
#endif
#define reg_class_contents \r (this_target_hard_regs->x_reg_class_contents)
難しそうに見えてやっていることはint_reg_class_contentsからdefault_target_hard_regs->x_reg_class_contentsへビットを移し替えているだけです。違いはint_reg_class_contentsが必ず32ビット幅であるのに対し、x_reg_class_contentsはアーキテクチャ最速の整数幅(x86_64なら64bitになるでしょう)である点です。
個人的には可読性を殺してまでやる意味あるの……?と疑問ですが、きっとGCC内で頻繁に呼ばれ速度的に重要なポイントだったのでしょう。
目次: GCC
インラインアセンブラで "v" constraintsを指定すると、何も実装していない場合はimpossible constraint in 'asm' と怒られました。レジスタのconstraintsだけ足すとinconsistent operand constraints in an asmと怒られるはずです。エラーをチェックしている箇所は、
static bool
curr_insn_transform (bool check_only_p)
{
...
if (process_alt_operands (reused_alternative_num)) //★★これが成立してalt_p = trueが期待値だが
alt_p = true;
...
if (! alt_p && ! sec_mem_p)
{
/* No alternative works with reloads?? */
if (INSN_CODE (curr_insn) >= 0)
fatal_insn ("unable to generate reloads for:", curr_insn);
error_for_asm (curr_insn,
"inconsistent operand constraints in an %<asm%>"); //★★ここに到達しエラーが出る
lra_asm_error_p = true;
/* Avoid further trouble with this insn. Don't generate use
pattern here as we could use the insn SP offset. */
lra_set_insn_deleted (curr_insn);
return true;
}
...
このcurr_insn_transform() 関数はやたら長くて(700行)訳のわからない構造です。うまく行く場合(rなどを渡したとき)を観察すると、alt_pがtrueになるのが期待値と思われます。幸いなことにalt_pの設定は一箇所だけ、条件もprocess_alt_operands() 関数だけです。
そう思ってprocess_alt_operands() 関数を見ると、これがまたもの凄い実装で、目を覆いたくなります(1200行!!)。GCC見ていると、クソコードには事欠かないです。これはひどい。
コードの一部を抜粋しても全く意味不明で、そもそもこの関数自体がかなりゴチャゴチャで意味不明です。全て追うのは不可能です。なので"r" がどの辺りを通るかをもって、当たりを付けました。下記のところが分岐点になっているようです。
static bool
process_alt_operands (int only_alternative)
{
...
do
{
//★★pは "=&v" が入っていて、cに先頭から一文字ずつ取って解析している
switch ((c = *p, len = CONSTRAINT_LEN (c, p)), c)
{
case '\0':
len = 0;
break;
...
default:
cn = lookup_constraint (p); //★★ 'v' に対しては、CONSTRAINT_vが返る
switch (get_constraint_type (cn))
{
case CT_REGISTER:
cl = reg_class_for_constraint (cn); //★★CONSTRAINT_vに対してはVP_REGSが返る
if (cl != NO_REGS)
goto reg; //★★このジャンプで飛ぶ
break;
...
reg:
if (mode == BLKmode)
break;
this_alternative = reg_class_subunion[this_alternative][cl];
this_alternative_set |= reg_class_contents[cl]; //★★どこかでみたreg_class_contentsが登場
if (costly_p)
{
this_costly_alternative
= reg_class_subunion[this_costly_alternative][cl];
this_costly_alternative_set |= reg_class_contents[cl];
}
winreg = true;
if (REG_P (op))
{
if (hard_regno[nop] >= 0
&& in_hard_reg_set_p (this_alternative_set,
mode, hard_regno[nop])) //★★これが成立しない
win = true; //★★少なくともwin = trueにならないと関数が失敗を返す(条件は他にもあるが)
else if (hard_regno[nop] < 0
&& in_class_p (op, this_alternative, NULL))
win = true;
}
break;
}
...
}
while ((p += len), c); //★★基本は次の文字に行くが、スキップすることもある模様
どこかでみたアイツです。このエラーはreg_class_contentsを見に行った結末に起きているようです。
REG_CLASS_CONTENTSを正しく設定すると、下記のコードがコンパイルできるはずです。雰囲気を出すためRISC-Vのベクトル命令を書いていますが、ぶっちゃけコンパイラは命令を全く見ないので、実はabcdでも何でも通ります。コンパイルのみ(*.sを出力)であればアセンブラすら要りません(※)。
// a.c
void _start()
{
int b[100];
int v;
__asm__ volatile ("vlw.v %0, %1\n"
: "=&v"(v) : "A"(b[10]));
}
ビルドして、逆アセンブルしてみます。
$ riscv32-unknown-elf-gcc -Wall -g -march=rv32gcv -mabi=ilp32f -nostdlib -O2 a.c $ riscv32-unknown-elf-objdump -drS a.out a.out: file format elf32-littleriscv Disassembly of section .text: 00010054 <_start>: void _start() { 10054: 7165 addi sp,sp,-400 int b[100]; int v; __asm__ volatile ("vlw.v %0, %1\n" 10056: 103c addi a5,sp,40 10058: 1207e007 vlw.v v0,(a5) : "=&v"(v) : "A"(b[10])); } 1005c: 6159 addi sp,sp,400 1005e: 8082 ret
それらしきベクトルレジスタ(v0)が出力されているようです。めでたし、めでたし。と言いたいところですが、実は全然ダメです。
まだまだ改善の余地があります。これも今後、調べていこうと思います。
(※)もしアセンブルまで実行したければ、RISC-VのGitHubにあるbinutilsを使ってください(GitHubへのリンク)。ビルド方法はUpstreamのコードとほぼ同じ(2019年4月19日の日記参照)です。唯一の違いはconfigure時に --with-system-readlineを付けないと、readlineがないと言われてエラーになる点です。
目次: C言語とlibc
標準Cライブラリにはdoubleを返す三角関数(sin(), cos(), tan())とfloatを返す三角関数(sinf(), cosf(), tanf())が定義されています。
標準Cライブラリの一つの実装であるmuslのコードを見ると、sinf, cosf, tanfの計算にdouble演算を内部で使っています。これは基になったFreeBSDのlibmと同じ実装です。PCはfloatでもdoubleでも関係なく速いんですが、doubleをハードで扱えない貧相なプロセッサには優しくない作りです。
もう一つの実装であるnewlibのコードを見ると、double版のsin, cosこそFreeBSDの実装と同じですが、float版のsinf, cosfはfloatを使ったコードが独自に追加されていて、貧相なプロセッサにも優しい作りになっています。組み込みにやたら使われる実績は伊達じゃないですね。
じゃあmuslにnewlibのfloat版のsinf, cosfを移植すれば、doubleが苦手なプロセッサでも速くなるのでは?と思いました。
コードを触る前に、それぞれの実装の素性を調べておこうと思います。テスト方法は、
どうしてtanfだけ判定が甘いかというと、正しい値がわからなかったからです。なぜかglibcの実装も誤差1に収まっていません。どういうことなの……。
テストのコードはGitHubに置き(リンク)ました。特に難しい点はありませんが、muslとnewlibから三角関数を拝借するところは、やや面倒かもしれません。
現在のプロセッサは超速いし、問題の性質上マルチスレッド化も簡単ですから、32並列くらいで頑張れば32bit全域を調査しても3分もかかりません。楽勝ですね〜。
テスト結果は、当然、全て一致かと思いきや、そんなことはなかった。最初にテストしておいて良かったですね。
良い方から言うとmuslは内部でdoubleで演算しているからか、結果もパーフェクトでした。
一方のnewlibはcosfだけ変な値を返します。32bit全域を試してわずか6パターンです。
cos,cosf_newlib: NG : x:3fc90fe0 f:1.570797 d:1, exp:b52bbbd3 -0.000001, res:b52bbbd0 -0.000001 cos,cosf_newlib: NG : x:3fc90fe1 f:1.570797 d:1, exp:b54bbbd3 -0.000001, res:b54bbbd0 -0.000001 cos,cosf_newlib: NG : x:3fc90fe2 f:1.570797 d:1, exp:b56bbbd3 -0.000001, res:b56bbbd0 -0.000001 cos,cosf_newlib: NG : x:bfc90fe0 f:-1.570797 d:1, exp:b52bbbd3 -0.000001, res:b52bbbd0 -0.000001 cos,cosf_newlib: NG : x:bfc90fe1 f:-1.570797 d:1, exp:b54bbbd3 -0.000001, res:b54bbbd0 -0.000001 cos,cosf_newlib: NG : x:bfc90fe2 f:-1.570797 d:1, exp:b56bbbd3 -0.000001, res:b56bbbd0 -0.000001
正負を考慮(浮動小数点は最上位ビットが符号を示すビット)すると、実質3パターンで変な値が返ることがわかります。
誤差は3でした。ほぼ合ってます、おしい。誤差が出ることも不思議ですが、sinfは合っていてcosfだけ値がズレるのも不思議です。
Facebookのエディタはめちゃくちゃ重くて、添付の画像のように表示がおかしくなったり、突然カーソル位置が先頭に吹っ飛んだり、変な動きばかりします。
文章が長い場合は、他のテキストエディタで書いて張り付けた方が良いですね。
これはFacebookに長い文章を書くんじゃねえよ、というFacebookの意思なのかな……。
メモ: 技術系の話はFacebookから転記しておくことにした。
目次: ゲーム
最近Steamで購入したTransport Fever 2(Good Shepherd Entertainment/Urban Games)というゲームにハマっています。在宅勤務を良いことに、仕事終わった瞬間から深夜までやりまくっていたら、プレイ時間が3週間で130時間に。やりすぎですね……。
Steamでは一定の条件を満たすと「実績」が解除されるシステムがあります。他のゲームでは、実績はほとんど気にしませんが、このゲームでは気になり59/61まで取りました。
もう1つ取れますが、最後の1個「大都市」は難しそうです。
Transport Fever 2を軽く紹介すると「輸送」シミュレーションゲームです。
シムシティから街を作る機能を除いて、交通網を作る機能のみを超強化したゲームとでもいいますか。A列車で行こう(アートディンク)をご存知でしたら、あのイメージが一番近いです。違いとしては、
プレイヤーが出来るのは、人と荷物の「輸送」のみです。輸送すると運賃が貰えるので、輸送網の維持費を上回る運賃を得て、輸送網を拡大するのが基本です。
ゲームの難易度は簡単です。人でも荷物でも同じですが、運びたいものを、運びたいところに真っ直ぐ運ぶ(運賃は道のりではなく、直線距離に応じる)だけで基本黒字です。めちゃくちゃしない限り、財政破綻はないはず。
むしろ序盤を過ぎると、資金が有り余って使い切れなくなるほどです。箱庭系ゲームでは、難易度が低いのは良いことで、資金を気にせず、レイアウトに凝ってみたり、道路縛りとか、鉄道縛りとか、比較的自由に遊べます。
その他の機能としては、動いている列車や車などのコックピットビューができます。自分が引いた鉄道や道路を走る様子をぼーっと見るのも楽しいです。
Steamでは素敵なModがたくさん公開されているので、お気に入りのマップや列車を探してみるのも楽しいかもしれません。
目次: ゲーム
Transport Fever 2の実績コンプリートしました。やはり最後になったのは「大都会」で、何度かやり直す羽目になって、とても時間が掛かりました。
都市のターゲット人口が271 +470% で1500人達成できました。+460% = 1518だとダメでした。基本的には都市の人口はこのターゲット人口に近づこうとするんですが、時に越えたり時に越えなかったりします。良くわからない数字です。
フリーマップはランダム生成なので、必勝法は無いです。ただまあ、何回かやり直す中で最初にこだわっておいた方が良いなと思った条件は、
< | 2020 | > | ||||
<< | < | 03 | > | >> | ||
日 | 月 | 火 | 水 | 木 | 金 | 土 |
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 | - | - | - | - |
合計:
本日: