Rust の Zero-Copy ページ:あるいは、我々がいつの間にか「ライフタイム」を心配せず、むしろ愛し始めた話

2026/04/16 15:11

Rust の Zero-Copy ページ:あるいは、我々がいつの間にか「ライフタイム」を心配せず、むしろ愛し始めた話

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

提供されたサマリーは、鍵となるポイントリストに記載されている主要な技術的業績およびアーキテクチャの詳細を効果的に捉えており、よく構成されており包括的です。ゼロコピー設計、所有権の保証、ハードウェア制約、バッファ管理といった重要な概念については明瞭さを保ちつつ、不要な専門用語を排除しています。改良点は必要ありません。

サマリー:

本記事は、カーネル空間とユーザー空間間の高価なデータのコピーを排除し、スループットを大幅に向上させる高性能な Rust データベースアーキテクチャを導入します。

O_DIRECT
フラグを活用して OS ページキャッシュを迂回することで、このシステムは速度を犠牲にせず安全性を維持します。これは、標準的なエンジン設計においてメモリの境界で頻繁にコピーを行うことで処理が低速化するという点に対する主要な利点です。このソリューションは、Rust の独自の所有権モデルに頼っており、
PageReadGuard
を使用して読み取り、
RwLockWriteGuard
を使用して書き込みを行いながら、固有のコピーを作成するのではなく借用されたビューを保持することでデータ整合性を確保します。また、64 ビット DMA サポートがない古いシステムといった特定のハードウェア制約についても巧妙に対応しており、
HeapHeaderRef
などの間接化技術を利用します。実践的な意味では、これにより挿入操作時のメモリの境界シフトが効率的に行われ、バッファプール内のページ除去ポリシーも滑らかに管理されます。結局のところ、このアーキテクチャを採用して導入する企業は、CPU オーバーヘッドの大幅な削減を期待でき、高ボリュームなデータ処理アプリケーションが最小限のリソース浪費と最大効率で負荷の高い運用を処理できることを意味します。

本文

プロジェクトのソースコードはこちらから入手できます。

ゼロコピー(Zero-Copy) とは、カーネルとユーザー空間のバッファ間で CPU がデータを転送することを省略する手法です。特にデータベースエンジンのような高スループットなアプリケーションにおいて非常に有利です。負荷の高い状況下、特に作業セットがキャッシュから追い出されている場合に、そのパフォーマンス向上への寄与は顕著です。

ゼロコピーとは

ここで一般的なデータベースエンジンの構成を示します。今回の記事では、主に「OS の境界」と「バッファプールから上位レイヤーに至る経路」の 2 つのコピー境界に焦点を当てます。

┌─────────────────────────────────────────────────────────┐
│                      クエリ層 (Query Layer)              │
└────────────────────────┬────────────────────────────────┘
                         │
┌────────────────────────▼────────────────────────────────┐
│                   実行エンジン (Execution Engine)         │
└────────────────────────┬────────────────────────────────┘
                         │
┌────────────────────────▼────────────────────────────────┐
│                  トランザクションマネージャー            │
└──────────┬─────────────┴─────────────────┬──────────────┘
           │                               │
┌──────────▼──────────┐       ┌────────────▼────────────┐
│    ロックマネージャー│       │      ログマネージャー   │
└─────────────────────┘       └─────────────────────────┘
                         │  より上位のレイヤーへの新鮮なコピー
┌────────────────────────▼────────────────────────────────┐
│                   バッファプール (Buffer Pool)            │
└────────────────────────┬────────────────────────────────┘
                         │  OS 境界でのコピー
┌────────────────────────▼────────────────────────────────┐
│                    ディスク                              │
└─────────────────────────────────────────────────────────┘

高性能なエンジンを構築しようとする場合、できるだけ不要な作業を省略することが求められます。そしてデータのコピーはまさにその範疇に分類されます。

各コピー操作を

memcpy()
の同等物と考えれば理解しやすいでしょう。
memcpy()
は実際にはパイプラインのストールを引き起こし、これは高性能アプリケーションでは回避したいものです。CPU はデータをソースから読み取り、宛先に書き込む作業を行うためです。つまり、重要な処理以外のことにサイクル(処理時間)を費やしており、これによりホットデータが CPU キャッシュから追い出される原因となります。

次に、通常の読み書き操作が行われるライフサイクルについて示します。ここで挙げられるすべての CPU コピーは、サイクルを消費するだけで実効性を欠く作業です。

イメージ元:Linux Journal

さて、まずはバッファプールとディスクの間のレイヤーにあるコピーを取り除くことに焦点を当ててみましょう。

バッファプールと Direct IO

