kawasin73のブログ

技術記事とかいろんなことをかくブログです

Rust で SQLite を再実装した 2023

気合いで実装、どうもかわしんです。

この記事は Rust Advent Calendar 2023 の6日目情報検索・検索技術 Advent Calendar 2023 の 6 日目です。

Rust で SQLiteフルスクラッチで実装しています。

github.com

なぜ SQLite を Rust で再実装しようと思ったのかについては以前の記事で紹介しています。一言で言えば、誰も Rust で SQLite を書いている人がいなかったからやってみたのですが、そもそも SQLite が強すぎるということが再実装しているうちにわかってきて絶望しています。

kawasin73.hatenablog.com

4 ヶ月前にこの記事を書いたときは簡単な SELECT 文しか実行できなかったのですが、現時点では SELECT, INSERT, DELETE 文をサポートし、expression についても比較などの一部をサポートしています。こんな感じで CLI から利用することもできますし、ライブラリとして組み込むこともできます。

$ git clone https://github.com/kawasin73/prsqlite.git

$ cd ./prsqlite

$ sqlite3 tmp/sqlite.db
sqlite> CREATE TABLE example(col1, col2 integer);
sqlite> CREATE INDEX i_example ON example(col2);
sqlite> INSERT INTO example(col1, col2) values(null, 1);
sqlite> INSERT INTO example(col1, col2) values(10, 2);
sqlite> INSERT INTO example(col1, col2) values(1.1, 3);
sqlite> INSERT INTO example(col1, col2) values('Hello prsqlite!', 4);
sqlite> INSERT INTO example(col1, col2) values(X'707273716c697465', 5);
sqlite> .quit

$ cargo build && ./target/debug/prsqlite tmp/sqlite.db
prsqlite> SELECT * FROM sqlite_schema;
table|example|example|2|CREATE TABLE example(col1, col2 integer)
index|i_example|example|3|CREATE INDEX i_example ON example(col2)
prsqlite> SELECT * FROM example;
|1
10|2
1.1|3
Hello prsqlite!|4
prsqlite|5
prsqlite> SELECT col1 FROM example WHERE col2 == 4;
Hello prsqlite!
prsqlite> INSERT INTO example(col1, col2) VALUES (123, 6);
prsqlite> INSERT INTO example(rowid, col2) VALUES (20, 20);
prsqlite> INSERT INTO example(rowid, col2) VALUES (6, 6);
the rowid already exists
prsqlite> INSERT INTO example(rowid, col2) VALUES (7, 7);
prsqlite> INSERT INTO example(col1, col2) VALUES ('hello', 21);
prsqlite> SELECT rowid, * FROM example WHERE col2 >= 6;
6|123|6
7||7
20||20
21|hello|21
prsqlite> DELETE FROM example WHERE col2 < 20;
prsqlite> SELECT * FROM example;
|20
hello|21
prsqlite> DELETE FROM example;
prsqlite> SELECT * FROM example;
prsqlite> .quit

今回は SQLite を再実装している上で頑張ったところを紹介していきたいと思います。

相互互換性

prsqlite は SQLite で生成したデータベースファイルで動くのはもちろん、prsqlite で生成したデータベースファイルでも SQLite が正しく動くようにすることを目指しています。そのため、SQLite のファイルフォーマット などのドキュメントや、SQLiteソースコードを読んでどのような挙動にするべきかを確認しながら実装しています。

Zero dependency

依存ライブラリを増やせば増やすほど自分のプロダクトは不安定になります。そのため、prsqlite では Rust の標準ライブラリ以外は使わないことにしています。逆にRust の標準ライブラリはそれなりに充実していて、OS ごとのファイルシステムの抽象化がされているので便利です。現在は開発のしやすさから例外的に anyhow というエラーの便利ライブラリを使っていますが、そのうち anyhow も独自のエラー型で置き換える予定です。

本家の SQLite ではもっと過激で、依存しているのは memcmp() などの 10 個の関数のみです。それを 自慢するドキュメント があります。printf() すら自作しています。残念ながら SQLite 独自のフォーマット記号があるので再実装の難易度が跳ね上がります。浮動小数点数の文字列変換 を試みましたが一旦諦める羽目になりました。

外部ライブラリを使わないので、全て手書きしています。SQL のパーサー (token.rs, parser.rs) やファイルフォーマットのシリアライザ・デシリアライザ (btree.rs, cursor.rs, record.rs) 、ページ管理 (pager.rs) 、簡単なクエリプランニング (query.rs) なども全部自作のものです。

No unsafe

Rust の大きな特徴の一つにメモリ安全があり、それゆえに Rust で書かれたコードはセキュリティ的な評価が高くなります。(The Rule Of 2)

Rust には unsafe という機能があり、そのブロックの中ではメモリ安全の検証をスキップすることで Rust の borrow checker には違反するが実際にはメモリを破壊しないコードを書くことができます。それによって複雑すぎるコードや無駄なチェックのない効率的なコードを書くことができます。(例えば copy_nonoverlapping () vs <[u8]>::copy_from_slice()) しかし、unsafe を使うことでプログラムの一部にコンパイラによってメモリ安全性が保証されていない部分ができてしまうので、unsafe のないライブラリには一定のセキュリティ的な価値があります。

prsqlite は今の所 unsafe を使わずに全てのコードが書かれています。全てのコードは Rust の borrow checker のルールに従って書かれているということです。これがなかなかしんどくて、本当は正しいコードも現行の Rust の borrow checker では弾かれてしまうこと (特にループが絡むと捕捉しきれないみたいです) があり、それを回避するために工夫する必要がありました。例えば query::Query::next()) はループの中から直接値を返すことができるはずなのですが、borrow checker はなぜかそれをエラー判定するので、一旦ループから抜けて値を返すようにしています。実際に次世代の Polonius という borrow checker で試してみるとループ内から値を返してもエラー判定はされません。

Pager はハッシュマップに保存されていますが、読み込みと書き込みの両方に対応するために RefCell<HashMap<PageId, Rc<RefCell>>> というちょっと複雑なデータ構造になっています。また、参照の作成のたびに内部のカウンターのチェックを行うので少しオーバーヘッドがあります。このオーバーヘッドはメモリ安全を完璧に保証するためには仕方ないのですが、なるべく参照の作成の回数を少なくするようにして対処しています。

テスト

あとで手戻りをするのが嫌なので、テストはたくさん書いてます。コンポーネントごとのユニットテストもですし、全体の統合テストも書いています。体感 3 分の 2 はテストな気がします。

将来的には本家の SQLite のテストケース も流用できたらいいなと思っていますが、まだサポートしている SQL 文の種類が少ないのでできていません。

なるべく速い実装

