アセンブリと ISA 基礎

curl がついにバイトをネットワークへ送るとき、send() 呼び出しを発する。 その呼び出しはひと握りのアセンブリ命令にコンパイルされ、末尾は syscall —— 制御をカーネルへ引き渡し、本 primer を Layer 2 へ橋渡しする唯一の特別な命令 —— で終わる。 5 セクションで全体像を組み立てる:コンパイラが実際に吐くもの+ C → アセンブリの並列比較;x86 vs ARM —— 2025 年に突然重要になった CISC vs RISC のトレードオフ; 関数間でレジスタ経由で引数を渡す呼び出し規約;syscall 命令 + ユーザ / カーネル遷移ウォークスルー; 最後に クイックリファレンス —— 冷たく説明できる価値のある問い。

01

コンパイラが実際に吐くもの

ソースコードは人間のため。アセンブリが実際に走る。中間層が読めるようになれば、 あらゆる性能問題が迷信ではなく具体になる。

C、C++、Rust、Go コンパイラはソースを アセンブリ(assembly)(movaddcall のような文字ニーモニック)に変換し、 アセンブラがそれを 機械語(machine code)(CPU がデコードする生バイト)に変換する。 アセンブリは機械語の 1 対 1 の人間可読ビュー、ラベル、コメント、 そして機械語対応のない .data.text のようなひと握りの疑似命令を含む。

命令セットアーキテクチャ(ISA)は公開された契約: 合法な opcode のリスト、それらが使うレジスタ、受け入れるアドレッシングモード、それぞれの副作用。 ISA は CPU ベンダが実装するもの、ソフトウェアはそれを対象にする。 同じ C ソースを x86-64 と ARM64 用にコンパイルすると、2 つの ISA は異なる opcode を持つので、 まったく異なる機械語ながら同じことを行うものができる。

最小例

典型的な入門関数:

int add(int a, int b) {
    return a + b;
}

gcc -O2 で x86-64(Linux/Mac は SysV ABI)向けにコンパイルすると、吐かれるアセンブリは:

add:
    lea  eax, [rdi + rsi]   ; 2 つの引数レジスタを加算
    ret                     ; 戻り番地をポップ、ジャンプ

2 命令。呼び出し規約により、引数は rdirsi に到着する (次節で説明する);戻り値は eax(rax の下位 32 ビット)に残される。 メモリは関与しない —— 小さなリーフ関数は完全にレジスタ内に住み、現代コードがこれほど速い理由の 1 つ。lea(load effective address)命令は加算のために濫用される:lea eax, [rdi + rsi] は 1 命令で rdi + rsi を計算して格納し、 キャリーフラグを触らない —— コンパイラの一般的なトリック。

ARM64 上の同じ関数

add:
    add  w0, w0, w1         ; 引数 0 = 引数 0 + 引数 1
    ret                     ; リンクレジスタへジャンプ

2 命令、構造的に同じ仕事、異なるニーモニックとレジスタ名。 ARM の SysV スタイル ABI は最初の 8 整数引数を x0x7(32 ビット int は w0w7)で渡し、戻り値は x0/w0。 ARM の ret はリンクレジスタ(lr = x30)へジャンプし、 そこには呼び出し元の bl(branch with link)命令がセットした戻り番地が入っている。

なぜアセンブリを読むのか

アセンブリの読解力が報われる 3 つの具体的瞬間:

  • プロファイラ属性。perf report はホット命令をアセンブリで表示する。 各ニーモニックのコスト —— lea は 1 サイクル、div は 20+、 メモリへの mov はヒット 1 サイクル / ミス ~100 —— を知っていれば、 「ソース 1 行のパーセンテージ」を実行可能な仮説に変えられる。
  • コンパイラの説明。コンパイラはそのループをベクトル化したか? 配列境界チェックを消したか?std::move はコピーを回避したか? アセンブリが地上の真実、ソースは願望思考。Compiler Explorer (godbolt.org) がソースとアセンブリを並べる、これを教える最高のツール。
  • マイクロアーキテクチャデバッグ。分岐誤予測、キャッシュミス、 ロックフリーコードのメモリ順序バグ、期待通りに動かないアトミック操作 —— これらはすべてソースレベルではなくアセンブリレベルで説明される。 強いメモリモデルを持つ言語(Java、Rust)はこれを稀にするが、C/C++ ではシニアレベルで不可欠。