リーナス・トルバルズが直接 I/O インターフェースに関する有名な激怒した発言です。彼はデータベース開発者を嫌います(と書かれています)。

バッファプールは

open()
システムコールを通じてファイルディスクリプターを開き、保管します。これらのファイルディスクリプターに対して
read()
write()
を呼び出すと、前述のユーザー空間、カーネル、DMA 間のコピーを含む一連の循環を辿ります。

ここで手軽に成果が得られるアプローチは、

O_DIRECT
フラグを使用した Direct I/O を採用することです。多くの現代のデータベースがこの手法を採用していますが、Postgres など例外もあります。これにより、アプリケーションは OS のページキャッシュをバイパスさせられます(64 ビット DMA 対応ハードウェアを搭載していることを前提とします)。32 ビットの DMA デバイスを持つシステムや、機密計算用仮想マシン(AMD SEV、Intel TDX)においては、カーネルがサイレントに SWIOTLB バウンซ์バッファを導入し、結果として CPU コピーが再導入される可能性があります。詳細については Linux カーネルドキュメントの swiotlb を参照してください。

O_DIRECT
を使用するには、提出されるバッファはポインタアラインメント、I/O の長さとファイルオフセットに対して整列している必要があります。Rust では、ページを保持するバッファに
#[repr(align(4096))]
によって前者を保証し、4 KiB ページサイズの読み書きとページアラインメントされたオフセットを使用することで、残りの要件も満たします。これを満たさないと、
O_DIRECT
の読み書きは
EINVAL
エラーで頻繁に失敗します(C 言語でのこの挙動を示す gist が存在します——最初のプログラムでは malloc を使用しているため 4096 字節アラインメントされていないため書き込みが失敗し、2 つ目のプログラムでは posix_memalign を使用するため成功しています)。

カーネルのページキャッシュをバイパスするため、リードaheadやライトの融合(coalescing)といった有益な恩恵を受けることはできません。しかし、これがなぜデータベースにおいてバッファプールが非常に重要なのかという理由そのものです。

バッファプールは OS のページキャッシュに代わるものであり、特定のワークロードのために設計されています。これについては「メカニズム + ポリシー」という観点で考えることが常に有益です。

  • メカニズム: 固定サイズのパージートブルであり、上位レイヤーからのページ要求に応えつつ、他のページを収容するために一部のページを驱逐(evict)します。
  • ポリシー: どのページを驱逐するかを決定する方法(驱逐ポリシー)です。
┌─────────────────────────────────────────────────────────┐
│                      クエリ層 (Query Layer)              │
│                                                         │
│   テーブルスキャン / インデックススキャン / B ツリートル スキャン │
│   (行をイテレーティングし、next()、get_value() を呼び出す) │
└────────────────────────┬────────────────────────────────┘
                         │ 使用
┌────────────────────────▼────────────────────────────────┐
│                    トランザクション                      │
│                                                         │
│   tx_id | ConcurrencyManager | RecoveryManager          │
└────────────────────────┬────────────────────────────────┘
                         │ pin() / unpin()
┌────────────────────────▼────────────────────────────────┐
│                   バッファプール (Buffer Pool)            │
│                                                         │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐               │
│  │ フレーム 0 │  │ フレーム 1 │  │ フレーム 2 │  ...          │
│  │          │  │          │  │          │               │
│  │ file:3   │  │ file:7   │  │  empty   │               │
│  │ pins: 1  │  │ pins: 0  │  │  pins: 0 │               │
│  │          │  │          │  │          │               │
│  │ 4KB データ│  │ 4KB データ│  │          │               │
│  └──────────┘  └──────────┘  └──────────┘               │
│                                                         │
│  ポリシー: LRU | CLOCK | SIEVE                          │
└────────────────────────┬────────────────────────────────┘
                         │ 
                         │ 
┌────────────────────────▼────────────────────────────────┐
│                        ディスク                           │
│                                                         │
│    [ ファイル 1 ]  [ ファイル 3 ]  [ ファイル 7 ]  [ ファイル 9 ]       │
└─────────────────────────────────────────────────────────┘

システムに適したポリシーを選択するのはワークロードの特性に依存しますが、ほとんどのシステムは CLOCK(LRU の近似)を採用するのが一般的です。バッファプールで使われている置換ポリシーの非排他的なリストが以下の通りです(※ここでの記述は要約と推測を含みます)。

O_DIRECT
は OS 境界のコピーを除去します。次の課題は、ページがすでにバッファプールにある状態から、そこから新たなコピーを引き起こさないことです。

リードパスからのコピーの排除

これまで「ゼロコピー」とは、カーネルとバッファプール間のコピーを除去することを意味していました。ここからは、その範囲を少し広げて、エンジン内部での冗長なコピーも除去することを指すと定義します。