パフォーマンス改善は後回しにしても、パフォーマンス改善するときはなかなか来ないですし、実際に改善しようとしてもどこをすればいいかを探すのは大変です。そのため、実装の複雑さが増さない限り最初から最適なコードを書くことが大切です。

僕は速い実装にするために この記事 でも紹介したように以下の 2 つのことを気をつけています。

まずは、無駄なことをしないことが大切です。メモリコピーの量やメモリアロケーションの回数もなるべく少なくするべきです。

次に、条件分岐を避けることです。条件分岐は分岐予測が外れたときのペナルティが大きいのでなるべく条件分岐をせずにコードが書けると良いです。僕はなるべく if 文を使わずに書くことができないかを意識しています。SQLite では、ページのヘッダサイズの計算を以下のように行なっていますが、

first = hdr + ((flags&PTF_LEAF)==0 ? 12 : 8);

prsqlite では以下のようにして条件分岐をなくしています。

    /// The btree page header size.
    ///
    /// * Returns 8 if this is a leaf page.
    /// * Returns 12 if this is an interior page.
    ///
    /// This does not invoke conditional branch.
    pub fn header_size(&self) -> u8 {
        // 0(leaf) or 8(interior)
        let is_interior = (!*self.pagetype()) & LEAF_FLAG;
        // 0(leaf) or 4(interior)
        let additional_size = is_interior >> 1;
        8 + additional_size
    }

Btree の実装

SQLite の実装の中で一番めんどくさかったのは、btree の実装です。特に、データの挿入と削除です。ページの分割や削除が異様にめんどくさいです。

SQLite のおもしろ仕様 (2) : ファイルフォーマット でも紹介したように、SQLite ではインデックスは B 木 を、テーブルは B+ 木を使っています。そのため、微妙に実装が違う部分と共通する部分があります。

また全てのデータは可変長です。そのため、ページ内に幾つのセルが保存されるかは動的ですし、セルのサイズも動的です。ページを分割した時に中間のキーが何番目のセルになるのかは詰め直さないとわからないですし、中間のキーが親のページに収まるかも詰めてみないとわからないです。

最悪なのは、データの挿入でページを分割するときに 3 つのページに分かれてしまうことすらあります。

最後に

だんだん実装が大変になってきて飽きてきましたが、時々頑張ります。本家の SQLite のテストケースを流せるようになるのは大きなマイルストーンだと思うのでそこまで頑張りたいです。

SQLite のおもしろ仕様 (2) : ファイルフォーマット

後方互換性って辛いね、どうもかわしんです。

最近 Rust で SQLiteフルスクラッチで再実装しています。

github.com

再実装するために SQLite の公式ドキュメントやソースコードを読み込んでいるわけですが、その過程で気付いたおもしろポイントを共有しようかと思います。

今回はその第二弾、ファイルフォーマット編です。第一弾はこちら:SQLite のおもしろ仕様 (1) : データ型 - kawasin73のブログ

前提知識 : ページ

まず、この記事を面白いと思ってもらうための前提知識です。

大抵のデータベースはデータを保存するファイルをページという単位で管理します。SQLite ではデフォルトでは 1 ページ 4096 バイトです。これは、ファイルを保存するデバイス(HDD や SSD など)としてブロックデバイスを想定しているからです。ブロックデバイスとはデータの読み書きをブロック単位で行う、1 バイト単位のアクセスができないデバイスです。ブロックのサイズは 512 バイトだったりもっと大きかったりしますが、2 の冪乗のサイズです(例外があるかは知りません)。1 ページには 1 つ以上のブロックがピッタリ収まります。

多くのデータベースシステムでは I/O レイテンシーが支配的になるため、ディスクへのアクセスを最適化するためにページの境界を超えたデータの保存はしません。全てのデータアクセスの単位はページ内に収まるように設計されています。

可変長のデータの保存の仕方

第一弾 で紹介したように SQLite ではそれぞれのカラムにどの型のデータが来るかは実際に INSERT されるまでわかりませんし、同じカラムでも別の行では異なるデータ型になることもあります。つまり、ページ内に保存されるセルは全て可変長です。そのため、1 ページに何個のセルを保存できるかは動的に決まります。

SQLite のフォーマットではセルをページの後から先頭に向かって保存していき、それぞれのセルの先頭のオフセットをセルポインタとしてページの先頭から後ろに向かって保存していきます。

セルポインタは 2 バイトなので、X 番目のセルポインタの位置は一撃でわかり、ビッグエンディアンエンコードされた値を読み取ればセルのオフセットも一撃で取得することができます。

もしセルサイズ + 2 バイトの余裕がそのページにない場合はそのページは満杯になったとして処理をします。

このシリアライズ方法は、可変長のデータをファイルに保存したいときとかに応用できそうです。

Overflow への対応

SQLite は、4096 バイトを超える文字列など 1 ページに収まらないような大きなデータの保存にももちろん対応しています。ある閾値を超えるサイズのデータは Cell Payload Overflow Pages に分割され、先頭のデータのみがセルとしてテーブルのページに保存されます。後続のはみ出たデータは Cell Payload Overflow Pages として連結リストの要領で複数ページに分割して保存されます。

さて、その閾値とセルとして保存されるバイト数は以下のように計算されます。

  • X is U-35 for table btree leaf pages or ((U-12)*64/255)-23 for index pages.
  • M is always ((U-12)*32/255)-23.
  • Let K be M+((P-M)%(U-4)).
  • If P<=X then all P bytes of payload are stored directly on the btree page without overflow.
  • If P>X and K<=X then the first K bytes of P are stored on the btree page and the remaining P-K bytes are stored on overflow pages.
  • If P>X and K>X then the first M bytes of P are stored on the btree page and the remaining P-M bytes are stored on overflow pages.

なんかぱっと見、複雑です。複雑なのは作者も認識しているようで、ドキュメントのすぐ下では後悔と負け惜しみが述べられていました。

In hindsight, the designer of the SQLite b-tree logic realized that these thresholds could have been made much simpler. However, the computations cannot be changed without resulting in an incompatible file format. And the current computations work well, even if they are a little complex.

(日本語訳): 振り返ってみると、SQLiteのBツリーロジックの設計者は、これらの閾値をもっとシンプルに設定できたと気づきました。しかし、計算方法を変更すると、ファイルフォーマットの互換性が失われるため、変更はできません。そして、現在の計算方法は、少々複雑であっても、うまく機能しています。

後方互換性って辛いね。

B 木 vs B+ 木

B 木とは 1 つのノードに 3 つ以上の値を保存する木構造です。ノードをページの単位に揃えることができるのでデータをブロックデバイスに保存するデータベースと相性がよく、データベースではデータ構造として B 木やその派生がよく使われます。