アセンブリを書く必要はない。perf report の 20 行関数が 不透明に感じなくなる程度に読めればよい。

要点。「ソースはアセンブリに、アセンブリは機械語にコンパイルされる; アセンブリは CPU が実際に実行するものの人間可読ビューに過ぎない。 ISA はハードウェアとソフトウェアの間の契約。アセンブリを読むことが 『コンパイラが何かをした』を信念から知識に変える方法 —— Compiler Explorer が最も簡単な入り口。」

02

x86 vs ARM —— CISC vs RISC、実際の意味

2 大ファミリーが世界を支配する。1980 年代に命令長、レジスタ数、アドレッシング複雑度について 異なる選択をした。40 年間ハードウェアに固定化されたその選択が、 あなたのラップトップが今 ARM である理由を決める。

x86-64(Intel と AMD)は CISC 設計。命令は可変長 —— 各 1 から 15 バイト。汎用 64 ビットレジスタは 16 個(32 ビット x86 では元々 8 個、REX 接頭辞で 16 に拡張)。 数百の異なる opcode が複雑なアドレッシングモードをカバーする:mov rax, [rbx + rcx*8 + 16] は 1 命令で、 「1 つのベースレジスタ + 8 でスケールされた 1 つのインデックスレジスタ + 即値オフセット」から 計算されたアドレスでメモリを読む。CPU フロントエンドは命令の境界を判別するのに実際の労力を割く必要がある。

ARM64(AArch64)は RISC 設計。命令は固定長 —— 各正確に 4 バイト。汎用 64 ビットレジスタは 31 個。opcode セットがより小さく規則的。 複雑なアドレッシングモードはない:[rbx + rcx*8 + 16] からのロードは ARM では 2〜3 のより単純な命令を要する。デコードハードウェアは相応により単純で小さく、 これが直接命令あたりの低電力に翻訳される。

なぜ当時トレードオフが重要だったか

  • コード密度。CISC はバイトあたりより多くの意味を詰めるので、バイナリが小さい。 メモリが高価だった 1980 年代には極めて重要。ARM の固定 4 バイト命令は ディスク上で等価な x86 コードより 30% 大きい場合がある、 ARM の Thumb モード(ホットコード用 16 ビット命令)で一部相殺される。
  • デコーダの複雑性。x86 デコーダは命令境界を見つけるためにバイトを直列にスキャンする必要がある (最長命令接頭辞は助けにならない —— 部分的にデコードするまで長さが分からない)。 ARM の 4 バイトアラインメントはデコーダが N 命令を並列に取得・デコードすることを自明にする。 これが ARM コアがワットあたりより高いデコード幅に到達する理由の一部。
  • レジスタファイルサイズ。レジスタが多いほどメモリへの spill なしで より多くの生きた値を保持でき、spill はコンパイラができる最も高価なことの 1 つ。 ARM の 31 GPR は x86 の 16 個に対してコンパイラに真の人間工学的勝利、 特に JIT コンパイルされた JavaScript のようなレジスタ密度の高いコード。

なぜトレードオフが今重要か

2020 年代が歴史的風景を逆転させた。3 つが推進した:

  • Apple Silicon。M1(2020)は十分な R&D を投じた ARM コアが Intel の最高峰を大幅に低い電力で凌駕できることを示した —— コアあたり持続 5–10W vs Intel の 25W+。 4 年内に出荷される全 Mac が ARM。
  • AWS Graviton。ARM ベースのサーバチップがドルあたりの性能で 20–30% 良く、 主要なクラウド移行を駆動した。2024 年までに AWS の新 EC2 インスタンスの半分が Graviton。 全ての主要 web サービスが今やマルチアーキ配備をサポートしなければならない。
  • スマートフォン。ARM は常にスマホの ISA だったが、2011 年の ARMv8 で デスクトップコアと競争力を持つようになった。今日ポケットの中のスマホは 2015 年のサーバ CPU より高いシングルスレッド IPC を持つ。

ソフトウェアにとって何を意味するか