Rust はデータを複製する際の課題に対処するための素晴らしい反面、恐ろしい方法を持っています。それが「参照(references)」です。素晴らしいところは単一の文字(

&
)で記述できる点ですが、恐ろしいところは生命期(lifetimes)を取り扱えるようになる必要がある点です。

生命期の最も単純な考え方は、「タイプ A が保持する参照が、その参照が指し示すデータの寿命を超えないことをコンパイラに証明している」というものです。

まずは単一のページの生データ(raw bytes)を以下のように定義しましょう:

pub struct PageBytes {
    bytes: [u8; PAGE_SIZE_BYTES as usize],
}

次に、単一のバッファプールのフレーム内に保持されるデータを定義します。ここで

RwLock<T>
はページラッチとして機能します。

#[derive(Debug)]
pub struct BufferFrame {
    page: RwLock<PageBytes>,
}

さて、このフレームのデータを

PageReadGuard
内に保管しましょう。

// この例を小さく保つために、以下の型は図式的なもの而非常に実際のものです。
// ここでは所有権とのトレードオフを示し、RwLockReadGuard の詳細な実装
// は省略します。
/// ピンされたページへの共有アクセスを提供するリードガーディアン。
pub struct PageReadGuard {
    page: PageBytes,
}

このバージョンはモデリングが簡単ですが、設計にコピーを埋め込んでいます。より上位のレベルのページオブジェクトがそれぞれ

PageBytes
を所有する場合、バッファプールストアからこれらのオブジェクトを構築すると、新しい所有された値を実体化(materialize)しなければなりません。

私たちが実際に望んでいるのは「所有権」ではなく、「すでに他 somewhere に存在するバイトに対する借用ビュー(borrowed view)」です。これを表現するには生命期を導入します。

pub struct PageReadGuard<'a> {
    page: &'a PageBytes,
}

この生命期の注釈により、コンパイラに対して

PageReadGuard
PageBytes
の寿命を超えないことを証明しており、より上位のページオブジェクトが新しいコピーを持つのではなく、既存のバイトに対するビュー(参照)となるように動作します。

実際の実装では、フィールドは

'a PageBytes
ではなく
RwLockReadGuard<'a, PageBytes>
ですが、所有権に関するストーリーは同じです:ガーディアンはページバイトを所有するのではなく借用し、ラッパーはその借用を先送りします。

pub struct PageReadGuard<'a> {
    page: RwLockReadGuard<'a, PageBytes>
}

通常のデータベースエンジンはヒープページと B ツリートルページの 2 つのコアなページタイプを持っています。ここでは前者に焦点を当てましょう。ページの構造は標準的なスロット付きページ(slotted page)レイアウトとなります。

この時点では、

PageReadGuard<'a>
を通じてバイトがすでに借用されています。問題は、ガーディアンがどこに住み、解析された参照がどこに住むべきかという点です。

最も自然な試みはすべてを 1 つの構造体に保つことです。

struct HeapPage<'a> {
    guard: PageReadGuard<'a>,
    header: &'a [u8],
    line_pointers: &'a [u8],
    record_space: &'a [u8],
}

これは Rust の古典的な「自己参照構造体」の問題へと導きます。ポインタの無効化が非常に難しくなります。想像してみてください、構造体に 2 つのフィールド A と B があり、B が A を指している場合、もし構造体 A が移動(move)した場合、B はどこを指していますか?A のあった場所を指し続けることになりますが、これは無効であり未定義挙動(UB)につながります。Rust では

Pin
、unsafe な生ポインタ、
Arc
ポインタや ouroboros などの外部パッケージを使ってこの問題を回避する方法がありますが、これらにはすべてオーバーヘッドが伴います。

したがって、次の試みとして、ガーディンの所有権とページに対する解析されたビューの所有権を分けます。

pub struct HeapPage<'a> {
    guard: PageReadGuard<'a>,
}

pub struct HeapPageView<'a> {
    header: &'a [u8],
    line_pointers: &'a [u8],
    record_space: &'a [u8],
    layout: &'a Layout,
}
let page = HeapPage::new(guard);
let view = HeapPageView::new(&'a page, layout); // ページとライフタイムをスタック上に維持し、ページが有効なまま

これは機能しますが、バイトを変更したい場合に問題が発生します。ヒープページにレコードを挿入したい場合、

record_space
line_pointers
の両方を変更する必要があります。この時点で、両者の境界(bounds)が変わる可能性があり、構造体内にある参照が古くなる(stale)ことになります。構造体をドロップして再構築する必要があります。これにはコストはかからないものの、漏れのある抽象化(leaky abstraction)です。

より良い形式は以下のようになります:

pub struct HeapPage<'a> {
    guard: PageReadGuard<'a>,
}

pub struct HeapPageView<'a> {
    header: &'a [u8],
    body_bytes: &'a [u8],
    layout: &'a Layout,
}
let page = HeapPage::new(guard);
let view = HeapPageView::new(&'a page, layout); // ページとライフタイムをスタック上に維持し、ページが有効なまま

スロット付きページでは、ヘッダーサイズは固定されており、何か操作を行うたびに、ヘッダーから得た情報を元にボディバイトを再解析して、バイトへの正確なビューを取得します。

しかし、これはページとビューの両方をスタック上に維持する必要があり、クエリ層に実装詳細を漏らしてしまうという問題があります。これもまた、漏れのある抽象化です。

私が採用したバージョンはこれを逆にしました。従来のコンピュータサイエンスの原則である「コンピュータサイエンスにおける問題は、別の階層の間接性(indirection)で解決できる」を適用します。具体的には、スロット付きページはヘッダー、ラインポインタ、レコード空間に分割されるため、これら解析された参照を

HeapPage
に格納し、
PageReadGuard
HeapPageView
に保持します。

pub struct HeapHeaderRef<'a> {
    bytes: &'a [u8],
}

struct LinePtrBytes<'a> {
    bytes: &'a [u8],
}

struct LinePtrArray<'a> {
    bytes: LinePtrBytes<'a>,
    len: usize,
    capacity: usize,
}

struct HeapRecordSpace<'a> {
    bytes: &'a [u8],
    base_offset: usize,
}

struct HeapPage<'a> {
    header: HeapHeaderRef<'a>,
    line_pointers: LinePtrArray<'a>,
    record_space: HeapRecordSpace<'a>,
}

pub struct HeapPageView<'a> {
    guard: PageReadGuard<'a>,
    layout: &'a Layout,
}

これらすべてが

'a
という正確な同じ生命期を共有しており、他の型によって保持されるこれらの参照のいずれかがその型の寿命を超えないことを意味します。そして、これらのすべてのタイプは
BufferFrame
の内の
PageBytes
に保持される同じセットのバイトへの参照です。

PageBytes (BufferFrame によって所有)
┌─────────────────────────────────────────────────────┐
│ ヘッダー (34 バイト)                                  │
│ page_type | slot_count | free_lower | free_upper .. │
├─────────────────────────────────────────────────────┤
│ ラインポインタ(成長)                                 │
│ [ slot 0 ] [ slot 1 ] [ slot 2 ] ...                │
├─────────────────────────────────────────────────────┤
│ 未使用領域                                          │
├─────────────────────────────────────────────────────┤
│ レコード空間(減少)                                  │
│ ... [ tuple 2 ] [ tuple 1 ] [ tuple 0 ]             │
└─────────────────────────────────────────────────────┘
      │               │                    │
HeapHeaderRef<'a>  LinePtrArray<'a>  HeapRecordSpace<'a>
      │               │                    │
      └───────────────┴────────────────────┘
                      │
                 HeapPage<'a>  ←─────────────────────────┐
                                                         │
                                                  built from
                                                         │
                                            HeapPageView<'a>
                                          ┌──────────────────────────┐
                                          │ guard: PageReadGuard<'a> │
                                          │ layout: &'a Layout       │
                                          └──────────────────────────┘
                                                   │
                                            保持するロックを所有
                                                   │
                                            PageBytes in BufferFrame

それらをつなぐのは、

HeapPageView
のメソッドで、いくつかの操作がページのバイトに対する解釈済みビューを必要とするたびに
HeapPage
を構築します。

impl<'a> HeapPageView<'a> {
    fn build_page(&'a self) -> HeapPage<'a> {
        HeapPage::new(self.guard.bytes()).unwrap()
    }

    pub fn row(&self, slot: SlotId) -> Option<LogicalRow<'_>> {
        let view = self.build_page();
        let mut current = slot;
        loop {
            match view.tuple_ref(current)? {
                // 簡潔にするためのコード省略
            }
        }
    }
}

impl<'a> HeapPage<'a> {
    fn new(bytes: &'a [u8]) -> SimpleDBResult<Self> {
        // PageKind トrait から共有された解析ロジックを使用
        let layout = Self::parse_layout(bytes)?;

        let header = HeapHeaderRef::new(layout.header);

        // ヒープ固有の追加検証
        let free_upper = header.free_upper() as usize;
        let page_size = PAGE_SIZE_BYTES as usize;
        if free_upper < header.free_lower() as usize || free_upper > page_size {
            return Err("heap page free_upper out of bounds".into());
        }

        let page = Self::from_parts(header, layout.line_ptrs, layout.records, layout.base_offset);
        assert_eq!(
            page.slot_count(),
            header.slot_count() as usize,
            "slot directory length must match header slot_count"
        );
        Ok(page)
    }
}

