ぱたへね

はてなダイアリーはrustの色分けができないのでこっちに来た

ChatGPTで中国語の勉強をしている

ChatGPTが中国語の勉強にとても役に立っているという話。

読んでいる本は機械学習 周 志華、最近翻訳も出てます。

機械学習 | 周 志華, 大和田 勇人, 玄 光男, 下川 朝有, 郝 新厂, 張 聞強 | コンピュータ・IT | Kindleストア | Amazon

このアンサンブル学習の章で「而基学习器有时也被直接称为弱学习器」という文が出てきて、僕の中国語レベルだとかなり難しい。 更に悪いことに呼ぶ(称する)という意味の称を、あなたという意味の你だと思ってしまって全く意味が取れない。

まずはChatGPTに聞いてみると、間違いを指摘してくれた上で意味を教えてくれる。

ここまでやってもらっても文の構造がわからないので、更に聞いてみる。

これはすごい。

  • 機械学習の文脈と専門用語が分かってる
  • 動詞フレーズなど学習者向けの表現で文法が説明できる
  • ピン音が全部振ってある

これを同時に満たしてくれる中国語の先生は僕の近くにはいないんじゃないかと思う。 勉強はかどる。

エンジニアのためのドキュメントライティング

エンジニアのためのドキュメントライティング読みました。 最近の仕事の悩みに対して、方向性を示してくれた良い本でした。

www.amazon.co.jp

自分のチーム内にドキュメントの文化が無い人にお勧めします。 全体のざっくりした感想だと、この手の本にありがちな小言っぽいことは書いてなく、作って意味のあるドキュメントはどうあるべきかを書いてあります。仕事のドキュメントで悩んでいる人には、具体的な取り組みのアイデアがいっぱい見つかるでしょう。

背景

うちの会社は転職者が多く、開発への考え方がバラバラ。当然、ドキュメントに対する考え方もバラバラで必要なドキュメントがなかなか揃わない。今の仕事は各部署からの寄せ集めチームでもあり、ドキュメントのテンプレートみたいなものもない。

世代間の格差も結構ある。ベテラン勢はドキュメントは印刷され保管される前提であり、表示にバージョン、日付、部署、作成者などを明記するべきだと思ってる。ドキュメントの承認印的な物が欲しい人もいる。一方若い人はドキュメントはVCSに組み込まれる物であって、作成日や作成者の情報はVCSで持っておけば良いという考え方があり、ドキュメントに作成者などを書かない。

若手の言い分もわかるけど、社外に出て行く文章はバージョン管理いるでしょう・・・みたいな悩みを抱えていたところ、この本を見つけました。この前に、Googleのソフトウェアエンジニアリングを読んで、ドキュメントについて考えることがあったけど、googleの本に書いてある事は大概の会社には当てはまらない。Googleのようにドキュメントの作成や管理をスケールさせる意味はまずない。

そんな感じでもやもやしているところでこの本を買って読むことにしました。

良いところ

ドキュメントの作り方みたいな本は、結構小難しいことがかいてあります。ドキュメントには承認プロセスが必要とかなんやらかんやら、用語の定義はこうしろ、フォントはこう使い分けようなんやらかんやら。こういうのは大企業のドキュメント管理を専用にする部署向けであったり正しいドキュメントを書くためのルールだったりします。エンジニアの気持ちやそれが何の役に立つのという観点はあまり考慮されないパターンが多いです。そもそもドキュメントを書きたくないという気持ちから目を背けてます。

この本はスタートアップの会社がWebサービスを公開するというシチュエーションでドキュメントの書き方と運用について書かれています。アメリカの本にありがちな謎の寸劇っぽい部分がすごく短く読みやすいです。堅い本に比べると、なぜ正確なドキュメントが必要なのかという点は説明少なめです。まあ、この本を読んでいる時点で何かしらの課題を持っているはずなので、ドキュメントの重要性をくどくどと書いてないのは良い。

書く側の気の持ちよう的な所が書いてあって、レビューする側、される側の心構えも書いてありました。作成したドキュメントがどれだけ役に立ったかの計測についても結構な量があり、お堅い本にはでてこないトピックで勉強になりました。

とにかく「ドキュメントを書く」事を目的とせず「ドキュメントを運用する」事にフォーカスされています。

関心した所

ドキュメントを書くに当たって

ドキュメントは開発していく物。コードと同じ。 大事なのは一貫性。用語、見出し等、一貫性があると読みやすい。 ドキュメントは流し読みしやすいように書く。 一回で良い物を作ろうとしない、文章で悩むくらいならレビューしてもらって他人に指摘してもらった方が早い。