B+ 木は B 木の派生の一種です。B 木はそれぞれの値が重複せずに 1 つのノードだけに保存されている一方で、B+ 木は全ての値とキーの組を葉ノードのみに保存して検索のためのキーが中間ノードに重複して保存されているという特徴があります。B+ 木は中間ノードに値を保存しないため、より多くのキーと子ノードへのポインタを一つのノードに保存でき、木の階層を減らすことができます。また、葉ノード同士を繋ぐポインタを持つことで範囲検索時にアクセスするページ数を減らすことができるという利点もあるらしいのですが、SQLite では葉ノード同士を繋ぐということはしていませんでした。詳しくは自分で調べてみてください。

SQLite ではテーブルに B+ 木を、インデックスに B 木をと使い分けています。僕の推測ですがインデックスは巨大なサイズのキーを葉ノードと中間ノードに重複して保存するときの無駄が大きすぎるため B 木を使っているのだと思います。一方、テーブルのキーは 64 ビットの符号付整数を Varint エンコードしたものなので 1 ~ 9 バイトとサイズの上限が十分小さく、中間ノードに重複して保存したときの無駄は小さく済みます。

木の構造が微妙に違うので、SQLite の実装ではテーブルとインデックスで sqlite3BtreeTableMoveto()sqlite3BtreeIndexMoveto() のように別々の関数が定義されています。一方でそれ以外の操作 (sqlite3BtreeInsert(), sqlite3BtreeNext() など) は共通の関数が提供されています。実装してみるとわかりますが、意外と共通の操作が多いです。

過去のバグの代償

Freelist はデータの削除などで使わなくなったページを再利用のためにリストするための構造です。

A bug in SQLite versions prior to 3.6.0 (2008-07-16) caused the database to be reported as corrupt if any of the last 6 entries in the freelist trunk page array contained non-zero values. Newer versions of SQLite do not have this problem. However, newer versions of SQLite still avoid using the last six entries in the freelist trunk page array in order that database files created by newer versions of SQLite can be read by older versions of SQLite.

(日本語訳): SQLiteの3.6.0 (2008-07-16)より前のバージョンでは、フリーリストのトランクページ配列の最後の6エントリのいずれかに非ゼロ値が含まれている場合、データベースが破損していると報告されるバグがありました。新しいバージョンのSQLiteにはこの問題はありません。しかし、新しいバージョンのSQLiteは、新しいバージョンのSQLiteで作成されたデータベースファイルが古いバージョンのSQLiteでも読み取れるように、フリーリストのトランクページ配列の最後の6エントリを使用しないようにしています。

3.6.0 より前のバグのために Freelist trunk page の最後の方の 24 バイトは未来永劫使われない領域になってしまいました。

後方互換性って辛いね。

ドキュメントされていない仕様

prsqlite は本家の SQLite と互換性を持つことを目標としているので、ドキュメントはもちろんですが本家のソースコードも読みながら実装しています。

そこで、どうやら 4 バイトより小さいセルは 4 バイトとして領域が確保されているらしいことがわかりました。これはドキュメントには書かれておらず衝撃でした。