クエリ層がデータページから何かを読み取る場合、

HeapPageView
を受け取り、
HeapPageView
の操作で特定の論理的セグメントへのアクセスが必要となる場合、ページのバイトのレイアウトを理解する
HeapPage
を構築します。

さて、おそらく毎回

HeapPage
を再構築するコストについて疑問に思っているかもしれません。実は非常に低コストです。それは完全に算術操作からなり、いくつかの不変条件が満たされていない場合に数少ないパニック(panic)で構成されています。算術操作は CPU が行うのに極めて低コストであり、特に
memcpy()
操作と比較した場合です。

すべての CPU 操作は等しいわけではありません。出典: ithare.com, Andrew Kelley の Data Oriented Designに関する演説より

ライトパスからのコピーの排除

前のセクションでは

PageReadGuard
HeapPage
、および
HeapPageView
を見てきましたが、これらはページの読み込み側パス全体を構成します。しかし、Rust にはエイリアシングか排他性変更の原則(aliasing XOR mutability)があり、これは複数の
&T
または単一の
&mut T
のいずれかであることを意味します。上記ではすべて
&T
パスでしたが、
&mut T
パスも必要です。

/// ピンされたページへの排他的アクセスを提供するライトガーディアン。
pub struct PageWriteGuard<'a> {
    page: RwLockWriteGuard<'a, PageBytes>,
}

pub struct HeapPageMut<'a> {
    header: HeapHeaderMut<'a>,
    body_bytes: &'a mut [u8],
}

pub struct HeapPageViewMut<'a> {
    guard: PageWriteGuard<'a>,
    layout: &'a Layout,
}

impl<'a> HeapPageViewMut<'a> {
    fn build_mut_page(&mut self) -> SimpleDBResult<HeapPageMut<'_>> {
        HeapPageMut::new(self.guard.bytes_mut())
    }

    pub fn insert_tuple(&mut self, tuple: &[u8]) -> SimpleDBResult<SlotId> {
        let mut page = self.build_mut_page()?;
        page.insert(tuple)
    }
}

さて、Rust のエイリアシング規則に従い、

BufferFrame
がある場合、読み込みラッチを複数回取得して読み込みパスを構築するか、または書き込みラッチを取得して書き込みパスを構築するかのいずれかを行います。

                    BufferFrame
                    RwLock<PageBytes>
                         │
              ┌──────────┴──────────┐
              │                     │
         read_page()           write_page()
              │                     │
    RwLockReadGuard          RwLockWriteGuard
    (共有、複数のリーダー)      (排他的、1 つのライター)
              │                     │
       PageReadGuard           PageWriteGuard
           &'a [u8]               &'a mut [u8]
              │                     │
         HeapPage              HeapPageMut
       (借用 '&a [u8])          (借用 '&a mut [u8])
              │                     │
       HeapPageView<'a>         HeapPageViewMut<'a>

そして、もちろん Rust の借 Checker は使用後無効化(use-after-unpin)をコンパイルエラーにしますが、ランタイムの危険にはしません。

ここで注目すべきもう一つの非対称性があります。

HeapPage
はバイトを 3 つのフィールド(ヘッダー、ラインポインタ、レコード空間)に分割していますが、これは読み込みが冓的であり、これらの境界は決してシフトしないためです。

一方、

HeapPageMut
はこれをできません:単一の挿入操作は
free_lower
free_upper
の両方を移動させ、分割されたすべての参照を即座に古くしてしまいます。したがって、
HeapPageMut
は単一の
body_bytes: &mut [u8]
を保持し、各操作でヘッダーからサブリージョンを再導出します。Rust はエイリアSED な変更アクセスを防ぎますが、分割点が常にヘッダーと一致するという深い不変条件は設計を通じて強制する必要があります。

ネストされた借用(Nested Borrows)

気づいたでしょうか、これまで私たちは単一の生命期

'a
のみを使用してきました。これは意味があることです。なぜなら、私たちはすべて同じセットの基礎となるバイトから借用しているためです。

しかし、実際には私たちが不正確で、コンパイラにいくつかの作業を任せていました。これまでの借用の連鎖は以下の通りです:

PageBytes
RwLock{Read,Write}Guard<'a>
Page{Read,Write}Guard<'a>
HeapPage[Mut]<'a>
HeapPageView[Mut]<'a>

各次の借用は前のものにネストされていますが、コンパイラは生命期の多変数性(variance)により、これらすべてのことを単一の生命期で行うことを許可します。