アプリケーションエンジニアにとって、違いは主に 3 箇所で現れる:

  • プロファイラのアセンブリの見た目が違う。同じアルゴリズムだが、 ニーモニック、レジスタ名、命令数が異なる。両方を読めるようになるには練習が要るが平易。
  • アトミックメモリ順序の意外。x86 は TSO(Total Store Order)—— 多くの操作はすでに無償で逐次整合的。ARM は弱順序 —— 明示的バリアなしの x86 で動く素朴なロックフリーコードは ARM では静かに壊れる。 これは最頻のクロスアーキ移植バグ。
  • コンテナイメージとバイナリ配布。Docker イメージはamd64arm64 の両方を宣言・ビルドする必要がある;docker buildx とマニフェストリストがこれを扱う。 GitHub releases の事前ビルドバイナリは両アーキを持つ必要がある。 マルチアーキビルドのコストは実在するが小さい。

要点。「x86 は CISC(可変長命令、16 レジスタ、複雑なアドレッシング); ARM64 は RISC(4 バイト固定、31 レジスタ、単純なアドレッシング)。 ARM のより単純なデコーダと大きなレジスタファイルが優れた電力効率に翻訳され、 Apple Silicon、AWS Graviton、全スマートフォンが ARM を走らせる理由。 最大のソフトウェア意外はメモリ順序:x86 は TSO(寛容)、ARM は弱順序 (ロックフリーコードに明示的バリアが必要)。」

03

呼び出し規約 —— 関数同士がどう話すか

関数は別の関数を呼ぶには合意が要る:引数がどこへ行くか、戻り値がどこから返るか、 どのレジスタを保持しなければならないか。その合意が呼び出し規約 —— システム上のあらゆるバイナリ間の契約。

呼び出し規約は、GCC でコンパイルされたコードが Clang コンパイルコード、C ライブラリ、 カーネルを透過的に呼べるようにするルール。各 OS+アーキテクチャに正典の ABI (Application Binary Interface)があり、すべてがそれを対象とする。 Linux/x86-64 では System V AMD64 ABI。 Windows/x86-64 では Microsoft x64 呼び出し規約。Apple/ARM64 では AAPCS64 のカスタム変種。 細部は異なるが構造は同じ。

SysV AMD64 を 1 画面に

引数レジスタ(整数):    rdi, rsi, rdx, rcx, r8, r9  (6 引数)
引数レジスタ(浮動):    xmm0..xmm7                  (8 引数)
戻り値(整数):          rax  (128 ビット int は rdx)
戻り値(浮動):          xmm0

caller-saved レジスタ:  rax, rcx, rdx, rsi, rdi, r8..r11
                        (caller が呼び出しを跨いで保存が必要なら自分で保存)

callee-saved レジスタ:  rbx, rbp, r12..r15
                        (callee が戻る前に復元しなければならない)

スタック:               呼び出し地点で 16 バイトアライン;下方向に伸びる;
                        rsp の下に 128 バイトのレッドゾーン。

多くの関数呼び出しで、最初の 6 整数引数と最初の 8 浮動小数点引数はすべてレジスタに収まり、 呼び出し自体はメモリを一切触らない。それを超える引数はスタックにプッシュされる。 戻り値は rax または xmm0 で返る。スタックアラインメントは 16 バイト —— SSE 命令がスタック上に確保されたベクタにトラップなしで動作できるための要件。

caller-saved vs callee-saved

この分割が存在するのは、caller と callee で物を保存するコストが異なるから。 caller-saved レジスタ(「スクラッチ」または「volatile」とも呼ばれる): 呼び出し関数は呼び出しを跨いで気にする値を知っており、それらだけを保存する。 大半を気にしなければ安い。callee-saved レジスタ: 呼ばれる関数は実際に使うものを知っており、それらだけを保存する。 関数が大半の callee-saved レジスタを使わなければ安い。

実務的にはコンパイラが変数ごとにどのタイプのレジスタに格納するかを決める。 ループカウンタや内側ループの一時値は通常 caller-saved レジスタに入る (ループ内で安く、作業中にホット)。関数呼び出しを越えて生存する長寿命値は callee-saved レジスタに入る(関数入口で一度保存、戻り際に一度復元 —— 呼び出しごとに再保存するよりずっと安い)。

スタックフレーム

高アドレス
   ┌──────────────────────┐
   │  caller の引数 #7..#n│  (6 整数引数を超える場合)
   ├──────────────────────┤
   │     戻り番地           │  ← 'call' がプッシュ
   ├──────────────────────┤  ← 入口時の caller の rsp
   │  保存された rbp(任意) │
   ├──────────────────────┤  ← callee がここに rbp を設定
   │     ローカル変数        │
   │   スピルされたレジスタ   │
   ├──────────────────────┤  ← callee の現在の rsp
   │  レッドゾーン(128 B)  │  ← rsp 調整なしで使える
   ├──────────────────────┤
低アドレス

関数呼び出しは戻り番地をスタックにプッシュしてジャンプする。callee プロローグは通常 フレームポインタをセットアップし(push rbp; mov rbp, rsp)、 ローカル用の領域を確保する(sub rsp, N)。 callee エピローグはこれを逆転させる(add rsp, N; pop rbp; ret)。 リーフ関数は命令節約のためフレームポインタを省くことが多い。

rsp の下の 128 バイトのレッドゾーンは、rsp を明示的に調整せずにリーフ関数のスクラッチ領域として予約される。 シグナルハンドラはそれを触ってはならない。シグナル安全コードが通常コードと異なる小さな細部の 1 つ。

なぜデバッグと FFI に重要か

  • スタックトレースデコード。各スタックフレームは規約に従うので、 バックトレースは rbp(フレームポインタあり)または DWARF unwind 情報の読み出し (なし)でフレームを辿れる。規約が破られると(例:インラインアセンブリが callee-saved レジスタを宣言なしに clobber する)、バックトレースが壊れ、 クラッシュが不透明になる。
  • FFI(Foreign Function Interface)。Python、Rust、Go、Java などから C を呼ぶのは常に共通言語として C ABI を通る。 各言語の FFI 層は引数を規約の期待するレジスタとスタックスロットへマーシャルする。 ミスマッチ(型幅の誤り、アラインメント不足)はメモリ破壊バグの一般的な原因。
  • カスタム呼び出し規約。JIT(V8、LuaJIT、Java の JIT)はしばしば、 より多くの引数をレジスタで渡すか、プロローグ / エピローグ作業をスキップする独自の内部規約を発明する。 C コードに渡るときには ABI 規約に切り替える必要がある。

要点。「呼び出し規約はシステム上のあらゆるバイナリが互いを呼べるようにする契約。 Linux/x86-64(SysV AMD64)では:最初の 6 整数引数は rdi/rsi/rdx/rcx/r8/r9、 戻り値は rax、callee-saved は rbx/rbp/r12-r15、 呼び出し地点でスタック 16 バイトアライン。FFI、スタックトレース、JIT 設計が すべてこれを知ることに依存する。」

04

Syscall —— 扉を開ける特別な命令

どの ISA でも数百ある命令のうち、ちょうど 1 つだけが CPU をユーザモードから カーネルモードへ切り替える。プログラムが行うあらゆる readwriteopenforkmmap がここを通る。 本節はその数ナノ秒の間に何が起こるかについて。

x86-64 または ARM64 の数百の命令のうち、ちょうど 1 つが特別: CPU をユーザモード(x86 で特権 ring 3、ARM で EL0)からカーネルモード(ring 0 / EL1)へ切り替える命令。 x86-64 では syscall 命令;ARM64 では svc #0。 プログラムが実行する他のあらゆる命令 —— ロード、ストア、加算、分岐 —— はユーザモードで動き、 CPU はプログラムがカーネルのマップしたメモリとデバイスしか触れないよう強制する。

アトミックに変化するもの

syscall 命令で、ハードウェアはいくつかの状態をアトミックに変える。 アトミック性こそが境界を安全にする:ユーザコードがカーネル特権で動いている瞬間、 またはカーネルコードがユーザページテーブルで動いている瞬間は存在しない。

  • 特権 ring 3 → 0。CPU のステータスレジスタのフラグが変わる。 この瞬間からプログラムは特権命令を実行でき、マップされたカーネルメモリにアクセスできる。
  • ページテーブルベース切り替え。x86 では CR3 レジスタがカーネルの ページテーブルベースをロードする。Meltdown(2018)が KPTI(Kernel Page Table Isolation) を必須にして以来、ユーザとカーネルは別々のページテーブルを持つ —— 切り替えは実コスト(TLB フラッシュ)。
  • スタックポインタ切り替え。ユーザモードの rsp が保存され、 スレッドごとのカーネルスタック(CPU の MSR に保持される)がロードされる。
  • 命令ポインタがジャンプする先は、x86-64 で LSTAR model-specific レジスタに記録されたカーネルの syscall エントリポイント。

カーネル側

カーネルは指定されたレジスタ(Linux/x86-64 では rax、Linux/ARM64 では x8) から番号を読み、どの syscall が要求されたか識別する。その番号をsys_call_table(syscall ごとに 1 つの関数ポインタを持つテーブル)のインデックスに使い、 対応するハンドラを呼ぶ。引数レジスタ(Linux/x86-64 の syscall ではrdi/rsi/rdx/r10/r8/r9;rcx でなく r10 なのは、 syscall 命令自体が rcx を clobber するから)が引数を運ぶ。

syscall 遷移 —— ユーザコードからカーネルを通って戻る 8 ステップユーザ:syscall 引数を引数レジスタへロードユーザモード; user-mode write(fd, buf, n): mov rdi, rax_fd ; arg 1 mov rsi, rax_buf ; arg 2 mov rdx, rcx_n ; arg 3 mov rax, 1 ; syscall # = write syscall ; ← ; ... continues after kernel returns mov rax_bytes, rax ; return valueカーネルモード; entry_SYSCALL_64: swapgs ; load kernel gs mov rsp, kernel_stack call sys_call_table[rax] ; → sys_write(fd, buf, n) ; sockfs_write → tcp_sendmsg → ... ; bytes_written into rax swapgs ; restore user gs sysretq ; ←ring = 3ユーザページテーブル
SysV ABI:最初の 6 整数引数は rdi、rsi、rdx、rcx、r8、r9。コンパイラが呼び出し準備中にこれらの mov を吐く。
1 / 8
syscall 上で 3 つがアトミックに変化する:特権リング(3 → 0)、 ページテーブルベース(ユーザ → カーネル、Spectre / Meltdown 以降必須)、 スタックポインタ(ユーザスタック → スレッドごとのカーネルスタック)。 カーネルは rax から syscall 番号を読み、固定テーブルでディスパッチし、 ハンドラを実行し、戻り値を rax に書き戻し、sysret が遷移を逆転する。 プログラムのあらゆる I/O、あらゆるメモリ割り当て、あらゆる fork がこの同じ列を通る。

コスト

syscall 自体は現代ハードウェアで 100–300 ns かかる(post-Meltdown の KPTI 込み)。 これは実際の作業の前 —— socket バッファからの read は syscall コスト + メモリコピー; ディスクからの read は syscall コスト + I/O レイテンシ。 syscall オーバーヘッドが床:カーネル遷移を含むあらゆることがそれより速くなることはない。

これが高性能サーバが syscall を回避するために大いに努力する理由。epoll は N 個の socket 準備クエリを N 個ではなく 1 つの syscall に凝縮する。io_uring はさらに進む —— ユーザ空間とカーネルが進行中操作のリングバッファを共有し、 I/O の提出と刈り取りが syscall なしで行われる。sendfilesplice はカーネルがファイルディスクリプタ間でバイトを ユーザ空間を経由せずにコピーできるようにする。

vDSO —— syscall に見える非 syscall

頻繁に使われる「syscall」のいくつか —— gettimeofdayclock_gettimegetcpu —— は vDSO(virtual Dynamic Shared Object)と呼ばれる 共有ユーザ空間ライブラリで実装され、カーネルがそれを全プロセスにマップする。 実装はカーネル管理データを読み取り専用ページから読み、即座に返す、特権遷移なし。 これはレイテンシを ~150 ns から ~15 ns に下げる —— これらを常に呼ぶ時間敏感なコード(ロギング、プロファイラ、トレースツール)に重要。

要点。「ISA のあらゆる命令のうちちょうど 1 つが特権レベルを切り替え、 ユーザコードをカーネルへ橋渡しする —— x86-64 では syscall、 ARM64 では svc #0。遷移はアトミック:リング、ページテーブル、スタックが すべて一緒に切り替わる。コスト(~100–300 ns)こそが、高スループットサーバがepollio_uringsendfile、vDSO を使って syscall そのものを回避する理由。」

05

クイックリファレンス

冷たく説明できる価値のある 6 つの核心問題と、コードレビューで一目で見抜くべき 5 つの赤旗。 まず質問を頭に刻み込む;回答骨子はその下に。

アセンブリ、機械語、ISA の違いは?

機械語は CPU がデコードする生バイト。アセンブリはそれらバイトの 1 対 1 人間可読ビュー —— 同じ命令、ニーモニックとラベルだけ。ISA(Instruction Set Architecture)は どの opcode が存在し、それぞれが何をするかを規定する公開された契約 —— CPU ベンダが実装するもの。

