プログラミング
Goose: C++より1.16倍、安全なRustより1.12倍高速でメモリ安全
Goose: 1.16x faster than C++ and 1.12x than safe Rust, while memory safe (github.com)
要約
Gooseは、メモリ安全性を持ちながらC++や安全なRustよりも高速で、より少ないメモリ使用量で動作する新しいシステムプログラミング言語です。ヒープやアロケータ、GCを持たず、コンパイラ管理下のデータスタックに全ての動的値を配置することで、パフォーマンスとメモリ安全性を両立させています。ライフタイムアノテーションやunsafeブロックも不要で、コンパイラが参照の生存期間を静的に管理します。
全文翻訳
Gooseプログラミング言語 メモリ安全なシステム言語で、C++や安全なRustよりも高速、メモリ使用量も少なく、アロケータ/GCなし、ライフタイムアノテーションも不要です。
チュートリアル · 仕様 · サンプル · ベンチマーク · 標準ライブラリ
GooseはCやRustに似た見た目で、一つのアイデアに基づいています。それは「ヒープがない」ということです。全ての動的値はコンパイラが管理するデータスタック上にインラインで配置され、成長はポインタのインクリメントで行われ、スコープを抜けるときに解放されます。残りの言語機能は、これを実際のプログラムで機能させるために必要なものと、それによって得られる測定可能な利点です。
なぜGooseなのか
C++や安全なRustよりも高速で、メモリ安全。
16以上のベンチマークで、Gooseは慣用的なC++の3.3倍、手作業で最適化されたC++の1.16倍、そして最良の安全なRustの1.12倍の速度を達成し、メモリ使用量はそれぞれ1.9倍、1.3倍、1.2倍少なくなっています(要約、全結果)。この優位性は構造的なもので、他の言語では表現できないことから来ています。アロケータ、GC、参照カウント、デストラクタがありません。メモリはコンパイラが静的に割り当てる少数のデータスタックです。百万要素の構造体の解放は、どれだけ深くネストしていても、ストア操作一つで完了します。何も移動しません。成長中の配列への参照は、配列が存在する限り有効です。C++が予約を必要とし、安全なRustがu32インデックスに後退する場所で、型付き参照を保持できます。注釈なしでメモリ安全。ライフタイム構文、エイリアシングや排他性のルール、unsafeはありません。コンパイラは全ての参照が何に根ざしているかを推論し、ただ一つのこと、つまり所有者よりも長く生存することを拒否します。すべてがフラットです。文字列、文字列の配列、可変サイズのフィールドを持つレコード、そしてそれらのレコードの配列は、それぞれポインタを含まない単一の連続したブロックです。160バイトのC++のレコードとアロケーションは、Gooseでは29バイトでアロケーションなしです。Enumは保持するものをコストとします。可変モードのADTは、各値に最大のバリアントのサイズではなく、そのバリアント固有のサイズを与えます。これにより、ベンチマークでそれを実行する際に、メモリ使用量が4倍、Rustのenumより2倍高速になります。ポインタより細いリンク。相対参照は、型指定されチェックされたリンクを1、2、または4バイトのオフセットとして格納します。それらから構築された構造体は位置独立なので、データ構造は既にファイル形式になっています。保存は書き込み、読み込みは読み込みと敵対的なバイトを拒否する検証パスです。すべてがその場で構築され、保証されます。値は、あらゆる深さの呼び出しを通じて、最終的な宛先に構築されます。items.push(parse(line))は、解析されたレコードを配列に直接書き込み、可変サイズの配列を値として返すことはコストがかかりません。配管なしのエラー。loadからのreturn errは、アンワインダーも?も各呼び出しにもなく、静的にチェックされた任意の数のフレームを関数から返します。何も共有しないスレッド。ワーカーは、独自のメモリを持つ別のプログラムとしてコンパイルされ、フラットな値はmemcpyとして型付きキューを横断します。データ競合、ロック、アトミック、メモリ順序は言語に存在しません。オーバーヘッドなしのジェネリクスと高階関数。型指定されていないパラメータはジェネリックです。関数値はコンパイル時のエンティティなので、xs.filter() { it > 0 } は見た目通りのループにコンパイルされ、結果を直接宛先に構築します。プレーンCの入出力。Gooseは単一のCファイルにコンパイルされるため、Cコンパイラが動作する場所ならどこでも実行でき、extern fnを介してCと直接呼び出しが可能です。バンドルされたTinyCCバックエンドは、ビルドステップなしでインプロセスでプログラムをコンパイル・実行します。チュートリアルはこれらすべてを例で説明し、仕様には正確なルールがあり、ベンチマークには損失を含めた数値があります。機能Gooseで異なる点のみを示しています。チュートリアルは同じ内容を適切にカバーしており、サンプルは実際の作業を行う26の完全なプログラムです。
一つのメモリモデル:スタック、スコープ終了時の解放
プログラムはネイティブコールスタック、静的データ、N個のデータスタックを持ち、Nはコンパイラが決定します。データスタックは大きなアドレス空間予約とバンプポインタで構成され、それ以外のメモリはありません。スタックごとに最大一つだけリサイズ可能な値が存在し、それは常にトップにあるため、成長は何も移動せず、容量をチェックすることもありません。これらすべてはコンパイル時に証明されます。ランタイムはバンプポインタしか保持しません。
for round in 3 {
var scratch: u8[>..] = []; // 成長専用:成長はポインタのバンプ
for i in 100000 {
scratch.push((i % 256) as! u8);
}
print("round ", round, ": ", scratch.len, " bytes");
}
// 解放:スタックトップへのストア一つ
何も移動しないため、参照は成長後も有効です
struct Item {
id: i32,
weight: f32
}
var items: Item[>..] = [];
let first .= items.push(Item { 1, 0.5 }); // 要素0への参照
for i in 2..1000001 {
items.push(Item { i as i32, 0.0 });
}
first.weight = 99.5; // 依然として有効、百万回のプッシュの後
pushは、それが作成した要素への参照を返し、データが構築中にリンクアップされる方法です。vector<T>やVec<T>は再アロケーションするため、どちらもこれを保証できず、ベンチマークの優位性のかなりの部分がここから来ています。
安全な参照、アノテーションなし
全ての参照とスライスは静的なルート(そのターゲットの生存期間を制限する変数)を持ち、ライフタイムシステム全体は一つのルールです:参照は、そのターゲットを所有する変数を超えて生存してはならず、間違った型で見てもいけません。ルートは推論され、関数はルートごとに特殊化されるため、これらすべてに対する構文はなく、エイリアシングや排他性のルールもありません。チェッカーが反対する場合、両端を名前で示します。
fn longest(a: u8[:], b: u8[:]) -> u8[:] {
if a.len >= b.len {
a
} else {
b
}
}
var outer: u8[>..] = [];
var w = outer[..];
{
var inner: u8[>..] = [];
format(inner, "inner text");
w = longest(outer, inner); // エラー:innerに根ざした参照を格納しているため、
} // デスティネーションを超えて生存しない(§9.2)
スライスT[:]は普遍的な「範囲を処理する」パラメータです。全ての配列種類は無料でそれに変換され、コピーされないため、splitは入力のスライスを返し、u8[:]をキーとする辞書は文字列を全く格納しません。
すべてがフラットです
単一の配列型はありません。サイズの違いのみを持つファミリーがあり、各メンバーは[メタデータ][要素...]であり、インラインでパックされ、要素ブロックへのポインタは決してありません。
スペル
意味
T[k]
コンパイル時に既知の固定サイズ
T[], T[varint]
構築時にサイズが決まり、その後フリーズ。固定または可変幅の長さ
T[..k], T[..]
インライン容量。内で成長・縮小する
T[>..]
成長専用:ワークホース。参照は有効なまま
T[>..<]
成長・縮小:スタック、キュー、ヒープ
文字列は単なるu8配列です:u8[>..]はビルダー、u8[]はそれを保持するものの中にインラインで格納された完成した文字列、u8[..16]は構造体内の小さな文字列、u8[:]はビューです。
構造体は可変サイズのパートを含むことができ、それらは宣言順にインラインに配置されるため、レコードは間接的なものを含まないバイトの連続です。
struct Item {
sku: u8[varint],
qty: varint,
cents: varint
}
struct Order {
id: varint,
customer: u8[varint],
items: Item[varint]
}
var book: Order[>..] = [];
book.push(Order {
id: 1001,
customer: "alice",
items: [
Item { sku: "SKU-441", qty: 2, cents: 1999 },
Item { sku: "SKU-7", qty: 1, cents: 500 }
]
});
その注文は29バイトでアロケーションなしです。C++のstd::string + std::vector<Item>としては160バイトと1アロケーション、RustのString + Vec<Item>としては153バイトと4アロケーションです。注文書全体はキャッシュを流れる単一の配列です。その代償は、可変サイズの要素の配列はシーケンシャルであることです。反復処理はできますが、インデックスを付けることはできません。
Enumは二つのサイズ
代数的データ型は唯一の動的ポリモーフィズムです。継承やvtableはありません。各ADTは使用時に選択される二つの方法で格納できます。固定モード(Shape)は、最大のペイロード用の領域とタグで、インデックス可能で上書き可能です。可変モード(Shape..)は、各値にそのバリアント固有のサイズを正確に与えます。配列はシーケンシャルになり、値はバリアントを変更しません。その交換として、ペイロードへの参照を取ることができます。
enum Shape {
Circle { r: f64 },
Rect { w: f64, h: f64 },
Dot
}
var packed: Shape..[>..] = []; // それぞれ17バイトではなく、9、17、1バイト
packed.push(Shape.Circle { r: 1.0 });
packed.push(Shape.Rect { w: