詳解Rustプログラミングを読んだ
tags:2021-12-26
ToC
詳解Rustプログラミングを読んだ
GoならわかるシステムプログラミングのRust版、という噂を聞いたので購入
Rustで低レベルのプログラミングについて学べるということで購入
学びたい内容としては、
- Rustの基礎知識で足りない部分の補強
- ライフタイム
- 型システム
- ローレベルの知識
- メモリ
- file system
- thread
- 時間
1~3章メモ
知らなかったことを端的にまとめる。内容が大きいときは節を切る
- loop内のbreakが値を返せる
- 「式」は値を返す。「文」は値を返さない
- 文は「代入」「型宣言」「;の付いた式」が該当する
- &で参照を作り、*でデリファレンスする
- *は暗黙的に省略される時がある
- すべての演算子はtraitで定義される
- 例えば加算(+)はstd::ops::Addに定義されている
- 加算可能な型のtrait境界の例は以下
T: std::ops::Add<Output = T>
- strで表現される値はutf-8であることが保証される
- ただし伸長などの操作は行えない
- strはreadonly、Stringはread&writeなデータというイメージが近いかもしれない
- Stringはヒープ上にデータを格納している
- Stringを所有型(owned type)と表現する
- 所有者はデータを自由に変更できるが、スコープから抜けるときにデータを削除する責任を持つ
- 一方で&strを借用型(borrowed type)と表現する
- 厳密な型は
&'static str
となる
- 厳密な型は
- charは内部的に1文字を4byteで表現する
- UTF-32と等価で、Stringや&strと違ってutf-8ではない
- utf-8でエンコードされると、1~4byteの長さを持つ
- [T; n]は型定義で、Tの要素n個の配列
- [T]はsliceの定義なので混同しないように
- 文字列sliceである&strと同様に、sliceを受けるときは&[T]のようにする
- unit型は長さ0のtuple
- new type patternと呼ばれるものがある
- Stringなどの要素だけを持つtupleもしくはstruct
- 既存の型を一部だけ特別扱いしたいときに使う
- taxの計算で引数に受けられるYen(usize)などで使えそうだ
4章メモ
ライフタイム,および借用チェッカーについて学ぶ
- 値の「ライフタイム」とはその値へのアクセスが有効になっている期間
- 値の「借用」とはその値をアクセスさせてもらう
- 返却の義務はない
- 一つの値を借りれるのは同時に一つまで
- Copyの作成は高速で安価、かつ暗黙的。必ず同一のものができる
- Copyの実装は、&selfを受けて*selfを返すような実装でも可能
- Cloneの作成は遅くて高価かもしれない
- 暗黙的に行われず、.clone()の呼び出しが必要
- Rcなど独自のcloneを実装しているtraitもある
- クローンがオリジナルの複製を返すかは実装に依存するため保証はない
- Rcは参照カウントを実行時に持ち越す
- 型Tの参照カウント
- 共有所有権(shared ownership)とも呼ぶ
- Rcは書き換えを許可しない。書き換えるためにはRc<RefCell>とする必要がある
第二部(5章)
-
多くのものがビットパターンで表現される
- 型システムはこの現実を抽象化したものになる
-
バイナリファイルとテキストファイルの違いの一部は、ビット列と数値とのマッピングにある
- テキストファイルもバイナリファイルの一種で、たまたまビット列と文字列が一貫して対応しているというわけ
- このマッピングをエンコーディング(符号化)と呼ぶ
- テキストファイルもバイナリファイルの一種で、たまたまビット列と文字列が一貫して対応しているというわけ
-
printlnのマクロのディレクティブで、:032bのような表記は、std::fmt::Binaryトレイトが呼ばれる
-
動的型付け言語では、代入時に制度が足りない場合など、自動で型を昇格する仕組みがあるとか?
-
エンディアン
- 最上位は英語でmost significant
- intelが歴史的な勝者で、整数は一般にリトルエンディアンで格納されるそう
-
浮動小数点数
- 以下の4つの登場人物
- 符号。
- 仮数。表現したい数そのもの
- 基数(radix, または底=baseとも)。^の前に書く
- 指数。値の規模を表現する。27なら27乗
- ここで構成要素は以下のようになる
- 符号bit、指数部、仮数部の3つのフィールドを持つ
- 基数は標準規格で2と定義されているので不要
- f32のメモリ上の構造
- 符号は1bit
- 仮数部は23bit
- 指数部は8bit
- 符号bitがあるため、0と-0が存在する
- 指数部のbitがすべて0の時、仮数部は「非正規化数」を表現する
- これで表現可能なゼロに近い小数の数が増える
- 正規化して表現できないような、0にごく近い数
- 最小の「正規化数」と0の間を埋める数値
- 以下のような計算になる
- 符号部 * 0.仮数部 * 2 ^ (1 - bias) // biasは127
- 以下のような計算になる
- 指数部のbitがすべて1の時、その小数は+-いずれかの無限大もしくはNANになる
- 以下の4つの登場人物
-
浮動小数点数を例に、バイト列のバラし方を学んだ
- bitmaskやシフト演算
-
型変換で使うFromは数学的に等値である必要があるそうな
- 失敗する可能性があるならば、std::convert::TryFromを使うべし
-
Rustのモジュールシステム
- crateがモジュールを結合してまとめている
- モジュールはプロジェクトのディレクトリによって定義される
- ディレクトリがmod.rsを含むときにモジュールになる
- モジュールのメンバはデフォルトでprivate
-
CPUエミュレーション
- CPU命令だろうと関数だろうとすべて数字のやり取り、というのがおもろい。当たり前のことではあるけど
- opは演算(operation)のこと。ハードウェア演算とか、instruction(命令)とか言ったりもする
- レジスタ群(registers)はCPUが直接アクセスできるようにデータを格納するコンテナの並び
- オペコードは演算の一つにマップされる数値
- 「ニブル」は4bitの値
- オペコードは「上位バイト」と「下位バイト」の2byteで表現される
- でもってそれらは上位ニブルと下位ニブルの2つで構成される
- u8の変数 <<8 | u8の変数とするとu16で返る。(型キャストなしでエラー出なくてビックリ)
- 関数呼び出しの実装
- 現在のメモリ位置をスタックに格納
- スタックポインタをインクリメント
- 現在のメモリ位置にCALLのとび先のメモリアドレスをセット
- return後の処理はこの逆順
- 16bitの並びでも、先頭のnibbleで処理が分岐する
- 思ったより複雑だった
-
メモリ
- メモリアドレスがusizeとしてエンコードされるのは、仮想のアドレス空間であるから
- メモリアドレスは、メモリ上の任意の1byteを参照する数値
- アセンブリ言語によって提供される抽象
- ポインタはある型の値を指し示すメモリアドレス
- 参照はポインタの一種。サイズが同的な場合は保障のための整数が加わった型
- Rustの提供する抽象
- 生ポインタ(row pointer)はポインタ型の安全じゃない性質を明示することが重要な時に使われる
- printlnなどのformatで、{:p}でメモリアドレスをプリントしてくれる
- std::borrow::CowというCopy on Writeに対応したスマポ?みたいなものがある
- c_charはi8のエイリアスだけど、プラットフォーム固有のニュアンスを含む
- B as *const u8 as *mut u8のように、const Tはmut Tに変換できる
- 生ポインタ
- *const Tはimutable
- *mut Tはmutable
- ポインタとメモリアドレスの違い
- ポインタは必ずTの第一バイトを指し示さなければならず、Tのサイズを知っている必要がある
- メモリアドレスはどこでもよい
- ポインタによってRAMからデータをフェッチするプロセスを「デリファレンス(参照外し)」という
-
Rustのポインタ体系
- スマートポインタ
- BoxやCellなどがあるが、性質として内部可変性、所有権共有に違いがある
- 実行時にコストがかかるが、受け取る値に可変性が必要な時はRc<RefCell>のようになる
- 実行コストの代わりに柔軟性を得られる
- Cowもスマポの一つ。readだけの時はパフォーマンスが上がるかも
- RawVecという動的なサイズを持つ方のbaseにある型も存在している
- Unique,Sharedなど普段使わない型もある
- UniqueはStringやBoxの基礎となる型
- Rc,Arcの基礎はcore::ptr::Shared。共有アクセスが求められる状況を処理できる
- 実行時にコストがかかるが、受け取る値に可変性が必要な時はRc<RefCell>のようになる
- WeakRefにはstr::rc::Weakとstd::arc::Weakがある(後者はマルチスレッド向け)
- CellとRefCellはstd::cell::UnsafeCellをbaseにしている
- BoxやCellなどがあるが、性質として内部可変性、所有権共有に違いがある
- スマートポインタ
-
stack
- スタックのエントリーはスタックフレームと呼ばれ、関数呼び出しとともに作られる
- CPUは自分のカーソルを更新して、現在のスタックフレームを指しているアドレスを反映させる
- このカーソルが「スタックポインタ」
- CPUは自分のカーソルを更新して、現在のスタックフレームを指しているアドレスを反映させる
- スタック上のアクセスが早いのは連続してRAM上に並んでいるから
- これに対してヒープの置き場はランダムなので、ページテーブルをlookupする処理が入る
- スタックのエントリーはスタックフレームと呼ばれ、関数呼び出しとともに作られる
-
heap
- コンパイル時にサイズがわからない型のためのメモリ領域
- データ構造ではなくてメモリ領域である
-
仮想メモリ関連の用語
- ページ
- 実メモリの固定サイズのワードブロック。64bitのOSだと4kbが典型的なサイズ
- ワード
- ポインタのサイズを持つ任意の型。CPUレジスタの幅に対応する
- rustではusize,isizeがワード長の型
- ページフォルト
- 仮想メモリアドレス要求に、その要求に対応するRAMがないときのCPU例外
- スワッピング
- ページメモリをメインメモリから一時的にディスクに保存するスワップアウトおよびその逆のスワップインのこと
- 仮想メモリもしくは仮想記憶
- プログラムから見たメモリの姿
- 実メモリ
- OSから見た物理メモリ
- ページテーブル
- OSによって管理される仮想メモリと実メモリの変換を行う際に使われる情報
- セグメント
- 仮想メモリ内のブロック
- せぐふぉ
- 無効なメモリアドレスが要求された時のCPU例外
- MMU(memory management unit)
- メモリアドレスの変換を管理するCPUコンポーネント
- 直近変換されたアドレス群(TLB、translation lookasize buffer)によるキャッシュを管理する
- ページ
-
elfファイル
- .bssはBlockStarted bySymbolの略称。初期化されないstatic変数が置かれる
- .rodataはread only dataの略称
-
スマートポインタという言葉は、ポインタに機能が追加されたデータ構造
- 空間的なオーバーヘッドがある
- Rustでは機能が多いほどランタイムコストが大きい
-
ヒープとスタックはOSとプログラミング言語によって提供される抽象概念で、実際のCPUには存在しない
-
cbor, bincode
- どちらもデータのフォーマット。jsonよりコンパクトだが人には読めない(機械にしか読めない)
-
hexdump
- ファイルなどのバイトストリームを入力として、それぞれのバイトを2桁の16進数にして読みやすく並べて出力する
- 0x0a(10)は改行文字\nを表す
-
pathの扱い
- std::path::Pathもしくはstd::path::PathBufを使う
- これでOS依存の区切り文字などを意識せずに扱えるとかなんとか
- std::fs::Pathはstd::ffi::OsStr上に、std::fs::PathBufはstd::ffi::OsString上に実装されている
- なのでUTF-8に従う保証がない
- 古のMacの区切り文字では:があったそうな
- Stratus VOSというOSでは>だったらしい。知識
- std::path::Pathもしくはstd::path::PathBufを使う
-
KVSを作るぞ
- 任意の長さのバイトシーケンス[u8]を格納してそれを取り出せる
- EOFの正体はゼロのバイト(0u8)
- エラーじゃなくて読み出しバイト数が0の時はEOF到達を推定する
-
Riak
- NoSQLのDBの一つ。対象外性の高さが特徴。遅いけどデータを失わない
- BitcaskはRiakのオリジナル実装用のストレージバックエンドの実装
-
std::io::Cursor
- Vec上でseekをサポートする。メモリ上のVecがファイルのようにふるまう
-
チェックサムのアプローチ
- パリティビット。簡単で早いけど信頼性が低い(1bit)
- byteストリームの中の1の数の偶奇を返すみたいだ
- CRC32,巡回冗長検査は複雑だけど信頼できる(32bit)
- 暗号学的ハッシュ関数はさらに複雑で低速。しかし保証レベルが高い(128~512bit)
- パリティビット。簡単で早いけど信頼性が低い(1bit)
-
BufWriter
- 書き込む内容をバッファリングでまとめて一回のflushでエイ!してくれる
-
SeekFrom::Current(0)
- 現在の位置にoffsetを0で足した位置
-
trait object
- 動的なサイズを持つ型(dynamically-sized type)
- &dyn Trait, &mut dyn Trait, Box がある
- 種類の異なるオブジェクトのコレクションを作る
-
時間
- 現実世界の時計には、一定周期で時を刻むティック回路と、ティックが発生するたびにインクリメントする一対のカウンタがある
- コンピュータではCPUのクロックが内部的なティックの元になる
- CPUのコアは一定の周波数で動作する。ハードウェア内部のカウンタはCPU命令と専用のレジスタを介してアクセスされる
- 用語
- 絶対時間
- 今何時?の時刻。クロックタイムとかカレンダータイムとも呼ばれる
- リアルタイムクロック
- コンピュータのマザボに搭載されている物理的なクロック。電源がなくても時を刻む
- CMOSクロックとも呼ぶらしい
- システムクロック
- OSの時計
- ブート時にOSはリアルタイムクロックからタイムキーピングの仕事を引き継ぐ
- 単調増加する時間(monotonically increasing time)
- 同じ時刻を二度表現することが決してないということ
- タイムスタンプが重複しないので便利
- システムクロックは単調増加ではない
- 同じ時刻を二度表現することが決してないということ
- ステディクロック
- 二つの保証が得られる
- 秒がすべて同じ長さで単調増加
- コンピュータのブート時に0から開始されて内部カウンタの増加に従ってカウントアップされる
- 現在時刻を知るのには使えないが、所要時間の計測では便利
- 高精度(high accuracy)なクロック
- 秒の長さが一定。クロックの差をスキューと呼ぶ
- なんと原子時計と比べてもほとんどスキューがないらしい
- 高分解能(high resolution)なクロック
- 10ナノ秒以下の分解能を提供するクロック
- 高分解能のクロックがCPUチップに実装されるのが典型的なのは、それほどの高周波を維持できるデバイスが少ないため
- CPUならそれが可能
- 高速クロック(fast clock)
- 時刻の読出しに要する時間が短いクロック
- 代償として精度や分解能が犠牲になる
- 絶対時間
-
時間のエンコード
- 32bitの整数ペアで、秒の整数と小数を表すというもの。小数部の精度はデバイス依存
- 始点は任意だが、1970念の1/1日を始点にするUTCエポックがUNIXで一般的
- 1900や2000の一月一日をエポックとするものもある
- 長所はシンプル。短所は範囲が固定であること、正確ではないこと
- 整数は離散的だが時間は連続的。秒に満たない制度はシステムによって丸め方が違い誤差が生まれる
- なんとアプローチの実装が統一されていないらしい。秒に以下の相違がある
- UNIXのタイムスタンプは32ビット整数でエポックから経過したミリ秒の数を表す
- WindowsのFILETIME構造体はwindows2000以降は64bit符号なし整数で、1601/1/1(UTC)空の経過時間を100ナノ秒単位で示す
- Rustクレートのchronoは32bit符号付き整数で、TZを表すenumとともにNaiveTime構造体を持っている
- Cの(libc)のtime_tはシステムごとに異なる
- 始点は任意だが、1970念の1/1日を始点にするUTCエポックがUNIXで一般的
- 32bitの整数ペアで、秒の整数と小数を表すというもの。小数部の精度はデバイス依存
-
TZの表現
- UTCからのオフセットを表現する
-
経過時間の追跡は難しい
-
時刻の表現もむずい
-
時間は逆戻りすることがある
-
プロセス・スレッド・コンテナ
- 並列化(parallelism)
- 並行処理(concurrency)
- 物理的なCPUコアを同時に使用する