手順書

一度開発者が正確に書いてから、ユーザー目線で改善していく。このやり方を作成者も意識していると、頑張って書いた手順書に対してわかりにくいという指摘も受け入れやすい。ユーザーの立場で正確に手順を書くというのはできないので正確に書くのは開発者、その後分かりやすい手順書にするのはユーザーの立場の人という分担は直ぐにでもチームに投入したい。

API

APIのドキュメントは大事だ。この本はWebサービス前提だが、何か開発した物に対して使い方がわかるドキュメントが大事なことはWebサービスに限らない。

APIのドキュメントを書くための専用のトレーニングコースまである。 https://idratherbewriting.com/learnapidoc/

サンプルコード

サンプルコードは、説明したいこと(もしくは動かしたいこと)に特化する。小粋なアルゴリズムは不要。 クラス名や変数名は、your_password等普段使わない説明的な名前をつかってもOK。

まとめ

本の章立てはドキュメントを作るところが前半、後半は運用について書かれています。 今、自分のプロジェクトでドキュメントについて悩みがあるなら、該当する箇所、もしくは今後ぶつかるであろう課題について書いてあります。

ドキュメントもチームの文化なので、良いチーム文化を作るためにも、みんなで納得しながらドキュメント作りたいですね。

自作コンパイラで演算子の優先順位をつけた

自作コンパイラでC言語の演算子に優先順位をつけました。

これの続き

natsutan.hatenablog.com

低レイヤを知りたい人のためのCコンパイラ作成入門には詳しい説明がなく、テキストの範囲ではバイナリオペレータに優先順位はほとんどついてないと思います。足し算よりかけ算優先くらい。 手持ちの教科書にあんまり詳しく載っていないのは、足し算とかけ算の優先順位付けられるなら後は分かるでしょって事だと思う。

2023/03/18追記 すいません。理解してから低レイヤを知りたい人のためのCコンパイラ作成入門を見たら、ちゃんと優先順位ついてました。論理AND等が無いこととごっちゃになってました。

自作コンパイラで優先順位がつくようになったでまとめました。

そもそも優先順位をどうつけているか

かけ算と足し算の優先順位付けは、この文法から来ています。

expr : term { + term }
term : factor { * factor }
factor : ID | '(' expr ')'

一般的にk個の優先順位をつけるためには、非終端のルールがk+1個必要になります。かけ算と足し算で2つの優先順位があるので、非終端の文法が3つ必要になります。

C言語の演算子

Retargetable C Compilerに実装方法が載ってました。

www.amazon.com

C言語の演算子の優先順位は15段階になっています。 パース関数については後述。優先順位は値が大きい方(表の下)が優先順位が高い。

優先順位 結合 演算子 パース関数
1 , expr
2 = += -= *= /= %= &= ^= |= <<= >>= expr1
3 ?: expr2
4 || expr3
5 && expr3
6 | expr3
7 ^ expr3
8 & expr3
9 == != expr3
10 < > <= >= expr3
11 << >> expr3
12 + - expr3
13 * / % expr3
14 * & - + ! ~ ++ -- sizeof (type-cast) unary
15 ++ -- postfix

今、バイナリオペレータの優先順位をつけたいので、優先順位4から13までを処理したい。 ざっくりというと、優先順位が15レベルあれば文法を16個作れば優先順位をつけてパースできる理屈になります。

優先順位の数だけ関数を作っても良いけど、ほとんど同じ内容の関数がたくさん出てきて面倒だよねということで、Retargetable C Compilerにでてくるパーサは優先順位4から13までを、expr3という一つの関数で捌いています。

LCCの実装

Retargetable C Compilerに出てくるLCCの実装はこうなっています。 exprで再帰するときに優先順位を渡して、その優先順位を使って一つの関数でパースしています。