生命期多変数の核心的は、共有参照は共変(covariant)であり、排他参照は不変(invariant)であるということです。これは別の方法でよく表現されるのですが、

&T
'a
に対して共変で、
&mut T
'a
に対して不変です。

共変な生命期の場合、Rust は必要なときにより長い寿命を持つ

&T
を短い寿命を持つ
&T
に短縮することができ、これによりネストされた生命期を省略し、コンパイラが中間の生命期を推測することを可能にします。

変更可能な借用は許容性が低く、変更はそれらの強制(coercions)をより厳密にします。

&mut T
の場合、Rust は短い寿命を持つ参照が長い寿命を持つことを約束する場所にある参照を書き込むリスクを避けるために、内部の生命期についてそれほど柔軟には扱うことができません。

この非対称性を示す具体例があります。2 つの生命期を持つ構造体を定義し、関数内で 1 つを短縮しようと試みます:

struct Inner<'a> {
    data: &'a [u8],
}

struct Outer<'short, 'long> {
    inner: &'short Inner<'long>,
}

fn shorten<'long, 'short>(outer: Outer<'short, 'long>) -> Outer<'short, 'short> { outer }

これはコンパイルされます。

&T
'long
に対して共変であるためです。関数は、データが借用の寿命を超えても参照を強制することができます。

さて、外部参照を可変に変更してみます:

struct Outer<'short, 'long> {
    inner: &'short mut Inner<'long>,
}

fn shorten<'long, 'short>(outer: Outer<'short, 'long>) -> Outer<'short, 'short> { outer }

これはコンパイルされません。

&mut T
はその生命期パラメータに対して不変であり、データが借用の寿命を超えていても短縮することはできません。生命期は正確に一致する必要があります。

これは

HeapPageViewMut::row_mut()
LogicalRowMut
の構築において起こるまさにそれです:

pub struct LogicalRowMut<'row, 'page: 'row> {
    view: &'row mut HeapPageViewMut<'page>,
}

impl<'a> HeapPageViewMut<'a> {
    /// スロットにあるライブのタプルを `LogicalRowMut` にデコードして編集します。
    /// 戻り値がドロップされる際に、変更は自動的にページに書き込まれます。
    pub fn row_mut<'row>(
        &'row mut self,
        slot: SlotId,
    ) -> SimpleDBResult<Option<LogicalRowMut<'row, 'a>>> {
        // 簡潔にするためのコード省略
        Ok(Some(LogicalRowMut {
            view: self,
            slot,
            values,
            layout,
            dirty: false,
        }))
    }
}

'page
は、すでにピンされたページバイトを借用している下位のページビューの生命期です。
'row
は、そのビューの上での 1 つの変更セッションの短い生命期です。関係
'page: 'row
は、ページビューが少なくとも mutable row エディタがそれを借用する期間と同じくらい有効である必要があることを示しています。

BufferFrame のページバイト
┌─────────────────────────────────────────────────────────────┐
│ ヘッダー │ ラインポインタ │ 未使用領域 │ タプル 2 │ タプル 1 │… │
└─────────────────────────────────────────────────────────────┘
<---------------------- 'page 用に借用 ---------------------->

                                 row_mut() の呼び出し 1 回
                                            │
                                            ▼

                       LogicalRowMut<'row, 'page>
                       ┌─────────────────────────┐
                       │ 論理的なレコードの 1 つを編集 │
                       └─────────────────────────┘
                       <---- 'row 用に借用 ----->

制約:'page : 'row

セーフ抽象化のコスト

読み取り用と書き込み用の型への分割は、実際のユーザビリティのコストをもたらします。

HeapPageViewMut
は自動的に
HeapPageView
の読み取りメソッドを取得しません。Rust 標準ライブラリでは、
&mut Vec<T>
Deref
を通じて自動的に
&Vec<T>
に強制されるため、可変参照は無償で不変メソッドを利用できます。

これは機能します。なぜなら、

Vec<T>
は内部で単一の生ポインタによって支えられているからです。それは
Deref<Target=[T]>
および
DerefMut<Target=[T]>
を実装し、どちらか
&[T]
または
&mut [T]
を提供します。1 つのポインタから、借 Checker は呼び出しサイトで参照を借用した方法に基づいて共有スライスか排他スライスかを決定します。unsafe な部分は Vec 内部にあり、一度監査され、呼び出し先からは目に見えません。

pub struct Vec<T, #[unstable(feature = "allocator_api", issue = "32838")] A: Allocator = Global> {
    buf: RawVec<T, A>,
    len: usize,
}

#[stable(feature = "rust1", since = "1.0.0")]
impl<T, A: Allocator> ops::Deref for Vec<T, A> {
    type Target = [T];