なぜ ARM が同等性能で x86 より電力効率が良いか?

2 つの構造的理由:固定長 4 バイト命令はデコーダが N-way 並列を自明に実行可能にする (vs x86 の境界を見つけるバイト直列スキャン);31 レジスタ vs 16 はメモリ spill を減らす、 spill は最も電力を消費する操作。加えて Apple/Qualcomm/ARM は近年 ARM 設計に Intel が x86 にかける以上の R&D 予算を費やしている。 純効果は同等ワークロードで 2〜3 倍の perf/watt。

SysV AMD64 で関数引数を運ぶレジスタは?

最初の 6 整数引数は rdi, rsi, rdx, rcx, r8, r9; 最初の 8 浮動小数点引数は xmm0..xmm7;それ以上はスタック。 戻り値:rax(128 ビット整数では rax+rdx、浮動小数点では xmm0)。 syscall では 4 番目の引数レジスタが rcx ではなく r10 ——syscall 命令自体が rcx を clobber するから。

syscall 命令で何が起こるか歩く。

アトミックに:特権 ring 3 → 0;ページテーブルベースがユーザからカーネルへスワップ (KPTI、Meltdown 2018 以降必須);スタックポインタがユーザスタックからスレッドごとの カーネルスタックへスワップ;命令ポインタがカーネルの syscall エントリへジャンプ (x86-64 では LSTAR に記録)。その後カーネルは rax を読み syscall 番号を取得、sys_call_table にインデックスし、ハンドラを呼び、戻り値を rax に書き戻し、sysret がすべてをアトミックに逆転する。

なぜ syscall が高価とされるか、何の技術がコストを下げるか?

syscall は現代ハードウェアで遷移だけで ~100–300 ns(KPTI 込み)、実作業の前。 削減技術:epoll は N 個の準備クエリを 1 syscall に凝縮する;io_uring はカーネルとリングバッファを共有し提出 / 完了が syscall なしで行われる;sendfile はカーネルが fd 間でバイトをユーザ空間を経由せずに動かせる; vDSO は一般的な読み出し(clock_gettime)をカーネルからマップされた ユーザ空間メモリで実装し、遷移を完全に回避する。

caller-saved と callee-saved レジスタの違いは?

両方とも規約。caller-saved(「volatile」)レジスタはあらゆる関数呼び出しで上書きされ得る; 呼び出し元が値を保持したければ自分で保存しなければならない。 callee-saved(「non-volatile」)レジスタは呼び出しを跨いで保持される —— 被呼関数は使うなら入口で保存し、戻り際に復元しなければならない。 この分割が存在するのは呼び出し元と被呼関数のコストが異なるから; コンパイラは寿命に基づいてどちらを使うか選ぶ。

コードレビューの赤旗

  • 適切な clobber 宣言のないインラインアセンブリ。asm volatile を使う関数はどのレジスタとメモリを変更するかコンパイラに伝える必要がある。 clobber の欠落は静かな破壊を引き起こし、コンパイラが asm ブロックを跨いで clobber されたレジスタに値を保持することを決めたときにのみ顕在化する。
  • ARM で実行されることを意図した、明示的メモリバリアのないロックフリーコード。x86(TSO)で動くコードが ARM(弱順序)で静かに壊れる。 直し方は各アトミック操作に明示的な atomic_thread_fence または 適切な memory_order
  • 引数幅が不一致な FFI 宣言。Python や Rust から間違った型幅で C 関数を呼ぶと、 規約が次の呼び出し地点で見つけることを期待するレジスタ / スタックを破壊する。cffibindgen、Rust の libc クレートが 正しいバインディング生成のために存在する。
  • 反復ごとに多くの syscall を発するタイトなループ。socket からの各 read が 1 syscall;1 メガバイトを 1 バイトずつ読むと 10 万 syscall になる。 ユーザ空間でバッファし、キロバイトごとに 1 回呼ぶ(または readv を使う)で償却する。
  • 2025 年に走るコードのシングルアーキ Docker イメージ。FROM alpinearm64 を指定または対応していなければ、 Graviton、M シリーズ Mac、あらゆる現代の開発環境で失敗する。docker buildx--platform linux/amd64,linux/arm64 で使う。