void expr(int k) {
 if (k > 13) 
     factor();
  else {
    expr(k+1);
    while(prec[t] == k) {
      t - gettok();
      expr(k + 1);
  }
}

precが優先順位のテーブルになっていて、prec['+']だと12を返します。

なるほど、わからない。

実装してみた

F#でwhileを使うのが苦手なので、愚直に文法増やして対応しました。 こんな感じです。

 expr = logicaland { || logicaland }
 logicaland = bitwiseor { && bitwiseor }
 bitwiseor = bitwisexor { | bitwisexor }
 bitwisexor = bitwiseand { ^ bitwiseand }
 bitwiseand = equality { & equality }
 equality = relational { == relational }
            relational { != relational }
 relational = shifting { < shifting }
              shifting { > shifting }
              shifting { >= shifting }
              shifting { <= shifting }
 shifting = additive { << additive }
            additive { >> additive }
 (省略)
 factor = num
        | ( expr )

ソースのコピペ連発でしたが、低レイヤを知りたい人のためのCコンパイラ作成入門の経験から、ここは一度作ればあとから直したくなることがないと判断しました。まずは動く物を作ろう。

こうやって文法を足した結果、こういう計算がgccの結果と一致するようになりました。

int main(void) {
  putd(15 << 2 == 15 >> 2);
  return 0;
}

次は変数とアサインの実装。

ソースはここ

github.com

自作コンパイラで基本的な演算子が動いた

自作コンパイラでRISC-VのC言語の基本的な演算子が動くようになりました。

これの続き

natsutan.hatenablog.com

こんなCソースで動作確認しています。

int main(void) {
  putd(2>=4);
  return 0;
}

低レイヤを知りたい人のためのCコンパイラ作成入門に書いてないところ中心にまとめました。

www.sigbus.info

動いた演算子

<< >>

右シフト、左シフトはそのまんまの命令があるので使う。 C言語で負の値の右シフトは処理系依存。符号が残る方が好きなので算術シフト sra を使いました。

            | BinOpKind.LShift -> fp.WriteLine " sll a0, a0, a1"            
            | BinOpKind.RShift -> fp.WriteLine " sra a0, a0, a1"

& | ^

これも対応する命令がある。

            | BinOpKind.BitAnd -> fp.WriteLine " and a0, a0, a1"
            | BinOpKind.BitOr -> fp.WriteLine " or a0, a0, a1"
            | BinOpKind.BitXor -> fp.WriteLine " xor a0, a0, a1"

&& ||

bit演算をした後、結果を0と比較する。 結果が0でなければ1、結果が0ならば0を返す。

            | BinOpKind.LogicalAnd ->
                fp.WriteLine " and a0, a0, a1"
                fp.WriteLine " snez a0, a0"
            | BinOpKind.LogicalOr ->
                fp.WriteLine " or a0, a0, a1"
                fp.WriteLine " snez a0, a0"

2023/03/15修正 &&は間違っていた。こっちが正解。 一旦両オペランドを0か1にしてからandを取る。

            | BinOpKind.LogicalAnd ->
                fp.WriteLine "  snez a0, a0"
                fp.WriteLine "  snez a1, a1"
                fp.WriteLine "  and a0, a0, a1"
                fp.WriteLine "  snez a0, a0"

>= <=

ちょっと頭を使った。 RISC-Vの比較命令は、slt (set lesser than)しかない。 greater thanを使いた時は、左右のオペランドを入れ替える。

問題は>=と<=。引き算して0以上とかで行けるかなと思ってパターソン先生の The RISC-V Readerを眺めていたら、<=は、>の否定なので専用の命令は要らないと書いてあって納得。

riscvbook.com

seqz a0, a0が、a0が0ならa0を1に、a0が0以外ならa0を0にしてくれる。

            | BinOpKind.LesserThan -> fp.WriteLine " slt a0, a0, a1"
            | BinOpKind.LesserEqual ->
                fp.WriteLine " slt a0, a1, a0"
                fp.WriteLine " seqz a0, a0"
            | BinOpKind.GreaterThan -> fp.WriteLine " slt a0, a1, a0"
            | BinOpKind.GreaterEqual ->
                fp.WriteLine " slt a0, a0, a1"
                fp.WriteLine " seqz a0, a0"

%

これもそのまんまの命令があるので使う。

            | BinOpKind.Modulo -> fp.WriteLine " rem a0, a0, a1"

この後

基本的な演算子が動くようになったが、優先順位が全然ついてないので次は優先順位をつけていく。

ソースはここ

github.com

ゼロからのOS自作入門

ゼロからのOS自作入門を1年かけて読みました。

book.mynavi.jp

長い時間かかりましたが、それだけの価値がある本です。 OSに限らずCPUや周辺デバイスの制御を手を動かしながら勉強したい人にお勧めです。

C++のソースコードが読みやすく、解説も詳しいです。ただ、扱っている内容が高度なのでちょっとよく分からないところがでてくるのはしょうがないと感じました。ただ、一回は動かしたという自信がつくので、今後何か必要な事が出てきたら、確実にこの本に戻ってこれます。

これからもずっと本棚に居続けるそんな本になりました。

本の内容

ブートローダーから始まって、GUI付きのOSを作っていきます。タスク(プロセス)の管理、入出力、ファイルシステム、GUIとパソコンっぽい機能が充実しています。最低限動くところをC++のソース付きで解説してあるのですが、その最低限の所がかなり上の方にありました。楽しいところだけ作ろうというのは感じるのですが、やっぱりOSとして動かしていく難しさがあって、それを一つ一つ乗り越えていきます。

1章~3章

開発環境の準備と、ブートローダを作り自作のカーネルが起動するまで。 UEFIを使った開発と画面の描画ができるようになります。

4章~5章

文字が書けるようになります。1文字を描画するWriteAscii関数は最後までお世話になります。 文字を書くためのフォントもここで導入します

6章~8章

最初の壁。PCIとUSBを動かします。マウスを動かしながら、割り込みやメモリマネージャを作ります。 CPU側ではセグメントの設定などアセンブラを使ってスタックの操作を行います。 ここはファイルごとコピーで分かったつもりで先に進んで良いと思います。 特にUSBの方は難しかった。

9章~12章

画面の表示を綺麗にしたり、タイマーやキーボードからの入力を行います。 目に見えてできることが増えて楽しくなってくるところです。

13章~14章

いよいよマルチタスクの導入です。ここもアセンブラが出てきます。 擬似マルチタスクから本当のマルチタスクへ。

15章~18章

ターミナルを作りながら、OSのユーザー寄りの機能を増やしていきます。 FATを使ったファイルシステムを導入し、lsコマンドを作ります。 ここからOSとアプリでソースコードが分かれてそれぞれ別々にコンパイルして動かします。 アプリ側ではcstring等の標準ライブラリの一部が使えるようになります。

19章

ページングの導入。ここも難しい。

20章

20章でシステムコールが導入されます。ここも難しい。 システムコールを導入するとともに、今までOSと同じメモリ空間で動いていたユーザーアプリをOSから分離していきます。

21章以降

21章から後は、新しい機能を追加しながら、システムコールが順番に増えていきます。 システムコールを増やす度にできることが増えていって楽しいところです。厳密にはOSではないのですが、自作OSでprintfが動いたのは感動しました。 メモリのコピーオンライトの実装がクライマックス感ありました。日本語の表示もできるようになります。UTF8のフォーマットもここで理解しました。

最後、画像ビューワーを起動しておしまいです。

写経

基本的にはソースコードをダウンロードして、本とソースを見ながら写経していきました。 USBの所で一度挫折して、難しそうな所は写経せず最初からコピペにしたらなんとか最後までいけました。 写経するかどうかは写し間違えたとき自分でデバッグできるかどうかを基準にしていました。

1日30分(1ポロモード)と時間を決めて写経を続けていて、9月からは毎日30分写経していたので終わりかなと思うとさみしいです。この本のC++はトリッキーな記述もなく、新しく使うC++の機能には簡単な説明もあります。最初から最後まで一貫したルールで書かれていて、毎日の写経がC++のコーディングスタイルを確認するような感じでした。

実装で関心したところはファイル周りの継承です。ファイルシステム上のファイルを読む、キーボードから文字を読み込む、パイプで別プロセスからの出力を読み込む、これらの全然実装が違う動きが同じように扱えファイルディスクリプタで一元管理されているのは実際に動かして驚きました。実務の継承もこれくらい綺麗に書けるとうれしい。

C++ちょっと勉強したけ複雑なプログラムをどこから作れば良いのか分からない人は、この本を写経すると良いでしょう。プログラミングの入門書に載っていないような、リアルな開発の進め方が体験できます。まずはmainにべた書きで一つだけ動きを確認する、一つ動いたら次を確認する、ある程度動いたところで別ファイルへ移動してI/Fを決め、それ以降はそのI/Fで動かす、といったループを何回も何回も繰り返します。機能はそのままでソースファイルを一気に整理するタイミングもあります。ここまでしなくても1章の最終形だけを解説しても十分なのに、全ての機能でこのステップを踏んでいるのが執筆大変だったと思います。

ソースコードは本の内容に合わせる形で、全てのタイミングでダウンロードできます。

github.com

デバッグで困ってにっちもさっちもいかなくなったら、ダウンロードした所から再開すれば良いので、挫折しかけても最後まで頑張れます。僕は、USBの所とページングの所がどうにもこうにもならなくなって、関係ありそうな所をファイルごと上書きして先に進みました。

USBはいつか自力実装したいところです。

最後

最後は、こういう画像ビューワーを作っておしまいです。教科書的な機能よりは見て楽しい機能を優先して実装すすめていきます。 ウィンドウを閉じる×ボタンも機能するのは最終日でそれまではただの絵なのですが、最初からいかにも動きそうな感じで描画されていて遊び心を感じました。

最初にこの本の事を書いたのが去年の三月なので、途中中断しながらも最後まで動かすのに一年間かかりました。

natsutan.hatenablog.com

もう一つ最後

本の最後に素敵な言葉があったので紹介します。

OSを作るにはコツがあります。それは、最初から完璧に作ろうとしないことです。最初から完璧を目指すと手が止まってしまって全然前に進まなくなります。

ちょうど今Cコンパイラを作っていて、OSの部分をCコンパイラに読み替えて元気もらいました。 素敵な本をありがとうございました。

自作コンパイラでRISC-Vの四則演算ができた

自作コンパイラでRISC-Vの四則演算ができるようになったのでまとめてみた。

RISC-Vで乗算命令を使う実績解除。

これが僕より先に僕が書いたコンパイラの実績解除になっていて感動している。

前回の話

natsutan.hatenablog.com

Cソース

こんな感じの四則演算ができるようになりました。

int main(void) {
  putd(5+6*7);
  return 0;
}

AST作るとこまでは問題なし。

RISC-V対応

演算部分

a0に左オペランド、a1に右オペランドが来るようにして、演算結果をa0に入れていいく。

        match binop.op with
            | BinOpKind.Add -> fp.WriteLine "  add a0, a0, a1"
            | BinOpKind.Sub -> fp.WriteLine "  sub a0, a0, a1"
            | BinOpKind.Mult -> fp.WriteLine " mul a0, a0, a1"            
            | BinOpKind.Div -> fp.WriteLine " div a0, a0, a1"            
         

push, pop

5+6x7の部分で、一度5をどこかに保存しないと6*7の演算でa0が書きされてしまう。RISC-Vは死ぬほどレジスタあるので手書きならどうでもなるけど、レジスタの割当はまだまだ先の話なのでスタックを使うようにする。

RISC-Vにはpush、popが無いので命令を組み合わせて作る。 スタックポインタを直接操作するのが20年ぶりくらいなので自信ないけどあってる気がする。

let push (fp : StreamWriter, reg) =
    stack_count <- stack_count + 1
    fp.WriteLine "# push %s{reg}"
    fp.WriteLine "  addi sp, sp, -8"
    fp.WriteLine $"  sd %s{reg}, 0(sp)"
    

let pop(fp : StreamWriter, reg) =
    stack_count <- stack_count - 1
    fp.WriteLine "# pop %s{reg}"
    fp.WriteLine $"  ld %s{reg}, 0(sp)"
    fp.WriteLine "  addi sp, sp, 8"

アセンブラ

出力されるアセンブラ。 絶対いらんやろみたいなpush, popが入っているけどこの辺を削っていくのももっともっと後の話。 まずはいろいろ動くようにしたい。

細かいところで、mainの戻り値を0を返すようにした。 pythonからテストをするときに0以外の値を返すと、外部プロセスの起動に失敗したのか、成功して変な値を返してきたのかわからなくなるため。

.globl main
.text
main:
# Prologue
  addi    sp,sp,-16
  sd      ra,8(sp)
  sd      s0,0(sp)
  addi    s0,sp,16

  li      a0, 7 
# push a0
  addi sp, sp, -8
  sd a0, 0(sp)
  li      a0, 6 
# pop a1
  ld a1, 0(sp)
  addi sp, sp, 8
 mul a0, a0, a1
# push a0
  addi sp, sp, -8
  sd a0, 0(sp)
  li      a0, 5 
# pop a1
  ld a1, 0(sp)
  addi sp, sp, 8
  add a0, a0, a1
  call putd
# Epilogue
.L.return.main:
  li      a0, 0
  ld      ra,8(sp)
  ld      s0,0(sp)
  addi    sp,sp,16
  jr      ra

Amazonでまた詐欺にあった

Amazonで洋書を買ったら詐欺にあった話。

欲しかった本

www.amazon.co.jp

The Little シリーズのDeep Learning版ですよ。欲しい。

買うとき思わず安くて早い方を選んでしまった。

届いた本。表紙は書影を印刷したもの。雑

薄い。下に見えるのがシャープペンシル。

中身全然関係ない。

すでにAmazonのページはない。完全にやられた。

この前Kindleでも騙されたので、Amazonで詐欺にあうのは二回目である。

www.amazon.co.jp

Amzaonは紙の本だけはちゃんと届くと思っていたが考えを改めた。 洋書はAmazon.comのAmazon発送から買った方が良いかも。高いけど。