    #[inline]
    fn deref(&self) -> &[T] {
        self.as_slice()
    }
}

pub const fn as_slice(&self) -> &[T] {
    unsafe { slice::from_raw_parts(self.as_ptr(), self.len) }
}

私たちの設計はこれを行うことはできません。

PageReadGuard
PageWriteGuard
は根本的に異なるタイプ、つまり
RwLockReadGuard
RwLockWriteGuard
を保持しており、これらを単一の生ポインタに統合することはできません。
RwLock
は実行時に読み書きの区別を強制するため、コンパイル時にも 2 つの異なる型として反映する必要があります。
HeapPageViewMut
の読み取りメソッドのいずれかを追加するには、明示的に書かなければなりません。

これは Rust API 設計におけるトレードオフです。unsafe なことはありませんが、明確な能力の分離があり、しかし非対称型の得るようなユーザビリティの良さではありません。

結論

最終的には、パスはこのようになります:

┌─────────────────────────────────────────────────────────┐
│                      クエリ層 (Query Layer)              │
└────────────────────────┬────────────────────────────────┘
                         │
┌────────────────────────▼────────────────────────────────┐
│                   実行エンジン (Execution Engine)         │
└────────────────────────┬────────────────────────────────┘
                         │
┌────────────────────────▼────────────────────────────────┐
│                 トランザクションマネージャー              │
└──────────┬─────────────┴─────────────────┬──────────────┘
           │                               │
┌──────────▼──────────┐       ┌────────────▼────────────┐
│    ロックマネージャー│       │      ログマネージャー   │
└─────────────────────┘       └─────────────────────────┘
                         │ バイトに対する借用ビュー
┌────────────────────────▼────────────────────────────────┐
│                   バッファプール (Buffer Pool)            │
└────────────────────────┬────────────────────────────────┘
                         │ `O_DIRECT` はこのコピーを除去
┌────────────────────────▼────────────────────────────────┐
│                    ディスク                              │
└─────────────────────────────────────────────────────────┘

O_DIRECT
はディスクとバッファプール間のコピーを除去し、ページ/ビュー設計はバッファプール上にある上位のページオブジェクトをすでにメモリにピン留めされたバイトに対する借用ビューに変換することで、バッファプール以上の鮮やかなコピーを除去します。

私にとって、この設計に興味深い点は、所有権を 1 つの場所、つまりバッファプールに移動させることです。それより上のすべてのものは、同じセットのバイトに対するビューに関するものです。ただし、これは API のユーザビリティのコストを伴います。ゼロコピーページアクセスを構造化して、コンパイラが私が気にする同じ不変条件を見られるようにしなければならなかったからです。生命期の注釈でコードが散在していますが、特定の種類のバグを排除し、冗長なデータ移動を回避します。

同じ日のほかのニュース

一覧に戻る →

2026/04/21 5:39

ジョン・テルナス氏、次期アップル CEO に就任

## Japanese Translation: 欠落している要素は、キーポイントリストからの具体的なデータポイントおよび製品の詳細を組み込んだ改良されたバージョンを採用する価値があり、ソース資料の包括的な反映を確保するためには十分な重大さがあります。 **改善されたサマリー:** Apple は、John Ternus が 2026 年 9 月 1 日に CEO に就任し、Tim Cook を後継することを含む大規模なリーダーシップ移行を公式に確認しました。取締役会はこの計画を全会一致で承認し、Cook が 2011 年に CEO に就任してから歴史的な業績を認識しています。彼は社員の市場価値を約 3500 億ドルから 4 兆ドルへと成長させ、収益を 2011 会計年度の 1080 億ドルから 2025 会計年度の 4160 億ドル以上までほぼ四倍に拡大しました。Cook は日常業務から退き、執行議長として務める一方で、夏の間は CEO を辞任せず、円滑な引き継ぎを確保します。彼の指導の下、Apple は Apple Watch、AirPods、Vision Pro という象徴的な新カテゴリーを導入し、自社設計のシリコンに移行し、200 カ国以上および地域に店舗を持つ 500 店以上の小売網を拡大しました。また、チームメンバーが 100,000 名以上増加し、現在稼働している 25 億台超のデバイスベースを支援しています。 John Ternus は、2001 年に製品設計チームの一員として Apple に加入し、2021 年にハードウェアエンジニアリング担当副社長となり、2026 年 9 月 1 日に CEO の役職を引き継ぎ、取締役会に就任します。彼の指導の下、MacBook Neo、iPhone Air、フルモデルの iPhone 17 シリーズ、アクティブノイズキャンセレーション搭載のアバンスド AirPods、Apple Watch Ultra 3 に用いられた再生アルミニウムや 3D プリンティング钛など、重要なハードウェア革新を監督しました。さらに、Arthur Levinson は 2026 年 9 月 1 日に非執行会長からリードインディペンデントダイレクターへ移行します。この戦略的なシフトは、Apple の長期的なビジョンを固めるものであり、ハードウェア革新と持続可能性への深いコミットメントを持つ内部人材へのリーダーシップの引継ぎによって実現され、新鮮でありながら親しみのあるリーダーシップの下での継続性を確保します。