static int balance_nonroot(
// ...
        while( b.szCell[b.nCell]<4 ){
          /* Do not allow any cells smaller than 4 bytes. If a smaller cell
          ** does exist, pad it with 0x00 bytes. */


static int freeSpace(MemPage *pPage, u16 iStart, u16 iSize){
// ...
  assert( iSize>=4 );   /* Minimum cell size is 4 */


static int fillInCell(
// ...
    if( n<4 ) n = 4;

僕の推測ですが、解放されたセルのサイズが 4 バイト未満だとページ内の freeblock list に追加できず "fragmented free bytes" として無視されてしまうため、なるべく解放されたセルを再利用させるための最適化なのだと思います。しかし、3 バイト未満のセルがあるファイルを SQLite が処理するとセルの解放時に別のセル領域を破壊してしまうため、これはドキュメントするべき仕様な気がします。

一方で、テーブルは最低 1 カラム以上、インデックスは最低 1 カラム + rowid の 1 integer 以上の要素が Record Format によってペイロードシリアライズされるため、全てのセルは 4 バイト以上になるのですが、btree cursor 自体は Record Format とは独立であること、SQLite 自身が 3 バイトのセルのテスト をしている以上仕様っぽいです。

最後に

ファイルフォーマットは一度ファイルがユーザーの手元で作られてしまうと後方互換性を常に意識しながら拡張していかないといけないので、メモリレイアウトと比べてやり直しが効かず特に大変です。

こういう後方互換性を避けるためには、MessagePack とか Protocol Buffers とかのシリアライズフォーマットを使うとか SQLite などの組み込みデータベースを使うと安心です。しかし、部分更新をしたい場合とか、やりたいことに対して組み込みデータベースは複雑すぎる場合などは、自分でファイルフォーマットを考える必要があり(あった)、大変でありつつも楽しいですね。ハハハ。

天下一 Game Battle Contest 2023 に参加した

予習は大切、どうもかわしんです。

「天下一 Game Battle Contest 2023」に参加しました。4位でした。

tenka1.klab.jp

去年このコンテストの存在をちょうど終了した時に知ってやりたいなと思い、Twitter アカウントをフォローして1年待ってました。今回の大会は去年の大会がベースになっているということで事前に予習してから挑みました。

ルールは以下を参照してください。

tenka1-2023/problem.md at master · KLab/tenka1-2023 · GitHub

言語の選択

個人的には Rust か Python がなれているのですが、Go で参加することにしました。

  • Rust
    • 所有権とか、mutable/immutable の制限を気にしながら書くのがめんどくさい。素早く書けない。
    • 複数スレッドでの処理もめんどくさい
  • Python
    • 素早く書けるけど、シングルスレッドになる
  • Go
    • 4年前は一番得意だった言語
    • マルチスレッドが goroutine を使うことで簡単に書ける
    • コンパイルするので凡ミス(typo とか)は防げる

4 方向への予測をマルチスレッドで並列に処理できたら有利だなと思ったので Go でやりました。が、結局僕のロジックはシングルスレッドで全部計算しても 10 ms もかからず、500ms の制約を十分クリアできたのでマルチスレッドの必要はなかったです。

Go は愚直に書かないといけないのでシンプルにめんどくさかったです。ぶっちゃけ Rust で書いた方が実装速度が速かったような気もします。

細かい小技

スタートダッシュ

2023 のコンテストは 2022 のコンテストと同様に複数リーグの構成になっていて、それぞれのリーグの上位数名と下位数名が上下のリーグに移動しながら4時間かけて順位が決まっていくルールになっています。

決勝に進める 1 ~ 8 位もそもそも最上位のクラス 1 のリーグにいないとなれません。リーグ自体は参加した時点で一番下のリーグに組み込まれるので、素早く試合に参加することでスタート地点を有利にすることができます。

予習してたので、ルールが公開されて大体のルールが同じことを確認してから、すぐにサンプルコードで試合を開始して無事クラス 1 のリーグからスタートすることができました。

バージョン管理

運営から提供される gorunner はそれぞれの試合ごとに登録されたコマンド (go run main.go) を実行して試合に参加させます。

Go のサンプルコードは tenka1-2023/go/main.go にありますが、これを開発しながら並行して試合に参加するとコマンド実行のタイミングによっては main.goコンパイルに失敗したり中途半端なロジックの状態で参加することになってしまいます。そのため、main.go で開発して実装の区切りがついたら以下のコマンドを実行してバージョン管理をしてました。

cp main.go v0001.go
go build v0001.go

実装したロジック

大枠としては、2つのエージェントそれぞれについて4方向に進んだ時にどういう感じに有利かを判定して比べながら一番いいものを選択するという方式でやりました。4方向はそれぞれ Prediction 型で表すことにしました。

github.com

戦術的優先度

ShortTermPrediction で表しているのが、1 手先のどのマスが有利なのかです。1 手先のマスの状態によって ShortTermPredictionValue で優先度をつけます。空白が一番優先度が高いです。

const (
    EmptySteal ShortTermPredictionValue = iota
    EmptyMayStolen
    Empty
    EmptyMayConflict
    SelfHalfMayConflict
    EnemyHalf
    EnemyHalfMayConflict
    SelfHalfRecover
    EnemyFullCanSteal
    EnemyFull
    SelfHalf
    EnemyFullMayConflict
    EnemyHalfMayStolen // TODO
    EnemyHalfMayConflictEnemy
    SelfFullMayConflict
    EnemyFullMayRecovered
    SelfFull
    Others
    Dont
)

また、進む先のマスの 1 マス先、2 マス先をそれぞれ見て、敵のエージェントがいないかどうかを確かめます。もしいる場合は、相手が被る方向に移動してきた時に有利になるかどうかで優先順位が変わります。例えば敵の半壊のマスはEnemyHalf* の派生で優先順位が決まります。

       if state[1] == 1 {
            priority = EnemyHalf
            if len(enemies0) > 0 {
                priority = Dont
            } else if slices.Contains(enemies1, state[0]) {
                priority = EnemyHalfMayConflictEnemy
                damageTarget = slices.Contains(enemies1, target)
            } else if len(enemies1) > 0 {
                priority = EnemyHalfMayConflict
                damageTarget = slices.Contains(enemies1, target)
            } else if len(enemies2) > 0 {
                priority = EnemyHalfMayStolen
                damageTarget = slices.Contains(enemies2, target)
            }

一番ムカつくのが自分が全壊にしたマスを敵に取られることなので、それが起きないように Dont を指定しています。

ターゲット指定

以下の記事で紹介されていた通り、順位が最終的なポイントに直結するので、1 位の時は 2 位の敵を、1 位でない時は一つ上の順位の敵を優先して攻撃するようにしました。EstimateRanking() で順位を予測しています。

天下一 Game Battle Contest 2022 参加記 - matsu7874のブログ

戦略的優先度

例えば周りが全部自分のマスに囲まれた時1手先の優先度だけでは効率的に動けません。

そのため、4 方向へのそれぞれの最短で到達できるエリアに存在するマスの状態によって potential を計算し (CalcPotential())、もし potential が低い方向(つまり自分のマスが多く意味の薄い方向)に移動しようとした場合は、第 2 優先の方向へ移動するようにしました。

     if idx_0 == 0 {
            minPotential := predictions0[0].potential
            minPotentialIdx := 0
            maxPotential := 0
            for i, p := range predictions0 {
                if p.potential <= minPotential {
                    minPotential = p.potential
                    minPotentialIdx = i
                }
                if p.potential >= maxPotential {
                    maxPotential = p.potential
                }
            }
            if predictions0[0].shortTermPrediction.priority > SelfHalfMayConflict && minPotentialIdx == 0 && maxPotential-minPotential > 700 {
                log.Println("potential 0")
                idx_0 = 1
            }
        }

不利なマスの乗り越え

1 マス先だけを見ているとあるマスを隔てて空白マスが広がっていた時にそちら側に移動することができません。壁になってるマスを乗り越えて空白マスを取りに行くと有利になります。

2 マス先の考慮を Length2Prediction で行うことにしました。ただし、わざわざ不利なマスを乗り越えていった直後に敵にそのマスを取られるのは悔しいので空白マスの周囲に敵がいるかどうかを検証して、マスの乗り越えをするかを判断します。

周期的な動きの抑制

一番優先順位の高いマスを選びランダムな要素はないので、似たようなユーザーと鉢合わせた時に同じ2マスを交互に移動し続けたり、周期的な挙動をする可能性があります。その場合自分の得点が変わらないまま周期的な動きをしていないユーザーだけが動き続けて不利になってしまいます。そのため、2ターン周期と4ターン周期で同じマスに同じ状態で移動していないかを確認して、無限に周期的な動きをしてターンを浪費することを防ぎます。

     // pendulum 2
        if len(agentLog) > 2 {
            previousPos := agentLog[len(agentLog)-1][0]
            pos := predictions0[0].pos
            if IsSamePos(pos, previousPos) && IsSameState(move.Field[pos[0]][pos[1]][pos[2]], fieldLog[len(fieldLog)-2][pos[0]][pos[1]][pos[2]]) {
                log.Println("pendulum 0 for 2")
                idx_0 = 1
            }
        }

        // pendulum 4
        if len(agentLog) > 4 && idx_0 == 0 {
            previousPos := agentLog[len(agentLog)-3][0]
            pos := predictions0[0].pos
            if IsSamePos(pos, previousPos) && IsSameState(move.Field[pos[0]][pos[1]][pos[2]], fieldLog[len(fieldLog)-4][pos[0]][pos[1]][pos[2]]) {
                log.Println("pendulum 0 for 4")
                idx_0 = 1
            }
        }

特殊移動

今回の大会では特殊移動が新しい要素として加わりました。4 方向のどれかに 5 マス移動して強制上書きするか、任意の場所にジャンプして周囲の 5 マスを強制上書きすることができる機能でエージェントにつき 1 回まで実行できます。

ぶっちゃけ上の部分で時間を使ってしまったので特殊移動は十分に考えることができなかったです。

任意ジャンプした場合は周囲 4 マスが塗りつぶされ、その次の移動は必ず自分のマスになるため、1 ターン無駄になります。そのため、縦に強制上書きするものだけ選択するようにしました。(NewSpecialPredictionStraight())

前半で特殊移動しても点数に加算されないので後半になったタイミングで実行するようにしました。また2つのエージェントが同時に特殊移動してマスが被ったら無駄なので違うタイミングで特殊移動するようにしました。(被るかどうかは予測可能なので余裕があれば改善できた)

感想

最初の方はスタートダッシュがうまくいって 1 位をキープできていたのですが、途中から抜かされてしまいました。考慮できる点はもっとあったし、もっと無駄な移動を省けていたはずなのでその差だと思います。4時間はあっという間でした。

楽しかったです。

SQLite のおもしろ仕様 (1) : データ型

型は型、どうもかわしんです。SQLite では型は絶対ではなく、あくまでも尊重です。信用しすぎると裏切られます。

最近 Rust で SQLiteフルスクラッチで再実装しています。

github.com

なるべく本家の SQLite と compatible にするために SQLite のドキュメントやコードを読んで挙動を理解しながら作っています。これを作ることになった経緯はこの記事で紹介していますが、その過程でいろいろ知らなかった面白い仕様や実装があったので紹介していきたいと思います。今回はその第一弾です。

kawasin73.hatenablog.com

データ型と Type Affinity

SQLite のドキュメントの中で、今の所一番面白いのがこれです。

www.sqlite.org

まず、SQLite の内部的には 5 つのデータ型しかありません。

  • NULL
  • INTEGER
  • REAL
  • TEXT
  • BLOB

一方で我々は SQL の CREATE TABLE 文でテーブルを作るときに以下のようにそれぞれのカラムの型を指定します。

CREATE TABLE t1(a INT, b VARCHAR(10));

ここでいう INTVARCHAR(10) というカラムの型指定は以下の 5 種類の Type Affinity に解釈されます。

  • TEXT
  • NUMERIC
  • INTEGER
  • REAL
  • BLOB

ここで最初に列挙したデータ型と比べると NULL がなくて NUMERIC があることに気づくかと思います。そうです、SQLite で扱われるデータ型と我々が指定する Type Affinity は別物です。

Type affinity はデータをカラムに格納するときの型の変換方法を指定します。(正確には CAST でも使われますがそれは後で)

INTEGER のデータを TEXT affinity の付いたカラムに格納して保存する場合は TEXT に変換されます。

TEXT のデータを INTEGER affinity の付いたカラムに格納して保存する場合は、INTEGER に変換できる時は変換し、変換できない場合 ('1234a' など変換すると一部の情報が欠ける時) は TEXT データのまま保存します。そうです、INTEGER affinity の付いたカラムであっても TEXT データが格納されている時があります。これを知らなかったので読んだ時びっくりしました。

sqlite> CREATE TABLE example1(col INTEGER);
sqlite> INSERT INTO example1(col) VALUES (123), ('123'), ('123a');
sqlite> SELECT typeof(col), col FROM example1;
integer|123
integer|123
text|123a

一方で、CAST(<value> AS <type name>) の構文でもデータ型の変換が同じように行われますが、CAST の場合は一部の情報が欠損する場合でも強制的に型を変換します。

sqlite> SELECT CAST('123a' AS INTEGER);
123

型名を Type Affinity に解釈する

さて、上の例での INTINTEGER は INTEGER affinity に、VARCHAR(10) は TEXT affinity に解釈されます。実は SQLite は型名を文字列の部分一致で解釈します。

3.1. Determination Of Column Affinity を日本語にすると

  1. INT が含まれていた場合は、INTEGER affinity
  2. CHAR, CLOB, TEXT が含まれていた場合は TEXT affinity
  3. BLOB が含まれていた場合は、BLOB affinity
  4. REAL, FLOA, DOUB が含まれていた場合は REAL affinity
  5. それ以外の場合は全て NUMERIC affinity

です。上の方が優先順位が高いので、CHARINT は INTEGER affinity になります。

なお、VARCHAR(10)(10) の数字の中身は SQL の文法としては正しいですが、ガン無視されます。わお。

Note that numeric arguments in parentheses that following the type name (ex: "VARCHAR(255)") are ignored by SQLite - SQLite does not impose any length restrictions (other than the large global SQLITE_MAX_LENGTH limit) on the length of strings, BLOBs or numeric values.

さらにおもしろポイントとしてドキュメントでも紹介されていますが、 FLOATING POINTINT が含まれているので INTEGER affinity です。わお。

データの比較

インデックスを実装するために目的の値を B tree 上で探す時に値の比較が必要になったのでデータの比較についてもドキュメントを読みました。SQLite は異なるデータ型でも比較でき、以下のような順序になります。

NULL < INTEGER | REAL < TEXT < BLOB

INTEGER と REAL 同士の場合は数値的に比較します。

データの比較の前の変換

データ同士の比較の際には前述の Type Affinity に基づいてデータ型の変換が行われます。

4.2. Type Conversions Prior To Comparison によると

  • 片方が INTEGER, REAL または NUMERIC affinity で、もう片方が TEXT, BLOB affinity か Type Affinity がついていない場合、後者に NUMERIC への変換が行われます。
  • 片方が TEXT affinity で、もう片方には Type Affinity がついていない場合、後者に TEXT への変換が行われます。
  • それ以外の場合は変換は行われません。

ここでの、Type Affinity がついていないとは比較文での文字列や数字リテラルなどです。

データ型の変換ではカラムへの格納の際と同じく一部の情報が欠けてしまう場合は変換されずそのままの型として比較されます。

SELECT col, col == '123', col > '456'  FROM example1;
123|1|0
123|1|0
123a|0|1

JavaScript についてのこんなツイートを見かけましたが、こうなる気持ちがすごくわかりました。

TEXT の比較

文字列の比較はデフォルトでは memcmp() と使ってバイト列として比較されますが、COLLATE を使うことで文字列の比較の方法を指定することができます。デフォルトでは BINARY, NOCASE, RTRIM の 3 種類が定義されていて、ユーザーが sqlite3_create_collation() によって独自に定義することもできます。(7. Collating Sequences)

こいつのせいで純粋な Value 同士の比較ができず、僕の prsqlite の実装では ValueCmp という型で wrap して使うことになり、既存のテストコードの変更がすごくめんどくさかったです。

Support Collating sequence for columns · kawasin73/prsqlite@afa31a2 · GitHub

さて、TEXT の比較自体は別にいいのですが、本題はその COLLATE の情報をどこまで引き回すかです。前述の Type Affinity は一度 expression の演算子の計算をするとその結果には Type Affinity は引き継がれません。

sqlite> SELECT CAST('123' AS TEXT) || 456 == 123456;
0

この例でいうと CAST によって '123' には TEXT affinity が付きますが、その TEXT affinity は || での文字列の結合の結果である '123456' には引き継がれません。そのため、'123456' (no type affinity) == 123456 (no type affinity) となり、左右のオペランドに Type Affinity がないので直接比較してデータ型が異なるので答えは 0 になります。

これは、CAST 由来の Type Affinity でも CREATE TABLE のカラム定義由来でも同じです。

しかし、COLLATE は違います。

CAST の結果についてはその内部の collation は引き継がれます。

他の expr 演算子 の演算結果については、その collation が CREATE TABLE のカラム定義由来の場合は引き継がれませんが、COLLATE expression 由来の場合は引き継がれます。わお。

このようにどのようにその collation が指定されたかによって挙動が違います。おそらく仕様としては決まっていなくて、実装した結果こういう挙動になったのだと思いますが、完全に再現するのはめんどくさかったです。

同様に比較などで左右のオペランドからどちらの collation を使うかは、COLLATE expression 由来のものが優先度が高く、同じ優先度の場合は左のオペランドの collation を比較に使い、選ばれたのがCOLLATE expression 由来の場合のみ引き継がれます。

最後に

このような感じで真面目に実装しようとすると面白い発見はありますが、めんどくさいです。

SQLite を再実装する時に役にたつドキュメント

困った時はドキュメント、どうもかわしんです。最近、諸事情で SQLite のドキュメントを読んでいます。

前回の記事 で紹介した通り SQLite を Rust で再実装しています。おかげさまで 300 を超える Github Star もいただき嬉しいです。

github.com

SQLite は全ての仕様が ドキュメント にまとめられているので、そのドキュメントと本家の実装を読み比べながら実装しています。

SQLite を再実装する上で特に以下のドキュメントは役にたちます。これらだけで最小限の SQLite の実装は作れると思います。

  • Overview Documents > About SQLite
    • いかに SQLite がすごいかを自慢しているドキュメント。使おうとしている人には安心を、再実装しようとする人には絶望を与えてくれます。
  • Programming Interfaces > Introduction to the C/C++ API
    • SQLite をどういう風に使うかがざっくりわかります。prepare() -> bind() して準備を行い、step() -> column() で 1 行ずつアクセスする流れみたいです。
  • Programming Interfaces > SQL Syntax
    • SQLite がサポートしている SQL 文が全部載っています。しかも、構文のダイアグラムが全て描かれているので SQL パーサーを書く時に超絶便利です。
    • ただ、四角と丸が何を表しているのかが説明されていないので最初は戸惑いました。僕は以下のように解釈してます。
      • 四角 : 別の構文 (詳細が別ページに用意されている)
      • 丸 + 大文字 : キーワード/予約語
      • 丸 + 記号 : 記号
      • 丸 + 小文字 : 多分そのページ内に説明されているなんらかのリテラル。こいつが曖昧で戸惑いました。
  • Programming Interfaces > DataTypes
    • SQLite のデータ型とその変換や比較についてです。面白情報が詰まってます。
  • Technical and Design Documentation > Architecture
    • SQLite がどのようなコンポーネントの組み合わせでできているかの概観が説明されています。わかりやすいです。
  • Technical and Design Documentation > SQLite File Format
    • これは必読です。SQLite のファイルフォーマットが全部載っています。SQLite を実装しない場合でも自分でファイルフォーマットを設計するときの参考になります。
    • 分量は多いですが、SELECT 文を実装するだけなら一部を読むだけで十分です。僕も必要ない部分はまだ読んでいません。

あとは目次にはないがドキュメント内のリンクを辿ることで到達することができるドキュメントもあります。まだ発掘できていないドキュメントも多々あるのだと思います。

最後に

いつも記事を書き終わった後に最初の書き出しを考えているのですが、この記事を書いて思いついたのが「困った時はドキュメント」でした。ただ、なんかすでに書いたことあるような気がしたので過去の記事を読んでみたらすでに使ってました。4年前の記事。懐かしい。

kawasin73.hatenablog.com

Rust で SQLite を再実装している

セキュリティを盾に一点突破。どうもかわしんです。最近 Rust で SQLite を実装してます。

以前の記事で HTTP Parser を Rust で実装しようとしたものの、すでに実装されていたので断念しましたが、いい題材を見つけました。SQLite です。開発中のリポジトリはこれです。

github.com

今の時点では、Read Only で1つの WHERE 句を持った SELECT 文しか処理できないですが、以下の機能を実装しています。

  • sqlite3 で生成された database ファイルの読み取り (cursor.rs, btree.rs, record.rs)
  • SQL 文の解析 (token.rs, parser.rs)
  • テーブルとインデックスのメタデータのパース (schema.rs)
  • 動的なファイルの読み込み (pager.rs)
  • SQL クエリとスキーマ情報を元に必要なインデックスを使ってデータの読み出し (lib.rs)

それぞれの機能はまだシンプルで最適化はされていないですし、CREATE TABLE や INSERT などの書き込み処理にも対応していかないといけないので先は長いです。

なぜ Rust で SQLite を実装するのか

元々データベースかサーバーを作ってみたいなという興味があったのと仕事で Rust を使っていていい言語だなと思ったのがきっかけです。

C 言語で書かれたソフトウェアを Rust で書き直すことには、特に Rust という言語を使うこと自体に優位性があります。

ChromiumThe Rule Of 2 では以下のうちの全てを満たすと危険で、2つ以下だけであれば安全という風に解釈されます。

  • Untrustworthy Inputs
  • Unsafe Implementation Languages
  • High Privilege

source

この3つのうちの一角を占める "Unsafe Implementation Languages" は Rust を使うことで解決します。つまり、C ではなく Rust を使うだけで安全なプログラムと解釈されやすくなります。

それだけ Rust で書くということには大きなインパクトがあると思っています。

強すぎる SQLite

すでに広く使われているソフトウェアを再開発するのは仕様が出来上がっているので実装に集中でき、頭を使わなくていいので楽です。さらに、リファレンス実装も公開されているので細かいロジックの予習・復習が簡単です。大まかな設計も参考にできます。

その中で、なるべくシンプルで簡単なものを作りたいなと思って思いついたのが SQLite でした。

SQLiteSQL をインターフェースにした埋め込みデータベースで、ライブラリ関数呼び出しから直接データベースを操作します。他の RDBMS に比べてシンプルそうだったので作ってみることにしました。

また、"Rust sqlite" などで Githubcrates.io を調べた限り見つかったのは C の実装ライブラリの binding だけで Rust での pure な実装はなかったので一番乗りする形で実装することにしました。(Let's Build a Simple Database という SQLite based を謳うチュートリアルを元にした mini-db がありましたが、どちらも SQLite file format とは関係なかったのでノーカンです)

SQLite はドキュメントがとても充実しており、実装するときに大いに参考になります。SQL SyntaxFile Format など全ての仕様がドキュメントにまとめられており、またとても読みやすいです。逆にそのドキュメントの充実具合は恐ろしさの始まりでもあります。

www.sqlite.org

SQLite のドキュメントを読み始めるとまず1行目で驚愕の事実を突きつけられます。

SQLite is a C-language library that implements a small, fast, self-contained, high-reliability, full-featured, SQL database engine.

今リンク先を見たら内容が変わっていましたが、僕が読んだ 2 月時点の web archive をみると初っ端から LLVMGCC に負けます。

The Clang/LLVM compiler is not competitive with GCC. Clang-generated binaries are consistently larger and slower than GCC-generated binaries.

Rust は LLVM ベースなのでもう負けです。勝てません。GCC の方が LLVM よりもバイナリサイズも小さいし速いらしいです。

SQLite is built using a DO-178B-inspired process.

リンク先の DO-178B の Wikipedia を読むと

DO-178B, Software Considerations in Airborne Systems and Equipment Certification is a guideline dealing with the safety of safety-critical software used in certain airborne systems.

航空機業界で使われる安全性を担保するための開発方法のガイドラインらしいです。なんだそれ、こちとら個人開発プロジェクトだぞ。勝てないじゃん。

というわけで、ここまでガチガチに開発されてカリッカリにチューニングされたソフトウェアに勝てるわけがないので、SQLite を再開発しようという人がいないのかなと思いました。が、僕は Rust を使うことよるセキュリティの向上、その一点突破にかけて開発していきます。

名前選び

Rust のソースコードファイルの拡張子は .rs なので rsqlite がいい感じだなと思ったのですが、すでにありました。C-binding library ですが3年前で開発が止まっています。ダウンロード状況を見る限り使われている様子もありません。うーむ。

他にも考えましたが、rusqlitesqritesqlitesqlite3 もすでに取られており、全て C-inding library でした。

というわけで苦肉の策ですが、"Pure Rust SQLite" ということで "prsqlite" という名前になりました。

最大の難関 VDBE

SQLiteSQL 文をパースしてバイトコードコンパイルし、VDBE (Virtual DataBase Engine) という Virtual Machine で SQL を実行しています。

www.sqlite.org

EXPLAINsqlite で実行してみると実際のバイトコードが見えます。

sqlite> EXPLAIN SELECT * FROM example WHERE col1 = 10 ORDER BY col2 ASC;
addr  opcode         p1    p2    p3    p4             p5  comment
----  -------------  ----  ----  ----  -------------  --  -------------
0     Init           0     14    0                    0   Start at 14
1     Noop           1     4     0                    0
2     OpenRead       0     2     0     2              0   root=2 iDb=0; example
3     OpenRead       2     3     0     k(2,,)         0   root=3 iDb=0; i_example
4     Explain        4     0     0     SCAN example USING INDEX i_example  0
5     Rewind         2     13    1     0              0
6       DeferredSeek   2     0     0                    0   Move 0 to 2.rowid if needed
7       Column         0     0     1                    0   r[1]=example.col1
8       Ne             2     12    1     BINARY-8       81  if r[1]!=r[2] goto 12
9       Column         0     0     3                    0   r[3]=example.col1
10      Column         2     0     4                    0   r[4]=example.col2
11      ResultRow      3     2     0                    0   output=r[3..4]
12    Next           2     6     0                    1
13    Halt           0     0     0                    0
14    Transaction    0     0     4     0              1   usesStmtJournal=0
15    Integer        10    2     0                    0   r[2]=10
16    Goto           0     1     0                    0

バイトコードの仕様はドキュメントに全て記述されていますが、めんどくさそうで正直実装したくないです。今は SQL構文木インタープリタ的に実行しているので、いけるとこまでこの方法でいきたいです。ただ、複数のテーブルや、中間テーブルを考え始めると複雑性が高まるのでバイトコードを導入しなければいけないこともなんとなくわかります。

最後に

コミットログを見て貰えばわかりますが、2月に作り始めて1週間くらいで社内のオープンソースのリリース手続きがめんどくさ過ぎることを知って飽きて辞めてましたが、最近再開して無事公開に漕ぎ着けました。ソースファイルに会社のコピーライトが付いているのもそのためです。はぁ。

最近は会社から帰ってきてから楽しくてずっと SQLite を作ってます。ちゃんと睡眠を確保して飽きるまで頑張っていきます。

前回の記事 ではソフトウェアエンジニアとしての AI の脅威についてビクビクしてましたが、当面は王道の正面切って AI を利用しながら high productivity を頑張ります。

AI とソフトウェアエンジニアの未来予想

キャリアの危機。どうもかわしんです。

この記事の内容は全て僕個人の意見であり、所属する組織を代表するものではありません。

最近周りで ChatGPT が話題です。特に GPT-4 が出てきたあたりからの Twitter で観測する人々の騒ぎ方をみるにこれまでとは何か潮目が変わったなと感じたので、これからの自分のキャリアを見つめ直すことにしました。

AI は詳しくないので間違ってることがあるかもしれないですが、それはそれで。

ChatGPT にみる AI の特性

GPT は大量の文章データを学習し、入力文章から文章を出力するものです。大規模言語モデルの驚異と脅威 - Speaker Deck のスライドを読んだ感じは、学習したデータを元に出力単語列にもっともらしい次の単語を繋げて文章として出力しているみたいです。

これを発展させると、GPT は「公開されているそれぞれの領域の最先端の知識やスキルに低スキルな人材でも簡単にアクセスできるツール」であると考えることができると思います。GPT は特にそのスキルの具現として、既に完成しているものの再現であったり既存のパターンを当てはめることは、得意でありそうな予感がします。

逆に言えば、ネットでアクセスできない秘密や、最先端の人々より上のスキルや情報は GPT からは出力されません。出力されたらされたでそれは AI によるより優れた AI の産出の可能性でありシンギュラリティになりますが。

現時点では普通にもっともらしい嘘が返ってくるなどその正確性は 100% 信頼できるものではないですが、あと数年経てばその正確性も改善されるはずで人間が出したアウトプットと同じ信頼性を持って受け入れられるようになると思います。ChatGPT が書くコードは検証する必要があり、その検証をするためにそもそものスキルがあることが求められるからソフトウェアエンジニアは依然として安泰だという意見もみられますが、GPT が書くコードが人間が書いたコードと同じ程度に信頼され、その検証すら必要なくなる未来は近いと思います。

ソフトウェアエンジニアとしての価値の源泉

現在、我々ソフトウェアエンジニアはそれなりの高給と働きやすい恵まれた環境にあります。なぜ我々ソフトウェアエンジニアにそれだけのお金が与えられるのか、その価値の源泉はソフトウェアエンジニアにしかプログラムが書けないからであり、また、まともなプログラムを書くことのできる人間の数が少ないからです。

いわばよく勉強して経験を積んだソフトウェアエンジニアであるからそれに見合った高い給料があるわけですが、GPT によってその勉強の成果と経験は誰でもすぐにアクセスできるものになってしまいます。そう、誰でもプログラムを書くことのできる未来です。

5年前に 作る人と使う人 - kawasin73のブログ で予想したように、アプリケーションは誰でも作ることができるようになり製作者としての優位性はビジネス価値やデザインを創出することができるかに寄ってきます。

僕自身はシステムエンジニアの道を選びました。では、我々ソフトウェアエンジニアが高給を保つためにはどのような戦略を取ればいいのでしょうか。

労働者としてのソフトウェアエンジニアの生存戦略

生存戦略としては3つあるかなと思います。

AI にできない、自分にしかできないことをする

OS やミドルウェアなどのシステムソフトウェアはアプリケーションに比べると要求される技術レベルが高く AI にはすぐに模倣することは難しそうです。しかし、技術要素は新しく発明されたものでない限り既存の技術の延長線上にありパターンがあります。パターンの適用が得意な AI に追いつかれるのも時間の問題なような気もしますが、AI との競争もグラデーションであるはずで最後まで残りそうな気もします。

ソフトウェアエンジニアの仕事として例えば、パフォーマンス改善や適切な API 設計、トレードオフの洗い出しと意思決定などがありますが、実際にやってみるとこれもパターンや定石に従ってるだけだなぁと思います。

新しい技術の発明や開発の上でのコミュニケーション以外で、多分ソフトウェアエンジニアにしかできない技術的な優位性というのは無くなってしまうのではないかと思います。

AI を活用して誰よりも効率よく大量のアウトプットを出す

AI のサポートによって最低限の経験を積んだエンジニアは十分な経験を積んだエンジニアと同じようなアウトプットができてしまいます。質が担保された以上、量で勝負するしかないです。

これからのソフトウェアエンジニアは手の速い人、アウトプットの多い人に価値が出てくると予想します。しかし、馬車馬のように働き続け、自分の稼働率を如何に高い状態で維持するかが求められる仕事は疲弊しそうで正直怖いです。

AI を開発する

僕は AI の開発の道は5年前の時点で捨てたのでこれはわからないです。が、AI の観点で言うと、システムエンジニアであれアプリケーションエンジニアであれ、ソフトウェアエンジニアは「使う人」で AI 開発者は「作る人」になります。

正直、今から AI の勉強を始めたところで追いつけるのかなという不安があります。

労働者としてのソフトウェアエンジニアをやめる

ソフトウェアエンジニアで働く以上労働者であり続ける限りは、自分の量の生産性を高く保ち続けることが高給を維持する条件になるとすると、疲弊しそうでできればやりたくないです。

解決策は労働者をやめ、AI を働かせるビジネスオーナーになることだと思います。実務を AI に任せてお金が生まれる仕組みを作ります。まだ自分自身、どういうビジネスがいいか考え中です。いいアイディアがあったら教えてください。

1つの方向性としては AI を利用したコンテンツの大量生産がありますが、同業他社が同じように大量生産する中でどうやってコンテンツを消費者に届けるのかという課題はありそうです。

結論

  • AI を活用した効率的な働き方を身につける
    • 多分僕自身は当分はソフトウェアエンジニアを続けるので、日常業務に AI を組み合わせて効率を上げる試行錯誤をしてます。
  • 労働者をやめ AI で横展開可能なビジネスを始める
    • いいアイディアあったら教えてください。

おまけ

ChatGPT に聞いてみました。

  • AI によってソフトウェアエンジニアの働き方はどのように変わりますか?

AIによってソフトウェアエンジニアの働き方には大きな変化がもたらされるでしょう。以下に、その一例を挙げます。

1 自動化されたコーディング

AIによってコードを自動生成する技術が進化すれば、ソフトウェアエンジニアはより高度なプログラミングタスクに注力することができます。また、繰り返しの作業や単純なタスクを自動化できるため、エンジニアの負荷を軽減することができます。

2 自動化されたテスト

AIによってテストを自動化することができれば、ソフトウェアエンジニアはコードを手動でテストする必要がなくなります。これによって、エラーを見つけるのがより早く簡単になり、プロジェクトの開発サイクルを短縮することができます。

3 プロジェクトの自動管理

AIによってプロジェクト管理が自動化されることで、エンジニアはより多くの時間をコードの開発に注力することができます。AIが進捗状況を監視し、問題が発生した場合は自動的にアラートを発信することができます。

4 ソースコードの自動解析

AIによってソースコードの自動解析が可能になることで、ソフトウェアエンジニアはコードの品質を向上させることができます。例えば、コードのセキュリティの脆弱性を自動的に検出することができます。

これらの変化は、ソフトウェアエンジニアがより効率的に作業を行うことができ、開発プロセスのスピードが向上することが期待されます。また、これらの技術を活用することで、エンジニアが直面するストレスや負荷を軽減することができる可能性があります。

  • AI によってソフトウェアエンジニアの報酬はどのように変化しますか?

AIによってソフトウェアエンジニアの報酬には変化がある可能性がありますが、その影響は複雑で一概に言えません。以下に、いくつかのポイントを挙げておきます。

1 需要の変化による影響

AIが進化するにつれ、ソフトウェアエンジニアのスキル要件が変化する可能性があります。AIによって自動化されたコーディングやテストが一般的になると、プログラマーに求められるスキルや知識が変わることが予想されます。これによって、需要が減少する可能性があり、それに応じて報酬が低下することが考えられます。

2 市場競争の変化による影響

AIによってソフトウェアエンジニアの報酬に影響を与える要因として、競争があります。AIが導入されることで、より多くの人々がソフトウェアエンジニアとして働くことが可能になるため、市場競争が増加する可能性があります。競争が激化することで、報酬が下落する可能性があります。

3 新しい機会の創出による影響

一方で、AIによって新しい機会が生まれることがあるかもしれません。例えば、AIに関連する技術を開発するために、ソフトウェアエンジニアにニーズが生まれることが考えられます。そのような機会によって、報酬が上昇する可能性があるかもしれません。

以上のように、AIによるソフトウェアエンジニアの報酬に影響を与える要因は複雑であり、特定の予測をすることは困難です。ただし、ソフトウェアエンジニアは常に新しい技術を学び、自己研鑽することが重要であるといえます。