2026/04/21 6:32

「楽しさと利益のためのジュージ・メガマージ」

## 日本語訳: 記事は、JUJUTSU で導入される簡素化されたバージョン管理ワークフロー「megamerge」について紹介しています。これは、オクトパス合併(3 つ以上の親を持つ合併)を用いて、複数の開発ブランチを単一のローカルのベースコミットに統合します。不安定なブランチの先頭に直接作業を行う代わりに、開発者は関連する上流ブランチ(機能追加、バグ修正、設定など)を親とする空の megamerge コミットを作成し、作業コピーが常にすべての変更を統合してコンパイル可能になるように確保するとともに、タスクを変更する際に予期せぬリモート合併競合を排除します。 megamerge を開始するには、`jj new x y z` を実行した後に `jj commit --message "megamerge"` を実行し、指定されたブランチを親とする空のコミットを作成します。すべての書き込みは、このベース(WIP ス tack)の上で実施され、megamerge がローカルに留まることで安定性を保ちます。個々の機能ブランチは遠隔リポジトリへ通常通り公開し続けますが、megamerge 自体はプッシュされません。 `jj absorb` を用いて上流の変更を自動的に統合するワークフローでは、約 90% の更新を後続的可変コミットに圧縮して同定します。新しい作業で独自のコミットが必要になる場合は、bookmark を更新しながら WIP を megamerge の下に移動するために `jj rebase --revision y --after x --before megamerge` を使用します。並列ス tack の管理には revset アリヤス(例: `"closest_merge(to)" = "heads(::to & merges())"`)および `stack` コマンドを用い、`stage = ["stack", "closest_merge(@).. ~ empty()"]` というようにのアリヤスで一度にステージリングし、その後 `jj stage` を実行します。 メインブランチ(`trunk()`)との同期を維持するには `jj rebase --onto trunk()` を使用でき、これは自分が所有するコミットに対して動作し、他者によるブランチは保護されます。Mutable コミットのみを安全に trunk へ rebase するための場合は、`restack = ["rebase", "--onto", "trunk()", "--source", "roots(trunk()..) & mutable()"]` というようなアリヤスを使用します。全体として、このアプローチは合併による面倒を大幅に削減し、新しい作業が堅牢な統合された基盤の上に自然と構築されるような円滑で協力的なサイクルをサポートします。

2026/04/21 4:51

『Soul Player C64 ―1MHz のコモドール64で動作する本物のトランスフォーマー』

## Japanese Translation: Soul Player C64 は、未修正の Commodore 64 でネイティブ速度(約 1MHz)で完全動作する縮小版变压器モデルを実行し、画期的な成果を達成しました。このシステムは、2 レイヤーのdecoder-only アーキテクチャを実装するため、手書きの 6502/6510 アセンブリ言語を使用しており、リアルなマルチヘッド因果的自己注意機構、RMSNorm、および ソフトマックス(128 エントリのルックアップテーブル経由で)を備えており、すべてが 1 つのフロッピーディスクに収まります。主要な技術的突破口としては、6502 プロセッサの精度限界を克服しつつ有意義な重みを保つために、標準の 17 ビットではなく 14 ビットのみで注意スコアをシフトすることなどが挙げられます。 ChatGPT のような現代の巨人と並んでモデルは動作しますが、約 25,000 int8 パラメータという厳格な制約下にあります:単語書式は 128 トークン(大文字を未知として扱う)、埋め込み次元は 32、最大トレーニングコンテキストウィンドウは 20 トークンです。推論にはトークンあたり約 60 秒かかりつつも、レガシーハードウェアでのローカル機械学習の探求へのアクセシブルなパスを提供します。 このプロジェクトには、ユーザーがカスタムモデルをトレーニングするための包括的なツールが含まれています:`train.py` は Quantization-Aware Training (QAT)、FakeQuantI8、およびラベルスムージングをサポートし、重みをコンパクトなバイナリ形式にエクスポートします;`build.py` は C64 バイナリをコンパイルします;`test.py` はほぼ 90 の厳密な検証テストにより安定性を確保します。リリースパッケージにはソースファイルと即座に実行可能なビルドの両方が含まれており、高度な AI コンセプトがハードウェア変更なしでビンテージシステム上で機能することを示しています。