TechCraft – エンジニアのためのスキルアップメモ

エンジニアのスキルアップを少しでも加速する技術ブログ

OS を作って理解したい

はじめに

OS自作系の本を2冊、読みたくなった。

普段は組込みCを書いている。RTOSもスケジューラも「使う側」としては慣れているつもりだが、中身を自分の手で組み立て直したことがあるかと言われると、ちょっと自信がない。

そんなタイミングで、OS自作系の本が続けて出る。

  • 2026年9月18日発売:『つくりながら学ぶ! リアルタイムOS自作入門』
  • 2026年10月26日発売:『【改訂新版】作って理解するOS x86系コンピュータを動かす理論と実装』

どちらもまだ読んでいない。この記事は読んだ感想ではなく、なぜ読みたいのかを残しておく読む前のメモ。読んだら印象が変わるかもしれない。

気になっている2冊

1. つくりながら学ぶ! リアルタイムOS自作入門 ― 小さく一周できそう

つくりながら学ぶ! リアルタイムOS自作入門 (Compass Booksシリーズ) | 矢野倉伊織 |本 | 通販 | Amazon

項目 内容
著者 矢野倉伊織
出版社 マイナビ出版(Compass Books シリーズ)
発売日 2026年9月18日
規模 368ページ(B5変形)、4,730円
題材 Arduino UNO R4 WiFi(Arm Cortex-M4)上で動くプリエンプティブな RTOS を作る

東京大学工学部の演習教材がもとになっている本。ブートシーケンスから始めて、コンテキストスイッチ、プリエンプティブなスケジューラ、シェル、メモリ保護まで、少しずつ OS を組み上げていく構成になっている。

ここが気になる

  • 小さなマイコンの上で、OSの芯の部分を一通り作る。コンテキストスイッチとスケジューラを自分で書けるのが一番の魅力
  • FreeRTOS との比較の章がある。自作したものと、実際に使われている RTOS を見比べられそう
  • アドバンス編が実務寄り。食事する哲学者問題による同期処理、ユーザー/カーネルモードとシステムコール、ヒープの実装、Flash 上の簡易ファイルシステムが並んでいる
  • OSからロボット制御まで繋がる。PWM や ADC のハードウェア制御に加えて、強化学習で四脚ロボットを歩かせる章まである。PiCar-X で遊んでいる身としては、この「OSの上でロボットを動かす」流れがかなり気になる
  • 敷居は低そう。サポートページによると、実機がなくてもブラウザ上の仮想ボードで完成版のファームウェアを動かせるらしい

読む前に確認したいこと

  • OS本体を何の言語で実装するのか(公式ページには書いていなかった)。ここは立ち読みか試し読みで確かめたい
  • 開発環境は Arduino IDE と PlatformIO。標準は PlatformIO とのこと

2. 【改訂新版】作って理解するOS ― 腰を据えて読む本

【改訂新版】作って理解するOS x86系コンピュータを動かす理論と実装 | 林 高勲, 川合 秀実 |本 | 通販 | Amazon

項目 内容
著者・監修 林高勲 著、川合秀実 監修
出版社 技術評論社
発売日 2026年10月26日
規模 1,072ページ(B5変形)、4,840円
題材 x86 系 PC で動く OS を、32ビット(BIOS)と 64ビット(UEFI)の両方で作る

2019年に出た本の改訂新版。64ビットモードと UEFI に対応し、300ページ超の大幅な増補が入っている。構成は4部。

  1. コンピュータの基礎を理解する(ハードウェア、ソフトウェア、メモリ管理、ファイルシステム、入出力)
  2. x86系PCのアーキテクチャを理解する(レジスタ、命令、NASM、周辺機器、BIOS)
  3. BIOS環境で起動する32ビットOSを実装する
  4. UEFI環境で起動する64ビットOSを実装する

ここが気になる

  • 理論と実装が一冊で繋がっている。第1部・第2部で仕組みを押さえてから、第3部・第4部で実際に作る流れ
  • OSの定番テーマが章立てにそのまま並んでいる。プロテクトモードへの移行、割り込み、マルチタスク、特権状態の管理、ページング、ファイルシステム。教科書で見た話を実装で追えそう
  • 「今のPC」に近いところまで行く。第4部で UEFI 環境の 64ビットOS を作るので、古い話だけで終わらなさそう
  • イントロダクションが二進数とC言語から始まる。基礎から入れるのは地味にありがたい
  • 監修の川合秀実さんは『30日でできる!OS自作入門』の著者でもある。OS自作の入口として信頼できそう

覚悟が要りそうなこと

  • 1,072ページ。通読というより、少しずつ進める本になりそう(正直、ちょっと怖い)

2冊を並べてみる

リアルタイムOS自作入門 作って理解するOS
動かす場所 Arduino UNO R4 WiFi(Cortex-M4) x86 系 PC
規模 368ページ 1,072ページ
得られそうなもの 小さな系で RTOS の芯を一周する経験 OS を支えるハードウェアと理論の全体像
気になるテーマ スケジューラ、メモリ保護、ロボット制御 プロテクトモード、ページング、UEFI

小さく一周できそうな RTOS 本と、広く深く掘れそうな x86 本。どちらも「OSを自分で作る」本だが、見える景色は違いそうなのだ。両方読みたい理由はここにある。

読んで何に効かせたいか

RTOSを「使う側」から「分かって使う側」へ

RTOS の API は日々使っているが、タスクが切り替わるときに裏で何が起きているのかを、改めて自分の手で確かめたい。中身が分かっていれば、設計判断の根拠も変わってくると思う。

Cで抽象化の層を作る練習

タスク管理、ドライバの境界、システムコール。OSは、Cで抽象化の層を作る題材としていちばん分かりやすいと思っている。設計の引き出しを増やす目的でも読みたい。

SDV遊びの足腰

今回の Raspberry Pi 5 は Arm なので、x86 の本の知識がそのまま使えるわけではない。ただ、割り込み、マルチタスク、特権、ページングといったOSの共通概念は同じ。

SDV の世界では、リアルタイム性が必要な部分(RTOS やマイコン)と、Linux のような汎用OSが同居する。SEA-ME の GitHub に Mixed-Criticality-System というリポジトリがあるのも見かけた。両方のOSを分かっている状態は、SDV を考える土台になりそうだ。

読み進め方

  • まず9月発売の RTOS 本で、小さく一周する
  • 10月26日発売の x86 本は、届いたら腰を据えて進める
  • 積ん読にだけはしたくない
  • 読み終わったら、感想と、SDV遊びに還元できたことを書く

まとめ

  • OSの中身を、小さな系(RTOS)と大きな系(x86)の両方で作って理解したい
  • 気になっているのは『つくりながら学ぶ! リアルタイムOS自作入門』と『【改訂新版】作って理解するOS』
  • RTOS は「使う側」から「分かって使う側」へ。SDV 遊びの足腰にもしたい

読み終わったら、感想と学びをまた記事にできればと思う。

参考

PiCar-X と SDV で実機を動かす ― Raspberry Pi 5 上に「車両ソフトの基盤」を載せてみる

はじめに

SDV(Software Defined Vehicle)は、資料やスライドだと概念の話が中心になりがちで、「結局どう動くの?」が掴みにくいと感じていました。 そこで、趣味で ラズパイ(Raspberry Pi 5)を搭載したラジコンカー(PiCar-X)の上に、SDV 風のソフトウェア構成を載せて実機を動かす ことにしました。

この記事では次の流れで進めます。

  1. 用意したもの
  2. GitHub で見つけた SDV 系リポジトリの紹介(何を参考にするか)
  3. PiCar-X にどうマッピングするか(設計)
  4. 実際の構築手順
  5. 動かしてみた結果・今後

「本物の SDV を作る」のではなく、SDV の考え方(ハードウェア抽象化・信号の標準化・アプリの分離)を小さな実機で体験する ことがゴールです。

用意したもの

もの 用途
PiCar-X(Raspberry Pi 用) 実機。モーター、ステアリング、カメラ用サーボ、超音波センサ等を載せたラジコンカー
microSD カード 256GB ラズパイの OS・コンテナイメージ・ログ置き場。余裕を持たせたいので大きめを選びました
Raspberry Pi 5 PiCar-X に載せる本体。車載コンピュータ役
  • PiCar-X:

Amazon.co.jp: SunFounder PiCar-X Raspberry Pi用 AIロボット スマートカーキット Raspberry Pi 5/4/3B+/Zero 2 W対応 OpenClaw・ChatGPT・Gemini・GrokなどのマルチLLM対応 音声・画像認識 Python・Scratch カメラ付き(※Raspberry Pi本体は含まれません) : パソコン・周辺機器

PiCar-X は、ラズパイをコントロール中枢にして、左右の駆動モーターとステアリング/カメラ用サーボを専用の Robot HAT で制御する構成です。ソフトは Python で書け、公式のライブラリが GitHub で公開されています。

なお PiCar-X のキットにはラズパイ本体は付属しません。SunFounder の製品ページでは、対応ボードとして Raspberry Pi 5 / 4 / 3B+ などが挙げられており、今回はその中から Pi 5 を選びました。Pi 5 を選んだ理由はできるならなるべく高いスペックで動かしたいよね。 それこそADASとかで動かすかもしれないし、そういう余裕しろのためになるべくいいモノに。

GitHub の SDV 系リポジトリを調べてみた

「SDV」で GitHub を検索すると、Eclipse SDV 関連がまとまって見つかります。今回、構成の参考にしたものを紹介します。

1. eclipse-sdv-blueprints/service-to-signal

「車両のサービスを、実際のハードウェアにつなぐ」ことを見せるサンプルです。 COVESA の VSS(Vehicle Signal Specification)で定義された Vehicle.Body.Horn.IsActive というシグナルを Eclipse Kuksa Databroker で管理し、そのシグナルと Zenoh 経由のメッセージを橋渡しして、ホーンのハードウェアを動かす、という構成になっています。

→ PiCar-X に一番マッピングしやすい題材。「ホーンのシグナルが true になったらスピーカーを鳴らす」を PiCar-X でやれば、そのまま実機版になります。

2. eclipse-velocitas/vehicle-app-python-sdk(と vehicle-app-python-template)

車両アプリ(Vehicle App)を Python で書くための SDK とテンプレートです。サンプルにはシート調整アプリがあり、MQTT で受けた要求を、アクチュエータ用のデータポイント経由で制御する流れが示されています。

→ アプリ層を「車両ハードウェアの詳細を知らない形」で書く考え方の参考にします。

3. eclipse-sdv-blueprints/fleet-management

車両のデータを VSS の形でバックエンドへ送り、Grafana のダッシュボードで見る、というブループリントです。センサ値の代わりに CSV の再生データを Kuksa の CSV Provider で流し込み、Docker Compose で一式起動します。

→ 「CSV の代わりに PiCar-X の実センサ値 を流し込む」ことで、実機版にできそうです。

4. SEA-ME(PiRacer 系の教育プロジェクト群)

SDV/SdV 人材育成を掲げる教育プログラムの GitHub で、ラズパイ + ラジコンカー(PiRacer) を題材に、メータークラスタやヘッドユニットの Qt アプリ、車線検出、OTA など、実機で SDV を学ぶリポジトリが並んでいます。

→ 発想はまさに今回のやりたいことと同じ。「小さな実機で SDV を学ぶ」先行事例として、全体像の参考になります。

5. boschresearch/kuksa.hardware(Eclipse KUKSA CANOPi)

Raspberry Pi Compute Module 4 向けのベースボードで、CAN-FD や OBD-II を備え、実車でのプロトタイピングを想定したハードです。

→ PiCar-X には CAN がないため今回は使いませんが、「本物の車ならここが CAN になる」という 対比 として押さえておく価値があります。

6. Donkeycar(Python 自動運転ライブラリ)

ラズパイ搭載のラジコンカー向けライブラリで、「パーツ」をパイプライン状に並べ、車両ループの中で毎周期、入力を読んで出力を書く、という設計です。

→ SDV とは用語が違いますが、「ハードウェア抽象 → ロジック → アクチュエータ」の分け方は参考になります。

PiCar-X 側のリポジトリ

  • sunfounder/picar-x(本体ライブラリ)、robot-hat、vilib(カメラ/画像処理)
  • 手元でどう書けるか試す用に、Go 製の emergingrobotics/gopicar のような別言語ドライバもあります

設計:SDV の考え方を PiCar-X に当てはめる

全体構成

┌──────────────────────────────────────┐
│ アプリ層 (Vehicle App)               │
│  ・障害物で自動停止  ・ホーン操作    │
└───────────────▲──────────────────────┘
                │ VSS シグナルだけを読み書き
┌───────────────▼──────────────────────┐
│ 車両抽象層                           │
│  Kuksa Databroker (VSS シグナル)     │
└───────────────▲──────────────────────┘
                │
┌───────────────▼──────────────────────┐
│ ハードウェア層                       │
│  Provider (picar-x ライブラリ)       │
│  ─ モーター/サーボ/超音波/スピーカー │
│  ※ 動作基盤: Raspberry Pi 5          │
└──────────────────────────────────────┘

SDV らしさのポイントは アプリが picar-x ライブラリを直接呼ばない ことです。アプリは VSS のシグナルだけを見て、PiCar-X 固有の処理は Provider に閉じ込めます。こうすると、PiCar-X を別の車体に差し替えても、アプリは変えずに済みます。

シグナルのマッピング(案)

やりたいこと VSS のシグナル(案) PiCar-X 側
ホーンを鳴らす Vehicle.Body.Horn.IsActive スピーカーでビープ音
ステアリング VSS の操舵系シグナル or 独自拡張 ステアリング用サーボ
前進/後退 VSS に適当なものがなければ独自拡張 左右駆動モーター
前方距離 VSS の ADAS 系 or 独自拡張 超音波センサ

※ VSS に該当が無いものは、独自のシグナルを追加定義する前提です。シグナル名は VSS のカタログで確認してから決めます。 ※ PiCar-X には車速センサがないので、「速度」はモーターの出力指令値であり、実速度ではない点に注意が必要です。

作るシナリオ

  1. ホーン:Horn.IsActive を true にするとスピーカーが鳴る(service-to-signal の PiCar-X 版)
  2. 自動停止:前方距離が閾値以下になったら、アプリが停止要求を出す
  3. 見える化:センサ値を Grafana で見る(fleet-management の PiCar-X 版)

構築手順

1. OS とマイクロ SD

256GB の microSD に、Raspberry Pi 5 に対応した Raspberry Pi OS を書き込みます。

Pi 5 でのセットアップ手順は、SunFounder の公式ドキュメントに最新のものがあります。ライブラリのブランチ指定などは更新されるため、下の手順も含めて必ず公式を確認してください。

2. PiCar-X のライブラリを入れる

公式ライブラリの README に沿ってインストールします(ブランチ名は更新されるので、必ず公式の最新手順を確認してください)。

# robot_hat
git clone --depth 1 -b 2.5.x https://github.com/sunfounder/robot-hat.git
cd robot-hat && sudo python3 install.py && cd ..

# vilib
git clone --depth 1 https://github.com/sunfounder/vilib.git
cd vilib && sudo python3 install.py && cd ..

# picar-x
git clone -b 2.1.x https://github.com/sunfounder/picar-x.git
cd picar-x && sudo pip3 install . --break-system-packages

3. まず素の PiCar-X を動かす(SDV 層を載せる前の動作確認)

from picarx import Picarx
import time

px = Picarx()
px.set_dir_servo_angle(0)   # 直進
px.forward(30)              # 出力 30%
time.sleep(1)
px.stop()

ここでサーボのゼロ点調整・キャリブレーションを済ませておくと、後が楽です。

4. Kuksa Databroker を立てる

Databroker を Docker で起動します。 Raspberry Pi 5 は arm64 なので、イメージが arm64 に対応しているかも確認します。 SDV 系のツールはバージョンで API が変わりやすいので、ここは公式ドキュメントに合わせてください。

実動作はラズパイから持ってくるのが大変なので省略。。。。

5. PiCar-X Provider を書く

役割は 2 つだけです。

  • アクチュエータ:Databroker のターゲット値の変化を購読し、picar-x の API に変換して実行
  • センサ:超音波センサなどの値を読み、Databroker に現在値として書き込む

構造のイメージ(擬似コード。Kuksa クライアントの API は使用バージョンに合わせて書き換えてください):

from picarx import Picarx

px = Picarx()

def on_horn(active: bool):
    if active:
        beep()          # スピーカーで鳴らす

def on_distance_poll():
    return px.ultrasonic.read()   # cm

# 1) Databroker の Horn.IsActive のターゲット値を購読 → on_horn()
# 2) 周期的に on_distance_poll() の値を Databroker に書き込む

6. アプリ(自動停止)を書く

アプリは PiCar-X を知らない 作りにします。前方距離のシグナルを購読し、閾値以下なら停止要求のシグナルを書くだけです。

7. Grafana で見る

fleet-management のブループリントの構成を参考に、センサ値を Grafana へ出します。

ハマったところ・気づき

  • 普通にRaspiで動かすまでに時間がかかった。
  • 一般論として、SDV 系の OSS はバージョンの更新が速く、サンプルがそのまま動かないことがあるので、動いたバージョンを記録しておくと後で助かります。

今後やりたいこと

  • ステアリング/駆動も VSS 経由にして、スマホからの操作をアプリ層に載せる
  • カメラ(vilib)で物体検出し、結果をシグナルとして扱う
  • コンテナ化して、Provider やアプリを個別に入れ替えられるようにする
  • SEA-ME のように、メータークラスタ風の表示を足してみる

まとめ

  • SDV の肝は「ハードウェアの詳細」と「アプリ」を シグナルの標準化 で分離すること
  • GitHub には、ホーン(service-to-signal)、車両アプリ SDK、フリート管理など、小さく真似できる題材が揃っている
  • PiCar-X のような小さな実機があると、概念が一気に手触りのあるものになる

参考

組込ペリフェラル開発

ベアメタルで走る組込ペリフェラル開発完全ガイド

皆さんは、マイコン開発で「Lチカ」をする際、どのようなコードを書いていますか? Arduinoなら digitalWrite(13, HIGH);、STM32のHALライブラリなら HAL_GPIO_WritePin(...) でしょうか。非常に便利ですよね。

しかし、プロの組込エンジニアとして一歩踏み込むなら、「その関数の裏側でハードウェア(ペリフェラル)がどう動いているか」 を理解することは避けて通れません。

今回は「ペリフェラルな組込開発」と題して、ライブラリの皮を一枚剥ぎ取り、レジスタ操作やメモリマップドI/Oといった「ベアメタル(Bare Metal)」な世界へダイブします。これを読めば、謎のバグに遭遇した時の解決力が飛躍的に向上し、データシートが「読める」ようになります。

ターゲット読者

  • 中級者向け: ArduinoやHALライブラリでの開発には慣れてきたが、中身の挙動までは自信がないエンジニア。
  • 学習内容:

1. 導入:なぜ「ペリフェラル」を深く知る必要があるのか?

「ライブラリがあるんだから、それを使えばいいじゃん」 その通りです。現代の開発で、すべてをゼロから書くのはナンセンスです。しかし、「ライブラリが動かない時」や「極限の性能が必要な時」に、ブラックボックスのままでは手も足も出ません。

マイコンの中身は「CPU」と「ペリフェラル」

マイコン(MCU)は、計算を行うCPUコア(ARM Cortex-Mなど)と、外部とのやり取りを行うペリフェラル(GPIO, UART, I2C, Timer, ADCなど)で構成されています。 CPUは、メモリ上の特定のアドレス(レジスタ)に値を書き込むことで、ペリフェラルに命令を出します。

例えば、「LEDを光らせる」という動作は、物理的には「GPIOペリフェラルの出力データレジスタ(ODR)の特定ビットに1を立てる」という電気的なスイッチ操作に他なりません。この感覚を掴むことが、今回の目的です。


2. 環境構築:STM32で始めるベアメタルへの第一歩

今回は、業界標準であるSTマイクロエレクトロニクスのSTM32シリーズを使用します。資料が豊富で、レジスタ構成が綺麗に整理されているため、学習に最適です。

必要なハードウェア

  1. 開発ボード: STM32 Nucleo-F401RE
    • Arduinoピン互換があり、デバッガ(ST-LINK)も内蔵されているため、USBケーブル1本で開発可能です。
  2. センサー: BME280 温湿度・気圧センサーモジュール
    • I2C/SPI通信の実験に使用します。Bosch製の実践的なセンサーです。
  3. 計測器(推奨):

3. 実践・ハンズオン:レジスタを叩いてみよう

3.1. GPIO:Lチカをレジスタ操作で書く

通常、HAL_GPIO_TogglePin()で済む処理を、レジスタ操作で記述します。 Nucleo-F401REのユーザーLED(LD2)は、Port Aの5番ピン(PA5)に接続されています。

#include "stm32f4xx.h" // デバイスヘッダ

int main(void) {
    // 1. GPIOAへのクロック供給 (RCC_AHB1ENR bit0 = 1)
    RCC->AHB1ENR |= (1 << 0);

    // 2. PA5を出力モードに設定 (MODER5[1:0] = "01")
    GPIOA->MODER &= ~(3 << (5 * 2)); // Clear
    GPIOA->MODER |=  (1 << (5 * 2)); // Set

    while (1) {
        // 3. LED点灯 (ODR bit5 = 1)
        GPIOA->ODR |= (1 << 5);
        for(volatile int i=0; i<100000; i++); // Wait

        // 4. LED消灯 (ODR bit5 = 0)
        GPIOA->ODR &= ~(1 << 5);
        for(volatile int i=0; i<100000; i++); // Wait
    }
}

3.2. 通信ペリフェラル:I2CでBME280を叩く

I2Cは「Start条件」「Address送信」「ACK確認」など、手順が厳格です。 ベアメタル開発では、このシーケンス(手順)を正しく理解しているかが勝負の分かれ目です。以下に、STM32(Master)がセンサー(Slave)のレジスタに書き込む際の手順を図解します。

【I2C書き込みシーケンス図解】

sequenceDiagram
    participant Master as STM32 (Master)
    participant Bus as I2C Bus
    participant Slave as BME280 (Slave)
    
    Note over Master,Slave: 1 byte Write Sequence
    
    Master->>Bus: Start Condition (S)
    Master->>Bus: Slave Address (7bit) + Write(0)
    Bus->>Slave: Address Match?
    Slave-->>Master: ACK (A)
    
    Master->>Bus: Register Address (e.g. 0xF4)
    Slave-->>Master: ACK (A)
    
    Master->>Bus: Data Byte (e.g. 0x27)
    Slave-->>Master: ACK (A)
    
    Master->>Bus: Stop Condition (P)

この図の ACK が返ってこない場合、以下の原因が考えられます。 1. 配線ミス: SDA/SCLが逆、あるいはプルアップ抵抗がない。 2. アドレス間違い: BME280はSDOピンの状態によってアドレスが 0x76 か 0x77 に変わります。 3. GPIO設定ミス: I2Cピンは必ずOpen-Drain(オープンドレイン)設定にする必要があります。Push-Pullにすると通信できません。


4. ガジェット活用:見えない電気を見る

ペリフェラル開発において、「コードは合っているはずなのに動かない」は日常茶飯事です。そんな時、オシロスコープやロジックアナライザは、エンジニアの「目」となります。

Rigol DS1054Z の出番

エントリーモデルながら4ch使えるこのオシロは、I2Cの波形品質を見るのに最適です。 - 波形のなまり: プルアップ抵抗の値が大きすぎると、矩形波が「なまって」しまい、High/Lowの判定ができずに通信エラーになります。これはコードを見ても絶対に分かりません。 - ノイズ: モーターなどのノイズ源が近くにある場合、通信ラインにヒゲ(グリッチ)が乗っていないか確認できます。

Saleae Logic 8 の出番

プロトコルの中身を解析するならロジックアナライザです。 PC画面上でI2Cパケットをデコード表示できるため、「Startコンディションが出ているか?」「ACKが返ってきているか?」が一目瞭然です。


5. トラブルシューティング:ハマりどころリスト

  1. クロックの供給忘れ: RCCレジスタの設定漏れ。これがないとペリフェラルは死んだままです。
  2. 代替機能(AF)設定: GPIOをI2Cとして使うには、MODERだけでなくAFRレジスタの設定が必須です。
  3. 最適化による消滅: for(int i=0; ...) のような空ループは、コンパイラの最適化(-O2など)で削除されることがあります。ウェイト用の変数には必ず volatile を付けましょう。

6. まとめ:抽象化の梯子を降りてみよう

今回の記事では、普段隠蔽されているマイコンのペリフェラル制御について解説しました。

ぜひ、手元のNucleoボードで、Lチカのその先へ踏み出してみてください。


7. 参考文献・リンク

【脱・C言語】Zig入門

Rustよりもシンプルで、Cよりも安全な「第三の選択肢」を徹底解説

「C言語は古すぎるが、Rustは学習コストが高すぎる...」 「もっとシンプルに、でも低レイヤーを完全に制御したい」 そんなエンジニアの心の隙間を埋める言語として、Zig(ジグ) が爆発的な人気を集めているのをご存知でしょうか? 2026年現在、Zigは単なる「実験的な言語」の枠を超え、Bun(超高速JSランタイム)やTigerBeetle(金融系データベース)など、パフォーマンスが命のプロジェクトで採用が進んでいます。

今回は、「可能性に有り余る言語」 と呼ばれるZigの魅力と、実際に手を動かして感じるその「手触り」を徹底解説します。


1. なぜ今、Zigなのか?(3つの革命)

Zigは、Andrew Kelley氏によって開発されたシステムプログラミング言語です。その哲学は驚くほど明確です。

① 「隠された挙動」が一切ない

C++やRustでは、演算子オーバーロードやコンストラクタ、デストラクタなど、コードを見ただけでは分からない処理が裏で走ることがあります。 Zigにはそれがありません。 * 隠れた制御フローなし: マクロや演算子オーバーロードが存在しない。 * 隠れたメモリ割り当てなし: ヒープメモリを使う場合は、必ず「アロケータ(Allocator)」を明示的に渡す必要があります。

「コードに書いてあることだけが実行される」。この安心感が、堅牢なシステムを作る鍵です。

② C言語との「完全な」相互運用性

他の言語(RustやGo)からCのライブラリを呼ぶには、FFI(Foreign Function Interface)というバインディングを書く必要があり、これが非常に面倒でした。 ZigはC言語のヘッダーファイル(.h)を直接インポートできます。 しかも、ZigのコンパイラはCコンパイラとしても機能します(zig ccコマンド)。既存のCプロジェクトをZigコンパイラでビルドするだけで、クロスコンパイル環境が手に入るのです。

③ 魔法のような「Comptime」

Zig最大の発明と言われるのが Comptime(コンパイル時実行) です。 ジェネリクスやマクロのような複雑な構文を覚える必要はありません。「コンパイル時に普通のZigコードを実行する」だけで、型生成や最適化ができてしまいます。これは一度体験すると戻れない便利さです。


2. 環境構築

Zigの環境構築は非常にシンプルです。インストーラーすら不要な場合がほとんどです。

インストール手順

公式の ダウンロードページ からバイナリを取得するか、パッケージマネージャーを使用します。

Mac (Homebrew)

brew install zig

Windows (winget)

winget install zig.zig

Linux / macOS (Version Manager) バージョン管理ツールの zvm (Zig Version Manager) を使うのが現在は主流です。

# zvmのインストール(例)
curl -L [https://github.com/tristanisham/zvm/releases/download/v0.7.0/install.sh](https://github.com/tristanisham/zvm/releases/download/v0.7.0/install.sh) | sh
zvm install master
zvm use master

インストール後、以下のコマンドでバージョンを確認しましょう。

zig version
# 出力例: 0.15.2 (2026年1月時点の最新安定版を想定)

VS Codeの設定 拡張機能「Zig Language」をインストールしてください。 また、LSP(Language Server Protocol)である ZLS を導入すると、補完や定義ジャンプが効くようになり、開発体験が劇的に向上します。


3. 実践・ハンズオン:Zigの「手触り」を知る

それでは、実際にコードを書いてみましょう。 プロジェクト用のディレクトリを作成し、初期化します。

mkdir zig-demo
cd zig-demo
zig init

これで build.zig や src/main.zig が生成されます。

ステップ1: Hello Worldと「テスト」

Zigはテスト機能が言語に組み込まれています。src/main.zig を以下のように書き換えてみましょう。

const std = @import("std");

pub fn main() !void {
    // 標準出力の取得
    const stdout = std.io.getStdOut().writer();
    
    // try はエラーハンドリング(失敗したらエラーを上に投げる)
    try stdout.print("Hello, {s}!\n", .{"TechCraft Reader"});
}

// テストコードを同じファイルに書ける
test "simple test" {
    const expected = 5;
    const actual = 2 + 3;
    try std.testing.expect(actual == expected);
}

実行:

zig build run

テスト実行:

zig test src/main.zig

ステップ2: メモリ管理の「明示」

ここがZigの真骨頂です。動的にメモリを確保する場合、Zigでは「どの種類のメモリ管理(アロケータ)を使うか」をプログラマが選びます。

const std = @import("std");

pub fn main() !void {
    // 汎用アロケータ(General Purpose Allocator)の初期化
    // メモリリークを検知する機能がついている
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    
    // スコープを抜けるときにdeinit(解放)を実行
    defer _ = gpa.deinit(); 
    
    const allocator = gpa.allocator();

    // 動的配列(ArrayList)の作成
    var list = std.ArrayList(i32).init(allocator);
    
    // ここで defer を使うことで、main関数終了時の解放を予約
    // C言語のような free 忘れを防げる!
    defer list.deinit();

    // データの追加
    try list.append(10);
    try list.append(20);
    try list.append(42);

    const stdout = std.io.getStdOut().writer();
    try stdout.print("List content: {any}\n", .{list.items});
}

ポイント: * defer: 「このブロックを抜ける時に必ず実行する」という予約語。Go言語にもありますが、リソース管理において最強の武器です。 * allocator: 関数が隠れてメモリ確保するのを防ぐため、メモリが必要な関数には必ず引数として allocator を渡します。これにより「どこでメモリが確保されているか」が一目瞭然になります。

ステップ3: C言語の関数を直接呼ぶ

ZigがいかにC言語と仲が良いかを見てみましょう。Cの標準ライブラリ time.h を使ってみます。

const std = @import("std");
// C言語のライブラリをインポート
const c = @cImport({
    @cInclude("time.h");
    @cInclude("stdio.h");
});

pub fn main() !void {
    // Cのtime関数を呼び出し
    const current_time = c.time(null);
    
    // Cのprintf関数を呼び出し(Zigから直接!)
    _ = c.printf("Current Timestamp from C: %ld\n", current_time);
    
    // もちろんZigの標準出力も使える
    const stdout = std.io.getStdOut().writer();
    try stdout.print("Timestamp via Zig: {d}\n", .{current_time});
}

特別なバインディング生成ツールは不要です。ただ @cImport するだけ。これがZigが「Cの後継」と言われる所以です。


4. トラブルシューティング:初心者がハマるポイント

Q. 文字列の扱いが難しいのですが?

A. Zigには「String型」という特別な型はありません。文字列は単なるバイトの配列([]u8)または、NULL終端されたポインタ([*:0]u8)です。 最初は戸惑いますが、「メモリ上でどうデータが並んでいるか」を意識できるようになると、無駄なコピーを避けた高速な処理が書けるようになります。

Q. コンパイルエラー "unused variable" がうるさい

A. Zigは「使われていない変数」をコンパイルエラーにします。デバッグ中に一時的に変数を残したい場合は、_ = my_var; と書いて、「意図的に捨てている」ことをコンパイラに伝えてください。

Q. バージョンによる破壊的変更が多い

A. Zigはまだバージョン1.0に達していません。ネット上の古い記事(0.9系や0.11系)のコードは動かないことがよくあります。 エラーが出たら、まずは公式ドキュメント(ziglang.org/documentation/)の最新版を確認するか、ziglings(後述)で最新の構文を学ぶのが近道です。


5. まとめ:Zigは学ぶ価値があるか?

結論から言えば、YES です。

  • 低レイヤーの理解が深まる: メモリ管理やポインタの仕組みが、C言語よりも安全に、かつRustよりも素直に理解できます。
  • ツールチェーンとして優秀: Zig言語自体を書かなくても、zig cc は強力なC/C++クロスコンパイラとして使えます。
  • 将来性: インフラ、データベース、ゲームエンジンなど、パフォーマンスが要求される分野での採用が加速しています。

Next Step: まずは、Zigの構文をクイズ形式で学べる Ziglings に挑戦してみてください。壊れたコードを直していく形式で、ゲーム感覚でZigをマスターできます。

C言語の複雑さに疲れた方、Rustの所有権システムで挫折した方。 Zigという「シンプルで強力な道具」を、あなたのツールボックスに加えてみませんか?


6. 参考文献・リンク

SDVでEPSを制御してみる

SDV時代のEPS(電動パワーステアリング)制御

――ソフトウェア定義で進化する「曲がる」技術の最前線

1. SDVにおけるEPS制御とは?

SDV(Software Defined Vehicle)におけるEPS制御とは、従来の「ハードウェア(モーター・ギア)性能に依存した固定的なアシスト」から、「ソフトウェアによって機能、操舵感、安全性能が定義・更新されるステアリング制御」へのシフトを指します。

従来は独立したECUで完結していた制御が、SDVでは中央集中型アーキテクチャの一部となり、以下のような数式イメージで動的に特性が書き換わります。

$$TargetTorque = f(DriverInput, VehicleState, ADAS_Request, UserProfile)$$

この $f$ (制御ロジック)がOTA(Over-The-Air)で更新可能になることで、購入後もハンドリングが進化したり、自動運転機能との高度な統合が可能になります。


2. 制御フロー(SDVアーキテクチャ)

SDVでは、ステアリング操作とタイヤの動作が分離(デカップリング)される傾向にあります(Steer-by-Wire等)。

  1. 入力検知(Input) ステアリングのトルク・角度センサーに加え、カメラ・LiDARからのADAS情報、路面状況などを取得。

  2. ビークルモーション制御(Central Compute) 統合制御ユニット(VCU)が、「ドライバーの意思」と「車両の安定性」を調停し、目標タイヤ角を算出。
    ※ ここで「スポーツモード」や「自動回避」などのロジックが介入します。

  3. アクチュエーション(Zone ECU -> Actuator) 算出された指令値を通信(Ethernet/CAN-FD)でEPSモーターへ送信し、タイヤを転舵。

  4. 反力生成(Force Feedback) タイヤからの路面情報を電気信号としてフィードバックし、ステアリングホイールに「疑似的な手応え」を発生させます。


3. 制御イメージ(Pythonによるロジックシミュレーション)

SDVの特徴である「パラメータの動的変更(プロファイルの切り替え)」を模した簡易コードです。 従来のEPSでは工場出荷時に固定されていたマップを、ソフトウェアで動的に書き換えるイメージです。

class SDVSteeringController:
    def __init__(self):
        # デフォルトの操舵特性(重み係数)
        self.assist_gain = 1.0
        self.damping = 0.5
        self.mode = "NORMAL"

    # OTAやモード切替で特性を変更(SDVの本質)
    def update_profile(self, mode_name):
        if mode_name == "SPORT":
            self.assist_gain = 0.6  # アシスト減らして重くする
            self.damping = 0.8      # 収束性を高める
        elif mode_name == "COMFORT":
            self.assist_gain = 1.5  # 軽くする
            self.damping = 0.3
        elif mode_name == "PARKING":
            self.assist_gain = 3.0  # 超軽量化
            
        self.mode = mode_name
        print(f"Profile updated to: {self.mode}")

    # リアルタイム制御ループ(擬似)
    def calculate_torque(self, input_torque, vehicle_speed):
        # 速度感応型の基本ロジック
        speed_factor = 1.0 / (vehicle_speed + 1.0)
        
        # モードごとの係数を適用
        assist_torque = input_torque * self.assist_gain * speed_factor
        
        return assist_torque

# --- 実行イメージ ---
sdv_eps = SDVSteeringController()

# 1. 高速道路走行時(Sportモードへ切り替え)
sdv_eps.update_profile("SPORT")
current_assist = sdv_eps.calculate_torque(input_torque=2.0, vehicle_speed=80)
print(f"Assist Torque: {current_assist:.2f}")

# 2. 駐車場での操作(自動駐車アプリがParkingモードを要求)
sdv_eps.update_profile("PARKING")
current_assist = sdv_eps.calculate_torque(input_torque=2.0, vehicle_speed=5)
print(f"Assist Torque: {current_assist:.2f}")

実際には、これに安全性検証(ISO 26262)やリアルタイムOS上でのミリ秒単位の処理が必要となります。


4. 具体製品・技術例

SDV化を象徴する製品群です。

■ Steer-by-Wire(SBW)システム

機械的な接続(ステアリングシャフト)をなくし、電気信号だけでタイヤを動かす技術。SDVとの相性が最高です。

  • トヨタ / JTEKT「ワンモーショングリップ」 (Lexus RZ搭載)
    ステアリングの回転角を速度に応じてソフトウェアで自由に変更可能。低速時は少し回すだけでタイヤが大きく切れ(持ち替え不要)、高速時は穏やかに反応します。

■ 高可用性(High Availability)EPS

  • Bosch / Nexteer 等の冗長EPS 自動運転中にEPSが故障しても、予備の回路と電源で制御を継続する「Fail-Operational」設計。SDVでドライバーが運転に関与しない状況(Level 3以上)では必須の要件です。

5. 技術要素の深掘り

デカップリングと反力生成

SDV時代のステアリング(特にSBW)では、以下の2つが物理的に分離しています。

  1. HWA (Hand Wheel Actuator): ドライバーの手応え(反力)を作るモーター。
  2. RWA (Road Wheel Actuator): 実際にタイヤを動かす高出力モーター。

これらを繋ぐのは通信線のみです。そのため、「路面のザラつき」などの情報は、センサーデータからアルゴリズムで人工的に生成してドライバーに伝える必要があります。ここに各社の「味付け」のノウハウが詰まっています。

バイワイヤとセキュリティ

物理的な軸がないため、ハッキングされると致命的です。そのため、ISO 21434(サイバーセキュリティ)準拠や、認証された信号以外受け付けないゲートウェイの設計が通常のEPS以上に重要になります。


6. SDVにおけるEPSの得意/不得意

得意な領域

  • 可変ステアリングギア比(VGRS)の完全な自由化 → 駐車時はF1カーのようにクイックに、高速ではバスのようにマイルドに、といった変更が自在。
  • ADAS連携 → 緊急回避時のステアリング介入が、キックバック(ハンドルが勝手に回る現象)なしに行える。
  • 配置の自由度 → 左ハンドル/右ハンドルの設計共通化や、運転席スペースの拡大。

課題・不得意

  • 電源喪失への対策 → 物理バックアップがない場合、電源が落ちると操舵不能になるため、バッテリーと回路の二重化が必須(コスト増)。
  • 自然なフィーリングの再現 → 違和感のない「操舵感」をデジタルで作るには高度なチューニングが必要。

7. 選定・開発のポイント

SDV向けEPSシステムの開発や選定では以下が重要です。

  • 通信レイテンシ Ethernet/CAN-FDを用い、遅延を人間に感知できないレベル(数ms以下)に抑えられるか。
  • 冗長性(Redundancy) モーター巻線、インバータ、電源系統が完全に二重化されているか(ASIL D対応)。
  • APIの公開範囲 自動車メーカーの統合制御ソフトから、どこまでパラメータ(アシスト量、粘性、慣性など)をいじれる仕様になっているか。

8. まとめ

  • SDVにおけるEPSは、単なる「重さ軽減装置」から「車両運動を司るスマートデバイス」へ進化しました。
  • 最大のトピックはSteer-by-Wireの実用化であり、これによりステアリングギア比や操舵感がソフトウェアで定義可能になります。
  • 開発には、従来の機械工学に加え、制御理論、通信、セキュリティの高度な知識が求められます。

「スマホのOSをアップデートすると使い勝手が良くなる」ように、「車のOSを更新するとハンドリングが上手くなる」時代の到来です。

HBMとNANDを比較しない

HBMとNANDを「比較」しないための比較

―― メモリ階層から理解する役割分担と設計思想

1. はじめに

HBM(High Bandwidth Memory)とNAND Flashは、しばしば並べて語られます。
しかしこの2つは、性能を競う関係でも、置き換え可能な技術でもありません。

それでもあえて並べて語る価値があるとすれば、それは
「メモリという言葉が指す世界の広さ」と
「計算と保存がまったく別の要求を持つ」ことを、最も分かりやすく示してくれるからです。

本記事では、

  • HBMとNANDを同列に「比較しない」前提を置いたうえで
  • メモリ階層の中での役割
  • それぞれが最適化している設計目標
  • 周辺に存在する他のメモリ技術

を整理します。


2. メモリ階層から見た全体像

0

まず前提として、コンピュータシステムは階層構造でできています。

階層 主な技術 主目的
レジスタ CPU内部 演算そのもの
キャッシュ SRAM レイテンシ削減
主記憶 DRAM / HBM 実行中データ
補助記憶 NAND Flash 永続保存

HBMとNANDは、この階層のほぼ両端に位置します。
ここを理解しないまま性能を比べると、話が噛み合いません。


3. HBMとは何か(主記憶側の極)

HBMはDRAMの一種です。
違いは「何を最優先で設計したか」にあります。

3.1 HBMが最適化しているもの

  • メモリ帯域(Bandwidth)
  • 並列アクセス
  • プロセッサ直結性

HBMはDRAMダイを3次元に積層し、TSVで垂直接続します。
さらにGPUやAIアクセラレータのすぐ隣に配置することで、

  • 数千bit幅のバス
  • 数百GB/s〜TB/s級の帯域

を実現します。

3.2 HBMの役割

HBMの役割は一貫しています。

計算を止めないこと

GPUやAI処理では、
「1回のアクセスが速い」より
「同時に大量のデータを流し続けられる」ことが重要です。

HBMはこの要求に特化しています。

3.3 HBMの制約

  • 容量は数GB〜数十GB程度
  • 実装コストが高い
  • パッケージ設計が難しい

つまりHBMは、
容量や汎用性を犠牲にして帯域を取りに行く技術です。


4. NANDとは何か(ストレージ側の極)

NAND Flashは不揮発メモリです。
こちらも最適化の方向は明確です。

4.1 NANDが最適化しているもの

  • 容量
  • コスト
  • データ保持

NANDは、

  • 書き込み前に消去が必要
  • 書き換えはブロック単位
  • ランダムアクセスが苦手

という制約を持ちます。

4.2 NANDの役割

NANDの役割はこれです。

電源を切ってもデータを残すこと

SSD、eMMC、UFS、SDカードなど、
私たちが「保存」と呼ぶ行為の大半はNANDが担っています。

4.3 なぜNANDは主記憶にならないのか

理由は単純です。

  • レイテンシが桁違いに大きい
  • 書き換え粒度が大きい
  • 耐久性管理が必要

CPUやGPUが前提とする
「1クロック単位で自由に読み書きできる世界」
とは設計思想が根本的に異なります。


5. HBMとNANDは「比較」ではなく「対比」

ここが本記事の核心です。

観点 HBM NAND
技術系統 DRAM Flash
揮発性 揮発 不揮発
主目的 計算継続 永続保存
最適化対象 帯域 容量・コスト
置かれる階層 主記憶 補助記憶

HBMとNANDは、

  • どちらが優れているか
  • どちらが新しいか

を問う対象ではありません。

「なぜ両方必要なのか」を示すための対比として意味を持ちます。


6. 間に存在するメモリたち

HBMとNANDの間には、多くの技術が存在します。

6.1 DRAM(DDR / LPDDR)

  • 汎用主記憶
  • 容量・コスト・速度のバランス型
  • HBMの対になる存在

6.2 SRAM

  • CPUキャッシュ
  • 極低レイテンシ
  • 容量は極小

6.3 GDDR

  • GPU向けDRAM
  • HBMより実装しやすい
  • 消費電力は高め

6.4 NOR Flash

6.5 新興メモリ(MRAM / FRAM など)

  • 不揮発
  • 書き換え高速
  • 容量はまだ小さい

「主記憶とストレージの中間」を狙う技術群ですが、
現時点では補完的な存在です。


7. 設計視点での整理

設計で迷ったときは、次の問いを立てると整理できます。

  • 電源断で消えてよいか
  • クロック単位のアクセスが必要か
  • 帯域がボトルネックか
  • 容量が最優先か
  • コスト制約はどこにあるか

HBMとNANDは、
この問いに対して正反対の答えを持つ技術です。


8. まとめ

  • HBMは「計算を止めないためのメモリ」
  • NANDは「データを失わないためのストレージ」
  • 両者は競合せず、階層の中で共存する
  • 比較の目的は「優劣」ではなく「役割理解」

HBMとNANDを正しく対比できるようになると、
DRAM、SRAM、NOR、GDDRといった他のメモリ技術も、
自然と“置くべき場所”が見えてきます。

それが、メモリを理解するということです。

EtherCATのイロハから

EtherCAT入門

―― 超高速フィールドバスの設計思想と実装イメージ、学習の指針

1. はじめに

産業用ネットワークの分野で EtherCAT は、すでに特別な存在ではありません。
サーボモータ制御、分散I/O、装置内ネットワークなど、「μsオーダの同期性」が求められる現場では事実上の標準になっています。

一方で、

  • なぜEtherCATはこれほど速いのか
  • Ethernetなのに、なぜリアルタイムが成立するのか
  • CANや他の産業Ethernetと何が決定的に違うのか

を、設計レベルで説明できる人は多くありません。

本記事では、EtherCATを
「規格の暗記」ではなく「設計思想」として理解することを目的に、

  • 通信モデルの本質
  • 実装をイメージできる抽象度
  • 学習に適した書籍(Kindle対応)

を整理します。

対象読者は 組み込み中級者。
割り込み、DMA、周期制御のいずれかに触れた経験があれば十分です。


2. EtherCATとは何か

EtherCAT(Ethernet for Control Automation Technology)は、
制御用途に特化した産業用フィールドバスです。

基本的な特徴は次の通りです。

重要なのは、「Ethernetを使っているが、Ethernet的には使っていない」点です。
EtherCATは、通信の考え方そのものを制御向けに作り替えています。


3. なぜEtherCATは速いのか

0

3.1 通常の通信モデル

一般的なネットワーク通信では、

  • 送信
  • 受信
  • 応答待ち

という往復を、相手ごとに繰り返します。
ノードが増えれば増えるほど、遅延も比例して増えます。


3.2 EtherCATの通信モデル

EtherCATは根本的に異なります。

  • マスタは 1枚のフレーム に全スレーブ分のデータ領域を用意する
  • フレームは全スレーブを直列に通過する
  • 各スレーブは、フレーム通過中に自分の領域だけを読み書きする
  • フレームは最後にマスタへ戻る

通信の往復は 基本1回。
スレーブは「受信してから考える」のではなく、「通過しながら処理」します。

この オンザフライ処理 により、

  • ノード数が増えても遅延増加は極小
  • スイッチが不要
  • μsオーダの周期制御が可能

という特性が実現されています。


4. EtherCATの構成要素

4.1 マスタとスレーブ

  • マスタ
    制御PC、産業用PC、組み込みLinux、RTOS搭載SoCなど
  • スレーブ
    サーボアンプ、I/Oモジュール、センサ、アクチュエータ

スレーブには ESC(EtherCAT Slave Controller) が搭載され、
フレーム処理をハードウェアで実行します。


4.2 トポロジ

EtherCATは配線の自由度が高いことも特徴です。

  • ライン
  • リング
  • スター
  • ツリー

物理構成は自由ですが、論理的には1本のバスとして扱われます。
この単純さが、大規模構成でも制御を破綻させない理由の一つです。


4.3 Distributed Clocks(分散クロック)

EtherCATの本質的な強みが Distributed Clocks(DC) です。

  • 全スレーブが共通の時間基準を持つ
  • サブμsレベルで同期
  • 多軸制御・同時制御が成立する

フラグやイベントに頼らず、
「同じ時刻に、同じ周期で、同じ状態を見る」
これをネットワークレベルで保証できる点が、他方式との決定的な違いです。


5. 実装イメージ:周期で「一括更新」する

EtherCATの制御は、個別リクエスト型ではありません。
感覚としては「共有メモリを周期的に更新する」に近いものです。

typedef struct {
    int32_t encoder;
    uint16_t status;
} pdo_input_t;

typedef struct {
    int16_t torque_cmd;
    uint16_t control;
} pdo_output_t;

pdo_input_t  inputs[NUM_SLAVES];
pdo_output_t outputs[NUM_SLAVES];

while (1) {
    ethercat_send(outputs);     // 全スレーブ分を一括送信
    ethercat_receive(inputs);   // 全スレーブ分を一括受信

    control_loop(inputs, outputs);
}

このモデルでは、

  • フラグで通知を待つ必要がない
  • 状態は周期で自然に更新される
  • 制御の因果関係が明確

になります。
組み込みでありがちな「非同期フラグ地獄」を避けやすい構造です。


6. 実装で注意すべきポイント

6.1 Ethernetだから簡単、ではない

EtherCATはリアルタイム性を要求します。

  • 割り込みレイテンシ
  • DMA設定
  • キャッシュコヒーレンシ
  • OSスケジューリング

特にLinuxマスタでは、
PREEMPT_RTやCPUアイソレーションなどの設計が不可欠です。


6.2 状態機械の理解が重要

EtherCATは状態遷移が明確に定義されています。

  • INIT
  • PRE-OP
  • SAFE-OP
  • OP

通信トラブルの多くは、
「なぜ今この状態なのか」を理解せずに進めることで起きます。


7. 学習ロードマップ

  1. EtherCATの通信モデルとDCを理解する
  2. PDO中心の周期制御をイメージする
  3. オープンソースマスタで実際のフレーム構造を見る
  4. 実機で周期・遅延・同期を測る

この順序を外すと、仕様書が急に難しくなります。


8. Kindleで読めるおすすめ本

1. EtherCAT Technology Introduction

EtherCAT Technology Groupによる公式入門書。
通信モデル、オンザフライ処理、分散クロックが整理されています。

Amazon Kindle版


2. Industrial Ethernet

EtherCATを含む産業用Ethernet全体を俯瞰できます。
他方式との違いを理解することで、EtherCATの立ち位置が明確になります。

Amazon Kindle版


9. まとめ

EtherCATは「速いネットワーク」ではありません。
リアルタイム制御のために設計された通信モデルです。

  • 往復しない通信
  • 周期同期による一貫性
  • 状態と時間を重視した設計

これらを理解すると、
なぜEtherCATが多軸制御や高精度制御で選ばれるのかが自然に見えてきます。

次に踏み込むなら、

  • 状態遷移とトラブル事例
  • Distributed Clocksの実装と誤差要因
  • EtherCATとRTOS/Linuxの関係

を扱うと、より実践的な理解につながるでしょう。

Cコードの壊れ方と原因:フラグ管理

そのフラグ、誰が下ろす? ― 組み込みで“たまに壊れる”Cコードを徹底解剖(実例つき)

1. 導入

--- フラグが原因のバグは、だいたい再現しない ---

組み込み開発で一番やっかいなバグの代表格が「フラグ管理の失敗」です。テスト環境では動く。ログも綺麗。ところが量産機や現場でだけ、たまに固まる・取りこぼす・二重処理する。原因を追うと、たいてい“ただのbool”に見えるフラグが、割り込み・DMA・最適化・タスク切り替えの圧力で、設計の弱点を暴いています。

この記事では「実際に壊れるCコード例」をガッツリ載せます。ポイントは、単に「volatile付けろ」では終わらせないこと。なぜ壊れるかを、時間軸(いつ立つ/いつ落とす)と責務(誰が所有/誰が消費)で分解し、直し方も“実装”ではなく“設計”として提示します。

対象読者:中級者(割り込み、メインループ、RTOS/DMAのどれかに触れたことがある人)
読後に得られるもの:フラグを「状態」と「イベント」に分け、壊れない設計に置き換える勘所


2. 前提

--- フラグが壊れるのは「状態」と「イベント」を混ぜるから ---

まず定義を揃えます。フラグには2種類あります。

  • イベント(出来事):発生回数が重要。「1回起きた」を取りこぼしたくない
    例)UART受信完了、ADC変換完了、ボタン押下、DMA完了
  • 状態(State):現在の状態が重要。「いま何中か」を表す
    例)BUSY、ERROR、CONNECTED、CALIBRATING

壊れる典型は「イベントをboolで表す」か「状態を複数フラグで表して組み合わせ爆発」かのどちらか。
以降のコードは、この2つがどう地獄を呼ぶかを実例で見せます。


3. 壊れるCコード例

3.1 例1:イベントをboolで表して“取りこぼす”

UART受信完了フラグをISRで立て、メインループで処理する。よくある構図です。

#include <stdbool.h>
#include <stdint.h>

volatile bool rx_done = false;
uint8_t rx_buf[64];

void UART_RX_ISR(void) {
    // 受信バッファにデータが入った前提
    rx_done = true;     // 「受信完了」を通知
}

void process_rx(void) {
    // rx_buf を解析する重い処理
}

int main(void) {
    while (1) {
        if (rx_done) {
            rx_done = false;
            process_rx();
        }
    }
}

このコード、99%は動きます。壊れるのはここ:

  • if (rx_done) を通過した直後〜 rx_done=false の間にISRが走ったら?
  • ISRが true にした直後、メインが false で上書きする

結果:イベントを“消す”。つまり取りこぼし。

原因の本質:イベントは回数を持つのに、boolは回数を持てない。
「1回以上起きた」しか表現できず、同時に“消費”が上書きで起きる。

直し方

イベントはカウンタにする

#include <stdint.h>

volatile uint32_t rx_events = 0;

void UART_RX_ISR(void) {
    rx_events++; // 発生回数を積む(オーバーフローは設計で扱う)
}

int main(void) {
    uint32_t local;
    while (1) {
        // クリティカル区間でスナップショットを取る(例:割り込み禁止)
        __disable_irq();
        local = rx_events;
        rx_events = 0;
        __enable_irq();

        while (local--) {
            process_rx();
        }
    }
}

ポイントは「回数を持つ」「スナップショットで一気に回収」「処理は割り込み外で」。
(__disable_irq() は環境依存なので、あなたのMCU/RTOSの方法に置換してください。)


3.2 例2:Read-Modify-Write(RMW)でフラグが“消える”(ビットマスク地獄)

複数イベントを1つのビットフラグに詰めるのも定番です。ここでよく壊れます。

#include <stdint.h>

#define EVT_UART (1u << 0)
#define EVT_ADC  (1u << 1)

volatile uint32_t events = 0;

void UART_ISR(void) { events |= EVT_UART; }
void ADC_ISR(void)  { events |= EVT_ADC;  }

int main(void) {
    while (1) {
        if (events & EVT_UART) {
            events &= ~EVT_UART;   // ←ここが罠
            handle_uart();
        }
        if (events & EVT_ADC) {
            events &= ~EVT_ADC;
            handle_adc();
        }
    }
}

壊れるパターンはこうです:

1) mainが events &= ~EVT_UART を実行する(これは内部的にRMW:読み→AND→書き)
2) 読みと書きの間にISRが走り events |= EVT_ADC を実行
3) mainの“古い読み値”に基づく書き戻しが、ISRの更新を潰す

結果:EVT_ADC が消える。再現しづらい。

直し方

ビットイベントは「まとめて回収」+「一括クリア」

uint32_t local;

__disable_irq();
local = events;
events = 0;        // 一括クリア(RMWを避ける)
__enable_irq();

if (local & EVT_UART) handle_uart();
if (local & EVT_ADC)  handle_adc();

「個別に &= ~BIT をする」より「一括で回収してゼロに戻す」が強い。
イベントを“箱から取り出す”イメージにすると設計が安定します。


3.3 例3:volatileを付けたのに壊れる(volatile神話を壊す)

次は「volatileを付けたのに壊れる」例です。
典型は「複数変数で状態を表している」ケース。

#include <stdbool.h>
#include <stdint.h>

volatile bool busy = false;
volatile bool done = false;

void start_job(void) {
    busy = true;
    done = false;
}

void ISR_job_done(void) {
    done = true;
    busy = false;
}

int main(void) {
    start_job();
    while (1) {
        if (done && !busy) {
            // 完了したはずなので結果処理
            use_result();
            break;
        }
    }
}

「doneがtrueかつbusyがfalse」を待っているので、一見大丈夫そう。
でも状態が2変数に分かれているので、観測タイミングによっては矛盾状態を見ます。

  • done==true だが busy==true(ISR内の書き込み順序/タイミング)
  • 逆もあり得る(特にマルチコア/キャッシュ/コンパイラ最適化が絡むと地獄)

volatileは「読み書きを省略しない」だけ。一貫性(アトミック性)や順序(メモリオーダ)を保証しない。

直し方

状態は1つのenumにまとめる

typedef enum {
    ST_IDLE,
    ST_BUSY,
    ST_DONE,
    ST_ERROR
} job_state_t;

volatile job_state_t state = ST_IDLE;

void start_job(void) {
    state = ST_BUSY;
}

void ISR_job_done(void) {
    state = ST_DONE;
}

int main(void) {
    start_job();
    while (1) {
        if (state == ST_DONE) {
            use_result();
            state = ST_IDLE;
            break;
        }
    }
}

複数フラグの「組み合わせで状態を表す」のをやめる。
状態は “ひとつ” に閉じ込める。これだけで壊れ方が激減します。


3.4 例4:ポーリング+フラグで“二重処理”する(消費の責務が曖昧)

ボタン押下の「押された」をフラグで持つと、二重処理が起きがちです。

volatile bool button_pressed = false;

void GPIO_ISR(void) {
    button_pressed = true;
}

int main(void) {
    while (1) {
        if (button_pressed) {
            // 押されたのでトグル
            led_toggle();
            // フラグクリア忘れ or タイミングのズレで二重実行
        }
    }
}

このコードは“クリア忘れ”が露骨ですが、実際はもっと陰湿です。
例えば「処理中に割り込みが再度入る」「チャタリング」「押下回数を数えたい」が混ざると一気に壊れます。

直し方

イベントはデバウンス+カウンタ or キュー

volatile uint32_t button_events = 0;

void GPIO_ISR(void) {
    // 実際はタイマでデバウンスするのが定石
    button_events++;
}

int main(void) {
    while (1) {
        uint32_t n;

        __disable_irq();
        n = button_events;
        button_events = 0;
        __enable_irq();

        while (n--) {
            led_toggle();
        }
    }
}

「押された状態」ではなく「押された回数」を扱うと、仕様に耐えます。


3.5 例5:リングバッファを“フラグで最適化”して壊す(取りこぼしの上位互換)

UART受信をリングバッファに入れて、rx_ready フラグで通知するパターン。
これ、実務で本当に多いです。

#define RB_SIZE 128
volatile uint8_t rb[RB_SIZE];
volatile uint16_t w = 0, r = 0;
volatile bool rx_ready = false;

void UART_ISR(void) {
    rb[w] = UART_DR;
    w = (w + 1) % RB_SIZE;
    rx_ready = true;
}

int main(void) {
    while (1) {
        if (rx_ready) {
            rx_ready = false;  // ←ここで “まだ残ってる” 可能性を捨てる
            while (r != w) {
                consume(rb[r]);
                r = (r + 1) % RB_SIZE;
            }
        }
    }
}

壊れ方: - rx_ready=false の直後にISRが入り true へ - mainはバッファを全部読み切る前に、またフラグの論理に引きずられる
- さらに複数バイトが高速に来ると、rx_ready が通知として役に立たない

直し方

“フラグ通知”をやめる:バッファが真実
リングバッファは r != w がすでにイベント/状態を持っています。
追加のフラグは冗長で、矛盾を生みます。

int main(void) {
    while (1) {
        while (r != w) {
            uint8_t b = rb[r];
            r = (r + 1) % RB_SIZE;
            consume(b);
        }
        // ここで低電力待機など(WFI等)を入れたければ条件付きで
    }
}

通知が必要なら、RTOSならタスク通知/セマフォ、ベアメタルならWFIと割り込み設計に寄せるのが筋です。


4. “壊れない”設計に置き換える(フラグ管理のベストプラクティス)

4.1 原則A:イベントは「回数」か「キュー」で表す

  • 1ビット(bool)でイベントを表すと取りこぼす
  • 複数イベントはビット集合でも良いが、回収は“一括”で
  • データを伴うならキュー(リングバッファ)

4.2 原則B:状態は「enum 1個」に閉じ込める

  • busy と done のような複数フラグ状態は破綻しやすい
  • 状態遷移表(簡単でいい)を作ると事故が減る

4.3 原則C:誰が立てて、誰が下ろすかを“契約”にする

設計レビューでこの2行を必ず書くのが効きます。

  • 生産者(producer):ISR / DMA完了 / タスクA
  • 消費者(consumer):main loop / タスクB
  • 消費の方式:一括回収?1回ずつ?上書き許容?

この契約が曖昧なフラグは、だいたい将来壊れます。


5. トラブルシューティング

症状1:たまにイベントが消える(取りこぼし)

  • boolイベントを使っていないか?
  • ビットフラグを個別RMWでクリアしていないか?
  • ISRとmainが同じ変数を“読み書き”していないか?

対策:カウンタ化/一括回収/クリティカル区間導入

症状2:二重処理する(同じイベントを2回処理)

  • クリアの責務が複数箇所にないか?
  • “状態”と“イベント”が同じフラグになっていないか?

対策:消費者を一箇所に固定/状態はenumへ

症状3:最適化レベルで壊れ方が変わる

  • volatile不足だけでなく、設計上の競合が潜んでいる可能性大

対策:volatileは最低限、設計は「一括回収」「単一状態」「責務分離」


6. まとめ

フラグは“ただのbool”ではない

フラグ管理の失敗は、コードの書き方というより「仕様の言語化不足」で起きます。

  • イベント(回数が重要)をboolで表すと取りこぼす
  • 状態(いまが重要)を複数フラグで表すと矛盾が生まれる
  • RMWでビットが消える、volatile神話で安心してしまう

最短の改善はこれです。

1) イベントはカウンタ or 一括回収 or キュー
2) 状態はenum 1個
3) 立てる人/下ろす人/消費方法を契約として明文化

次回記事にするなら、ここからさらに踏み込んで
「RTOSのイベントフラグ/タスク通知でも同じ事故が起きる」
「DMA+キャッシュ(コヒーレンシ)でフラグが正しくてもデータが壊れる」
といった“もう一段深い地獄”にも行けます。


参考文献・リンク

Making Embedded Systems: Design Patterns for Great Software (English Edition)

  • 低レベルな並行性/メモリ可視性の背景理解(volatileの誤解を正す補助)
    cppreference: volatile

組込みエンジニアの教科書

データの保存と速度性を両立するWeb技術のベストプラクティス

データの保存と速度性を両立するWeb技術のベストプラクティス

― 「都度問い合わせは遅い」「データ保持は大きくなりすぎる」をどう解くか ―

はじめに(導入)

Webアプリケーションを設計・運用していると、ほぼ必ず次の壁にぶつかります。

  • 都度DBやAPIに問い合わせると遅い
  • キャッシュすると今度はデータ量が肥大化する
  • どこまで保持し、どこで捨てるのが正解なのか分からない

これは個々の技術選定ミスではなく、「データのライフサイクル設計」が曖昧なままシステムが成長した結果として起きる問題です。

本記事では、

  • Webにおける「遅さ」の正体
  • データ保存戦略のレイヤ分解
  • 実際に使われているベストプラクティス構成
  • 技術選定の考え方(DB / Cache / CDN / Edge)

を 実務エンジニア視点 で整理します。

対象読者は 中級者向け。
バックエンド or Webインフラに一度は触れたことがある方を想定しています。


1. Webが遅くなる本当の理由

1.1 「問い合わせが遅い」の内訳

「DBアクセスが遅い」「APIが遅い」と言いがちですが、分解すると原因は以下です。

特にWebでは、

「1回の問い合わせが100ms」 × 「数十回」

が致命的です。
そのため 回数を減らす設計 が最優先になります。


2. データ保存をレイヤで考える

ベストプラクティスの前に、まず 保存場所をレイヤで分解 します。

[ Client ]
   ↓
[ CDN / Edge Cache ]
   ↓
[ App Cache (Redis等) ]
   ↓
[ DB / Object Storage ]

重要なのは、

すべてをDBに取りに行かない

という思想です。


3. キャッシュ戦略の基本原則

3.1 Cache Aside パターン(王道)

最も使われているのが Cache Aside です。

  1. キャッシュを確認
  2. なければDBから取得
  3. キャッシュに保存
  4. レスポンス
if cache.exists(key):
    return cache.get(key)
else:
    data = db.get(key)
    cache.set(key, data, ttl=300)
    return data

メリット - 実装が単純 - DB負荷を大幅に削減

注意点 - キャッシュミス時のスパイク - TTL設計が重要


3.2 Write Through / Write Back

  • Write Through
    書き込み時にDBとキャッシュを同時更新
    → 一貫性重視

  • Write Back
    キャッシュに先に書いて後でDB反映
    → 速度重視(障害時リスクあり)

Webサービスでは Read-heavy → Cache Aside + Write Through が主流です。


4. 「データ保持が大きくなりすぎる」問題の正体

4.1 すべて保存しようとする設計

よくある失敗例:

  • アクセスログを無期限保存
  • セッションをDBに永続保存
  • キャッシュにTTLを設定していない

これは 保存目的の未整理 が原因です。


4.2 データは役割で分ける

種類 目的 保存先 保持期間
マスタデータ 正 DB 永続
セッション 一時 Redis 数分〜数時間
キャッシュ 高速化 Redis / CDN 数秒〜数分
ログ 分析 Object Storage 圧縮・期限付き

「これは永続か?一時か?」を必ず問う
これだけで設計は大きく改善します。


5. CDN・Edgeを使う理由

5.1 CDNは「画像用」ではない

CDN(CloudFront / Fastly / Cloudflare)は、

  • 静的ファイル
  • APIレスポンス
  • HTML

すべてをキャッシュ可能です。

Edgeで止められる通信は、アプリに届かせない
これが最大の価値です。


5.2 Cache-Control設計

Cache-Control: public, max-age=60, stale-while-revalidate=30
  • max-age:新鮮期限
  • stale-while-revalidate:裏で更新しつつ古いレスポンスを返す

これにより、

  • ユーザー体感速度を落とさず
  • 裏で安全に更新

が可能になります。


6. ベストプラクティス構成(実例)

6.1 よくあるモダンWeb構成

Browser
 ↓
CDN (Cache / Edge Function)
 ↓
API Gateway
 ↓
App Server
 ↓
Redis (Cache)
 ↓
DB (RDB / NoSQL)

ポイント

  • 8割のリクエストをCDNで完結
  • Appは「キャッシュミス時のみ仕事」
  • DBは最後の砦

7. 技術選定の指針

7.1 速度を出したいなら

  • Redis / Memcached
  • CDN(Edge Cache)
  • 非同期処理(Queue)

7.2 データ肥大を防ぐなら

  • TTLを必ず設定
  • ログは圧縮+期限削除
  • Hot / Cold データ分離

8. よくあるアンチパターン

  • キャッシュ無しでスケール
  • TTL未設定Redis
  • DBを分析用途に流用
  • キャッシュと永続の責務混在

これらは 後から必ず破綻 します。


まとめ

Webにおける「速さ」と「データ量」はトレードオフではありません。

重要なのは、

  • データの役割を分ける
  • 問い合わせ回数を減らす
  • 保存期間を明確にする
  • キャッシュは前段に置く

という設計思想です。

ベストプラクティスは特定の技術名ではなく、

「どこで止め、どこで捨てるか」

を意識したアーキテクチャです。


参考リンク

ノイズキャンセリングを基礎から丁寧に

ANC(アクティブノイズキャンセリング)の技術を基礎から理解する

― 仕組み・DSPアルゴリズム・実装コード・実製品まで ―

はじめに(導入)

電車の走行音、飛行機のエンジン音、カフェの環境音。
こうした「不快ではないが集中力を奪う低周波ノイズ」を劇的に消してくれる技術が ANC(Active Noise Cancellation:アクティブノイズキャンセリング) です。

今やANCは、ハイエンドヘッドホンだけでなく、完全ワイヤレスイヤホン、Web会議用ヘッドセット、さらには自動車の車内制御や空調設備にまで広がっています。しかしその中身は、かなりガチなDSP(デジタル信号処理)技術です。

この記事では以下を目標にします。

  • ANCの原理を「数式・信号処理視点」で理解する
  • 実際のANCアルゴリズム(FxLMS)をPythonコードで追体験する
  • 市販ANC製品がどの方式を採用しているのかを知る

「ANC=なんとなく逆位相の音を出している」から一歩踏み込み、
エンジニアとして腹落ちする理解を目指します。

対象読者は 中級者向け。
DSPやPythonに軽く触れたことがあれば十分ついてこられます。


1. ANCとは何か? ― 受動型との決定的な違い

1.1 パッシブノイズキャンセリングとの比較

まず混同されがちな用語を整理します。

  • PNC(Passive Noise Cancellation)
    イヤーパッドやイヤーチップで物理的に音を遮断
    → 高周波に強いが、低周波に弱い

  • ANC(Active Noise Cancellation)
    マイクで騒音を検出し、逆位相の音を生成
    → 低周波に非常に強い

特に 100Hz〜1kHz以下の低周波ノイズ はANCの独壇場です。
エンジン音・空調音・走行音がよく消える理由はここにあります。


2. ANCの方式分類

ANCは大きく3種類に分かれます。

2.1 フィードフォワード型

  • 外側マイクで騒音を取得
  • 早めに逆位相音を生成

メリット - 応答が速い - 外部騒音に強い

デメリット - 装着ズレに弱い - 伝達関数誤差の影響を受けやすい

多くのワイヤレスイヤホンで採用。


2.2 フィードバック型

  • 耳元マイクで残留ノイズを検出
  • 結果を見て補正

メリット - 装着誤差に強い - 安定性が高い

デメリット - 高周波で不安定になりやすい - 遅延に弱い


2.3 ハイブリッド型

現在の フラッグシップANC製品の主流 です。


3. ANCをDSPとして捉える

ANCは制御工学+DSPの世界です。

3.1 システム構成

ANCの基本ブロックは以下。

  • 騒音信号:n(t)
  • 制御フィルタ:W(z)
  • スピーカー〜耳までの伝達関数:S(z)(Secondary Path)
  • 誤差信号:e(t)

目的は明確です。

誤差信号 e(t) を最小化する


4. 実践:FxLMSアルゴリズムをPythonで実装する

4.1 なぜFxLMSなのか

通常のLMSでは、スピーカーや耳の伝達特性 S(z) を考慮できません。
そこで登場するのが FxLMS(Filtered-x LMS) です。

FxLMSのポイントは、

  • 参照信号を Secondary Pathで事前にフィルタ
  • その結果を使って係数更新

という点にあります。


4.2 Pythonによる簡易実装

以下は ANCの学習挙動を理解するための最小構成コード です。

import numpy as np

# パラメータ
N = 10000           # サンプル数
mu = 0.0001         # 学習率
filter_len = 32     # フィルタ長

# 信号生成
noise = np.random.randn(N)
desired = np.zeros(N)

# Secondary Path(簡易モデル)
secondary_path = np.array([0.5, 0.3, 0.2])

# 制御フィルタ
W = np.zeros(filter_len)
x_buf = np.zeros(filter_len)

# シミュレーション
for n in range(N):
    x_buf = np.roll(x_buf, 1)
    x_buf[0] = noise[n]

    # 出力
    y = np.dot(W, x_buf)

    # Secondary Path通過
    y_sec = np.convolve([y], secondary_path)[0]

    # 誤差
    e = desired[n] - y_sec

    # Filtered-x
    x_f = np.convolve(x_buf, secondary_path)[:filter_len]

    # LMS更新
    W += 2 * mu * e * x_f

このコードでは、

  • 騒音をランダム信号として生成
  • ANCが徐々にノイズを打ち消す方向に学習

という 「ANCの心臓部」 を体験できます。


5. 実製品で使われているANC技術

5.1 Sony WH-1000XM5

  • ハイブリッドANC
  • 専用QN1プロセッサ
  • 装着状態・気圧補正あり

製品リンク:
SONY WH-1000XM5


5.2 Bose QuietComfort Ultra Headphones

  • フィードバック重視設計
  • 人間の聴覚特性に最適化

製品リンク:
Bose QuietComfort Ultra


5.3 Apple AirPods Pro(第2世代)

  • H2チップによるリアルタイム適応
  • 外耳道形状を個別補正

製品リンク:
AirPods Pro


6. ANCの限界と今後

ANCは万能ではありません。

  • 突発音(拍手・食器音)には弱い
  • 高周波は物理遮音頼り
  • 遅延・ハウリングのリスク

今後は、

  • AIによる環境分類
  • 個人の耳特性に適応するANC
  • 車載・建築分野への展開

といった方向に進化しています。


7. まとめ

ANCは単なる「便利機能」ではなく、

が融合した 高度なエンジニアリング技術 です。

この記事で紹介したFxLMSを理解できれば、
ANC製品を見る目が確実に変わるはずです。

次のステップとしては、

  • 実測したSecondary Pathでのシミュレーション
  • リアルタイム処理(PyAudio / C実装)
  • 周波数領域ANC

に挑戦してみてください。


参考文献・リンク

TechCraftでは、今後も 「中身が分かる技術解説」 を続けていきます。

TPU(Tensor Processing Unit)とは?

TPUとは?

――GPU時代のその先にある「テンソル専用プロセッサ」

1. いまさらだけど「TPUって何者?」

TPU(Tensor Processing Unit)は、Google が自社の機械学習ワークロードを高速化するために設計した 専用プロセッサ(ASIC) です。

テンソル/tensor(名詞) 座標系を変換しても一定の変換規則に従って成分が変わる、多次元の量。 スカラー(0次)、ベクトル(1次)、行列(2次)を一般化した数学的概念で、物理学・機械学習・統計において利用される。

  • CPU:汎用。OS・アプリ・制御など何でもこなす
  • GPU:もともとはグラフィックス、今は行列計算にも強い汎用並列プロセッサ
  • TPU:ニューラルネットワーク(テンソル演算)専用に振り切ったプロセッサ

という位置付けになります。Google はこの TPU を自社サービス(検索 / 翻訳 / 写真 / Maps / Gemini など)の裏側で使うだけでなく、Cloud TPU として Google Cloud 上からも利用可能にしています。

最近では第7世代「Ironwood TPU」まで進化しており、
1チップあたり数千 TFLOPS 級の演算性能と数百 GB/s クラスをはるかに超えるメモリ帯域を持つ、AI 特化スーパーコンピュータの“心臓”になりつつあります。


2. なぜ「Tensor Processing Unit」なのか

名前にある “Tensor” は、ディープラーニングで扱う多次元配列(行列・テンソル)を指します。

2.1 ニューラルネットは行列だらけ

ニューラルネットワークの内部では、ざっくり言うと

  • 行列×行列(全結合層)
  • 行列×ベクトル(畳み込み層の後半や注意機構)
  • 大量の加算・活性化関数

といった計算がひたすら繰り返されています。

この「行列演算を、できるだけ効率よく回したい」という1点に全振りしたのが TPU です。

Cloud TPU の公式ドキュメントでも、

「ML アルゴリズムで多用される大規模行列演算を高速に実行するために設計された ASIC」

と明言されています。

2.2 CPU/GPU/NPU/TPUのざっくり比較

  • CPU

    • 強み:汎用性・分岐の多い処理・OSや制御系
    • 弱み:行列演算のスループットは専用チップに劣る
  • GPU

    • 強み:大量のコアで並列演算、行列計算にも強い
    • 弱み:もともとグラフィックス向け設計で、AI 向けには“汎用並列プロセッサ”としての性格が残る
  • NPU(Neural Processing Unit)

    • 強み:ニューラルネット向けに設計されたプロセッサ全般を指すカテゴリ
    • TPU を含め、各社が独自に設計する AI 専用チップ
  • TPU

    • 強み:Google が設計した NPU の一種で、「行列演算+Google のソフトスタック」に特化
    • Google のクラウド・サービス・AI 研究のために最適化された“社内専用→外販もする”タイプのチップ

3. TPU のアーキテクチャをざっくり分解する

細かい数字を追い始めると沼なので、構造のイメージだけ掴んでおきます。

3.1 ASIC(特定用途向け集積回路)

TPU は ASIC(Application Specific Integrated Circuit)です。
これは GPU のような「汎用プロセッサ」ではなく、“用途を決め打ちして作った LSI” という意味です。

  • 命令セットや演算器は「テンソル演算」に最適化されている
  • 逆に言うと、それ以外の用途にはあまり向かない(汎用性を犠牲にスループットを取っている)

3.2 行列乗算ユニット(MXU)とシストリックアレイ

TPU の心臓部にあるのが 行列乗算ユニット(MXU) です。
これは、シストリックアレイと呼ばれる構造(演算素子を格子状に並べ、データを“リズムよく”流して処理する構造)を用いて、行列×行列の積を一気に計算するユニットです。

  • 入力側から行列の要素を順番に流し込む
  • 格子状に並んだ MAC(Multiply-Accumulate)ユニットが、拍を刻むように演算を繰り返す
  • 出力側から結果の行列が押し出されてくる

…という「ベルトコンベア式の行列計算工場」をイメージすると分かりやすいです。

3.3 高帯域メモリ(HBM)とオンチップメモリ

ニューラルネットのボトルネックは、計算量だけでなく「データをいかに速く供給できるか」です。

TPU では、

  • HBM(High Bandwidth Memory) をチップ近傍に載せ、数 TB/s 級のメモリ帯域を確保
  • オンチップ SRAM も活用して、「同じ重みを何度も遠くから読み直さない」設計

といった工夫で、MXU が空回りしないように供給を続けます。

3.4 ポッド(Pod)と相互接続

最新世代の TPU では、数千〜数万チップを相互接続した「TPU Pod」 として動作させることができます。

  • TPU v5p Pod:数千チップを接続し、大規模モデルの分散学習に対応
  • Ironwood TPU Pod:9,216 チップで 40 エクサFLOPS超の計算性能を実現可能と言われる

チップ間の通信を最適化するために、メッシュ/トーラスなどの専用ネットワークトポロジが用いられており、大規模な行列演算を“分割して投げる”前提の設計になっています。


4. TPU の世代(ざっくり)

細かいスペックはおいておき、進化の方向性だけ押さえておきます。

  • 第1世代(TPU v1)

    • 主に推論(Inference)向け
    • 16bit 整数演算(bfloat16 以前)で高スループット
    • すでに AlphaGo などの初期 DeepMind システムで活用
  • 第2・3世代(TPU v2/v3)

    • 学習(Training)も本格サポート
    • HBM 搭載、Pod 構成で数百〜数千チップを接続
  • 第4世代以降(v4/v5e/v5p など)

    • 大規模 Transformer / 大規模言語モデル(LLM)を意識した設計
    • スケールアップ・スケールアウト双方に最適化
    • v5e は学習と推論の“両用”、v5p は GenAI 用のハイエンド学習向け製品として位置づけ
  • Trillium / Ironwood(第6・7世代)

    • 電力効率の改善とチップあたりの FLOPS をさらに引き上げ
    • Ironwood では 1 チップで 4,614 TFLOPS、192GB HBM、7.2TB/s メモリ帯域という超ハイエンド仕様を謳う

世代が進むにつれ、

  • 精度(bfloat16 / FP8 等)の選択肢拡大
  • メモリ容量・帯域の増強
  • チップ間接続の柔軟化(再構成可能なメッシュネットワーク)

といった方向に進化している、というイメージで捉えておくとよいです。


5. TPU vs GPU:何が違うのか

5.1 単に「速いチップ」ではない

Cloud TPU のサイトでは、GPU との違いをざっくり次のように説明しています。

  • GPU:
    • グラフィックスから派生した汎用並列プロセッサ
    • AI 以外にも用途が広く、エコシステム(CUDA 等)が非常に成熟
  • TPU:
    • ニューラルネット専用に設計された ASIC
    • 行列乗算ユニット(MXU)や専用インターコネクトなど AI 向けの機能を搭載
    • Google Cloud 上での利用を前提に最適化されている(≒オンプレで自由に買って挿すものではない)

ざっくり特徴を表にすると:

CPU GPU TPU
用途 汎用 汎用だが並列演算に強い ニューラルネット向けに特化
形態 サーバ / PC / 組込み サーバ / PC / 組込み ほぼ Google Cloud / 一部 Edge 用モジュール
得意分野 OS, ロジック, 制御 行列演算, グラフィックス 大規模行列演算, 分散学習
エコシステム きわめて豊富 CUDA, ROCm, 各種フレームワーク 主に TensorFlow / JAX / PyTorch(XLA)

5.2 エンジニア目線でのメリット・デメリット

メリット

  • 大規模モデルの学習・推論で、電力効率・コスト効率が高いケースが多い
  • Google のソフトウェアスタック(JAX / TensorFlow など)と密に統合されている
  • Pod を使った大規模分散学習を比較的少ない設定変更で利用できる

デメリット / 注意点

  • 基本的に Google Cloud 専用ハードウェア なのでベンダーロックインを伴う
  • GPU に比べると、低レベル API やエコシステムはまだ狭い
  • 「なんでもかんでも TPU に乗せれば速い」わけではなく、行列演算中心でないワークロードは得意ではない

6. 実際どうやって使うのか(ごく簡単に)

6.1 Cloud TPU の利用イメージ

Cloud TPU では、GCE インスタンスではなく 「TPU VM」 として直接ログインできる環境が提供されています。

JAX での利用イメージ(超ざっくり):

import jax
import jax.numpy as jnp

# TPU/CPU/GPUにまたがって配列を配置
x = jnp.ones((1024, 1024), dtype=jnp.float32)
w = jnp.ones((1024, 1024), dtype=jnp.float32)

@jax.jit  # XLAコンパイル(TPU向け機械語に変換)
def matmul(x, w):
    return jnp.dot(x, w)

y = matmul(x, w)
print(y.shape)
  • jax.jit を付けることで、JAX/XLA が行列演算をまとめてコンパイル
  • 実行対象(TPU / GPU / CPU)は環境側の設定で切り替え

TensorFlow の場合も tf.distribute.TPUStrategy を使って、
「GPU で動いていたコードをほぼそのまま TPU に載せ替える」といったことが可能です。

6.2 どんなワークロードに向いているか

など、「行列演算中心で、巨大なモデルを大量に回したい」ワークロードに強みがあります。

逆に、

  • 条件分岐が多いロジック
  • 小規模でメモリ依存が強い処理
  • GPU で最適化され尽くしたメディア処理系

などは、無理に TPU に載せても旨味が少ないことが多いです。


7. Edge TPU:データセンタの外に出た TPU

ここまで話してきた TPU は、「Google のデータセンタの中」にある Cloud TPU が中心でしたが、
エッジ向けの TPU も存在します。

7.1 Edge TPU とは

Edge TPU は、

  • 低電力・小型
  • 推論(Inference)専用
  • Raspberry Pi や小型 Linux マシンと組み合わせて使う

といった用途に向けた、オンデバイス用 TPU です。

たとえば Google Coral シリーズの USB / M.2 / Mini PCIe モジュールに搭載されていて、

  • 監視カメラの映像をローカルで物体検出
  • 工場内の異常検知をデバイス側で実行
  • ネットに出せない環境での AI 推論

といった「エッジAI」用途で使われます。

7.2 ラズパイ+Edge TPUで学ぶ

日本語で Edge TPU をちゃんと扱った本としては、

  • ラズパイとEdge TPUで学ぶAIの作り方(高橋 秀一郎)

があります。
Raspberry Pi と Edge TPU を組み合わせて、

  • TensorFlow Lite モデルの作成
  • Edge TPU 向けコンパイル
  • 実機での推論

といった流れを実践しながら学べる構成になっており、
「Cloud TPU はハードルが高いけど、手元で TPU を触ってみたい」という人にちょうどよい入門パスです。


8. 戦略的な意味での TPU:なぜ各社は「自前チップ」を持ちたがるのか

最後に、少し視点を引いて「TPU がなぜここまで重視されているのか」を整理しておきます。

  • 生成 AI・LLM・マルチモーダル AI の時代では、計算資源そのものが競争力 になっている
  • GPU 供給がボトルネックになり、各社は「Nvidia 依存」からの脱却を模索している
  • Google は TPU によって、自社の AI インフラを 垂直統合(モデル〜ソフト〜チップ)で最適化し、
    コスト・電力・性能の三方を押し上げようとしている

同様の動きは他社にも見られます。

  • AWS:Trainium / Inferentia
  • Microsoft:Azure Maia / Cobalt
  • Meta なども独自 AI チップを検討

つまり TPU は、「ただの速いチップ」ではなく、

AI 時代のクラウド・プラットフォーム戦争における、Google の戦略兵器

という位置づけでもあります。

エンジニアとしては、

  • GPU だけでなく TPU という選択肢も視野に入れておく
  • AI インフラが「クラウド+専用チップ」の形で急速に変化している

という全体像を押さえておくと、アーキテクチャ設計やクラウド選定の判断軸が増えるはずです。


9. まとめ

  • TPU(Tensor Processing Unit)は、Google が設計した ニューラルネット専用 ASIC
  • 行列演算ユニット(MXU)や高帯域メモリ(HBM)を備え、行列演算だらけのディープラーニングを全力で高速化する
  • Cloud TPU として、Google Cloud 上で大規模モデルの学習・推論に利用できる
  • GPU に比べて汎用性は低いが、AI ワークロードに限れば電力効率・コスト効率で強みを持つケースが多い
  • Edge TPU を使えば、Raspberry Pi などの小型デバイスでオンデバイス推論ができる
  • TPU をはじめとする「自前 AI チップ」は、クラウド事業者にとって AI インフラ覇権のカギ になりつつある

TPU そのものを日常的に触るエンジニアはまだ多くありませんが、
AI・データサイエンス・クラウドインフラに関わるなら、「TPU とは何か」「GPU とどう違うのか」を押さえておく価値は大きいと思います。


参考リンク・書籍

公式ドキュメント

解説記事(日本語)

Edge TPU で学びたい人向け書籍

  • ラズパイとEdge TPUで学ぶAIの作り方
    Edge TPU を搭載したモジュールと Raspberry Pi を使って、推論モデルを実際に動かしながら学べる日本語の入門書。Cloud TPU よりも手元で「TPU っぽさ」を体験したい人にちょうど良い。
    ラズパイとEdge TPUで学ぶAIの作り方

Autosar と SDV

AUTOSARとSDV

――クラシックECU時代からソフトウェア定義車両への橋渡し

1. なぜ「AUTOSARとSDV」をセットで考えるべきか

ここ数年、自動車ソフトウェア界隈では

  • AUTOSAR(Classic / Adaptive)
  • SDV(Software-Defined Vehicle:ソフトウェア定義車両)
  • ドメイン/ゾーンアーキテクチャ
  • SOAFEE、Eclipse SDV といったオープンイニシアチブ

といったキーワードが一気に増えました。

ざっくり言うと、

  • AUTOSAR:ECUソフトウェアの標準プラットフォーム
  • SDV:クルマ全体を「アップデートされ続けるソフトウェア製品」として捉える考え方とアーキテクチャ
  • SOAFEE / Eclipse SDV:SDV を実現するためのオープンなソフトウェア基盤

という関係になっています。

従来の「ECU単位の開発」から、「車両全体を1つの分散システムとして見る」世界にシフトする中で、
AUTOSAR エンジニアは何をどうアップデートすればいいのか。
この記事では、その接続点を整理していきます。


2. AUTOSARざっくり復習 ― Classic と Adaptive

2-1. AUTOSAR Classic Platform(CP)

AUTOSAR Classic Platform は、マイコンベース ECU 向けの標準規格です。
アーキテクチャの最上位では、以下の3層に分かれています。

  • Application
  • RTE(Runtime Environment)
  • Basic Software(BSW)

特徴をざっくりまとめると:

  • OSEK/VDX ベースの静的 OS
  • C 言語中心
  • コンフィグレーションは基本コンパイル時に確定(静的)
  • 車体制御・パワートレイン・ボディ系など、クラシックECUで今も主流

大量の ECU を安定的に量産する世界で、「再利用・自動化・標準化」を進めるための基盤として発展してきました。

2-2. AUTOSAR Adaptive Platform(AP)

一方、AUTOSAR Adaptive Platform は

高性能マイコン/SoC 上で、C++ / POSIX OS / サービス指向アーキテクチャを前提としたプラットフォーム

として定義されています。

主な特徴:

  • POSIXベースの動的 OS(Linux/QNX など)
  • C++ / オブジェクト指向
  • サービス指向アーキテクチャ(SOA)
  • ソフトウェアの動的ロード、アップデート、コンフィグ変更を想定
  • 自動運転、ADAS、高度なインフォテインメントなど高性能コンピューティング(HPC)ECU向け

AUTOSAR公式も、「AP は CP の上位互換ではなく、適材適所で共存する」と明言しています。

2-3. CP と AP の共存イメージ

SDV 時代の典型的な構成をざっくりイメージすると:

が配置され、Ethernet/SOME/IP や CAN/FlexRay 等でつながる構図になります。


3. SDV(Software-Defined Vehicle)とは何か

3-1. 「SDV」の中身をバラしてみる

SDV はバズワードっぽく聞こえますが、実体はかなり具体的です。
各社・各団体の定義を総合すると、だいたい次のような特徴が挙げられます。

  • OTA(Over-The-Air)更新が前提
    • 不具合修正だけでなく、機能追加・性能向上・サブスク課金などを車両ライフサイクル全体で行う
  • ハードとソフトの分離
    • ハードウェアを長寿命・高汎用に設計し、その上でソフトウェアを頻繁に入れ替える
  • 集中・ゾーンアーキテクチャ
    • 従来の「ドメイン別 ECU の寄せ集め」から、ゾーン+中央 HPC の構成へ
  • 標準化された車載OSとAPI
  • クラウド連携・DevOps
    • クラウド上でのシミュレーション・CI/CD・大規模フリート管理を前提にした開発/運用プロセス

PwC や半導体ベンダの資料では、「レベル3以上の自動運転向け車両は概ね SDV に分類される」といった整理もされています。

3-2. ドメイン→ゾーンアーキテクチャ

E/E アーキテクチャの観点では、

  • 従来:
    • パワートレイン / ボディ / ADAS / インフォテインメント などドメイン別 ECU 群
    • 車両1台あたり 100個近い ECU というケースも
  • SDV 志向:
    • 車両を物理的な位置で区切ったゾーン制御 ECU+中央 HPC
    • ECU 数を 20 以下にまで削減し、配線・重量・コストを削る

といった変化が進んでいます。

ゾーン ECU は、複数ドメインのセンサ/アクチュエータを束ねて中央 HPC に集約する役割を持ち、
中央 HPC 上でソフトウェアサービスを動的に配置・アップデートしていく、というのが典型的な SDV 像です。


4. AUTOSARはSDVのどこにフィットするのか

4-1. Classic:ゾーン/エッジ側のリアルタイム制御

ゾーンアーキテクチャでは、ゾーン ECU や一部のアクチュエータ制御は、今後も「マイコン + RTOS」が主役です。

  • ステアリング・ブレーキ・エアバッグなどの安全クリティカル機能
  • ボディ制御(ライト、窓、ドアロック)
  • パワートレインのローレベル制御

こういった領域では、AUTOSAR Classic Platform が引き続き有力な選択肢になります。

CP の役割は、

  • センサ値の取得・前処理
  • 安全クリティカルな制御ループ
  • 中央HPCとのゲートウェイ機能の一部

といった「ゾーン/エッジに近いリアルタイム処理」を担うことが多くなります。

4-2. Adaptive:中央HPC上のサービス基盤

中央HPC側では、以下のような要件が出てきます。

  • 高度な環境認識・経路計画(自動運転)
  • インフォテインメント、音声アシスタント、アプリエコシステム
  • 車両データの収集・解析・OTA 更新
  • サービス指向アーキテクチャ(SOA)による機能提供

ここでフィットするのが AUTOSAR Adaptive Platform です。

AP の特徴は、まさに SDV の要求と重なります。

  • POSIX OS 上で C++ アプリケーションをサービスとして動的に実行
  • SOME/IP や DDS を用いたサービス指向通信
  • セキュリティ(認証・暗号化)と機能安全に配慮したアーキテクチャ
  • OTA によるアップデートを想定した実行環境

言い換えると、AUTOSAR Adaptive は「SDV 時代の in-vehicle OS/API 標準候補の1つ」だと見なせます。

4-3. CP/AP+オープンスタックのハイブリッド

ただし、すべてを AUTOSAR だけで完結させるのが唯一の答えではありません。

といった棲み分けがあり、実際の車両では

  • 下回り:AUTOSAR CP
  • 中央HPC:
    • 基本の車載サービス:AUTOSAR AP or AP + 独自ミドル
    • 上位アプリケーション:Linux + コンテナ(SOAFEE/Eclipse SDV系)

といったレイヤー分けが増えていくはずです。


5. SDVのオープンエコシステムとAUTOSAR

5-1. Eclipse SDV:オープンな車載ソフトプラットフォーム

Eclipse Software Defined Vehicle(Eclipse SDV)ワーキンググループは、
「車載ソフトウェアプラットフォームをオープンソースで作ろう」という取り組みです。

役割をざっくり挙げると:

  • SDV.Edge:車載側のサービスファブリックやランタイム・通信
  • SDV.Ops:クラウド側のフリート管理・OTA・運用基盤
  • SDV.Dev:開発ツールチェーン・CI/CD・仮想環境

AUTOSAR が「仕様ベースの標準化」であるのに対し、
Eclipse SDV はコードファーストなオープンソース実装に重心があります。

両者は競合というより、以下のような関係に持っていくのが現実的です。

  • AUTOSAR:車載向けの API / サービス定義・抽象化レイヤ
  • Eclipse SDV:それを利用するアプリケーションやツールチェーンの一部、あるいは非安全クリティカル領域の実装

5-2. SOAFEE:クラウドネイティブな車載エッジ

SOAFEE(Scalable Open Architecture for Embedded Edge)は、Arm 主導のイニシアチブで、
「クラウドネイティブな開発スタイルを車載エッジに持ち込む」ことを目指しています。

特徴的なポイント:

こちらも、AUTOSAR と以下のように共存させるイメージが考えられます。

  • AUTOSAR AP:安全クリティカル寄りのサービス
  • SOAFEE スタック:インフォテインメントやデータ処理・AI推論などの非安全クリティカルなサービス

つまり、SDV プラットフォームは複数のスタックの“合奏”になる、というのが現実的な姿です。


6. AUTOSARエンジニアがSDV時代に身につけたいこと

では、すでに AUTOSAR(主に Classic)をやっているエンジニアが、
SDV 時代に向けてどこをアップデートしていけばいいか、具体的に挙げてみます。

6-1. POSIX / C++ / サービス指向

Adaptive Platform を意識するなら:

  • POSIX OS(Linux / QNX など)の基本
  • C++17 以降のモダン C++
  • SOME/IP / DDS などのサービス指向通信
  • プロセス間通信(IPC)・マルチスレッド・非同期処理

は避けて通れません。

Classic で培った RTE / BSW / COM の感覚は、そのまま

  • AP の ARA(AUTOSAR Runtime for Adaptive Applications)の理解
  • サービス契約と実装の分離

に生きてきます。

6-2. ゾーン/HPCアーキテクチャとネットワーク

SDV 文脈では、ソフトウェアだけでなく E/E アーキテクチャの知識も重要です。

「このサービスはどのゾーンのどの ECU で動くのが良いのか?」という設計をするには、
ハードウェア側の制約も踏まえた視点が必要になります。

6-3. クラウド/DevOps/オープンソース

SDV では、ソフトウェアのライフサイクルが

  • クラウド上での CI/CD
  • テストベンチやシミュレータでの検証
  • OTA によるフリートへの展開
  • 運用時のテレメトリ収集・フィードバック

というループで回ることが前提になります。

そのため、

  • Git / CI サーバ / コンテナ / Kubernetes などの DevOps ツールチェーン
  • オープンソースプロジェクト(Eclipse SDV / SOAFEE)との付き合い方
  • セキュリティライフサイクル(SBOM, 脆弱性対応)

といったテーマも、AUTOSAR に閉じない知識として徐々に押さえておきたいところです。


7. 学習ロードマップ例:AUTOSARからSDVへ

最後に、実務エンジニア向けに「何から手をつけるか」の一例をまとめます。

  1. AUTOSAR Classic の基礎を固める

    • RTE / BSW / COM / MEM / DIAG あたりを一通り経験
    • ECU コンフィグツールの使い方・生成コードの読み方を理解
  2. Adaptive Platform に触れてみる

    • POSIX / C++17 ベースのサンプルアプリ
    • SOME/IP によるサービス公開・利用
    • Adaptive AUTOSAR 対応ツール(仮想ECU等)で少し遊んでみる
  3. E/E アーキテクチャと SDV の概要を押さえる

    • ドメイン→ゾーン移行の記事や半導体ベンダのホワイトペーパーを読む
    • 「中央HPC+ゾーンECU」の役割分担を自分なりに図にしてみる
  4. オープンスタックを触る

    • Eclipse SDV / SOAFEE のドキュメントを眺める
    • 可能ならコンテナベースのサンプルをローカルやクラウドで動かしてみる
  5. クラウド連携と DevOps を自分の現場に引き寄せる

    • 既存の AUTOSAR プロジェクトでも、「ビルド〜静的解析〜ユニットテスト」を CI に乗せてみる
    • テストベンチ/SIL 環境をクラウド上に用意するイメージを持つ

このあたりまで来ると、「AUTOSAR エンジニア」から「SDV 時代の車載ソフトエンジニア」への地ならしがかなり進んでいるはずです。


8. 参考リンク

AUTOSAR公式

SDV / ゾーンアーキテクチャ

オープンイニシアチブ

AUTOSAR と SDV は「どちらか一方」ではなく、
これからの車載ソフトウェアを支える両輪として理解しておくと、設計の選択肢もキャリアの選択肢も一気に広がってきます。

3Dデータ解析入門: 数学とPythonで点群処理を理解する

点群 × 数学 × Python

――3次元データ時代のデータサイエンス入門

3次元点群処理と数学

1. 点群の時代に「ちゃんと数学」をやる意味

LiDAR、RGB-D カメラ、フォトグラメトリ、SLAM、3D スキャナ。
現実世界をスキャンしてそのままデータにする手段が一気に増えたおかげで、「3次元点群」はもはや研究室だけのものではなくなりました。

  • 自動運転の周囲認識
  • ロボットの地図生成
  • VR/AR の空間把握
  • 建築・土木の BIM/CIM
  • 工場のデジタルツイン

こうした分野では、

「大量の 3D 点の集まり」を、いかに効率よく処理し、特徴を抽出し、モデルにつなげるか

が日常業務になってきています。

この世界に踏み込むとき、
Python で触れるコード力と、
線形代数・確率・統計といった数学の基礎の両方が効いてきます。

  • 行列・ベクトルが分かると、点群の回転・平行移動・射影が腑に落ちる
  • 確率・統計が分かると、ノイズをどう扱うか、外れ値をどう見るかが変わる
  • ライブラリ(NumPy / scikit-learn / Open3D)が読めると、点群アルゴリズムの実装が「黒魔術」ではなくなる

この記事では、

  • データサイエンス数学の“現場で効く”押さえどころ
  • 点群処理の基本フローと Python 実装の雰囲気
  • 3D × 機械学習までつながるロードマップ

を、2冊の良書と結びつけながら整理していきます。


2. データサイエンスに必要な数学を「Python で」押さえる

2.1 現場で本当に使うのはどの数学か

データサイエンスと言うと、「数学が必要」とはよく言われるものの、
実務の現場でよく顔を出すのはだいたい次の4つです。

  1. 線形代数

  2. 微積分

  3. 確率

    • 事象・確率・条件付き確率
    • 期待値・分散・共分散
    • ベイズの考え方
  4. 統計

    • 推定(平均・分散・区間推定)
    • 検定(仮説検定・p値)
    • 回帰分析(線形回帰・ロジスティック回帰)

これらが、線形回帰・ロジスティック回帰・ニューラルネットワーク・クラスタリングなど、
ほとんどの「実務で使うモデル」の裏側で動いています。

2.2 数式よりも「コードで見る」アプローチ

数学書で挫折しがちなポイントは、「記号が多くてイメージしづらい」ことです。

そこで、

  • ベクトルの内積 → NumPy で書いてみる
  • 行列の固有値 → numpy.linalg.eig で触ってみる
  • 線形回帰 → scikit-learn の LinearRegression を使って、「自分で導いた式」と答え合わせする
  • ニューラルネット → ほんの数十行の Python で単純なネットワークを自前実装してみる

という方向から攻めると、
「式で読んだ内容」が「コードと実行結果」として頭に定着しやすくなります。

例えば、線形回帰の最も基本的な形は、
「誤差二乗和を最小にするようなパラメータを探す」問題です。

import numpy as np

# シンプルな線形回帰を NumPy で最小二乗解
X = np.array([[1, x] for x in [1, 2, 3, 4, 5]], dtype=float)  # バイアス項1を含める
y = np.array([1.2, 1.9, 3.1, 3.9, 5.2])

theta = np.linalg.inv(X.T @ X) @ X.T @ y  # 正規方程式

print(theta)  # [切片, 傾き]

「最小二乗法」「正規方程式」と聞くと一気に難しく感じるところですが、
コードで動かしてみると、「ああ、X の転置と行列積でこうなるのか」と感覚がつかめます。

「データサイエンスのための数学入門」はまさに、こうした “コードから数式へ” の橋渡しをしてくれるタイプの本です。


3. 点群処理とは何をするのか

次に、3次元点群の世界に目を向けてみます。

3.1 点群 = 「3D 空間にばらまかれた点の集合」

点群(Point Cloud)とは、ざっくり言えば

3D 空間上の位置を示す点 (x, y, z) の集まり

です。RGB-D カメラなら (x, y, z, R, G, B) のように色情報も付くことがあります。

この点群に対して、典型的には以下のような処理を行います。

  • フィルタリング:ノイズ除去・ダウンサンプリング(Voxel Grid など)
  • 特徴抽出:法線ベクトル・キーポイント・局所特徴量
  • レジストレーション:複数の点群の位置合わせ(ICP など)
  • セグメンテーション:平面・物体などへの分割
  • 認識:物体クラスの分類・検出(PointNet など)

これらを Python で実装しつつ、アルゴリズムの意味を丁寧に追っていくのが、「詳解 3次元点群処理」です。

3.2 Open3D で触る点群の基本

Python で点群を触る場合、Open3D が事実上の標準ライブラリになりつつあります。

雰囲気だけ示すと、こんなコードで点群の読み込みとダウンサンプリングができます。

import open3d as o3d

# PLY/PCD 形式などの点群ファイルを読み込む
pcd = o3d.io.read_point_cloud("example.pcd")

print(pcd)              # 点数やバウンディングボックスを表示
o3d.visualization.draw_geometries([pcd])  # 3Dビューアで表示

# ボクセルグリッドによるダウンサンプリング
down = pcd.voxel_down_sample(voxel_size=0.05)
o3d.visualization.draw_geometries([down])
  • 点群の読み書き
  • ダウンサンプリング・外れ値除去
  • 法線推定
  • ICP による位置合わせ
  • メッシュ化

といった処理が、Open3D を使うとかなり短いコードで書けます。
本としての「詳解 3次元点群処理」は、その背後にある数学とアルゴリズムをちゃんと説明しながら、Open3D のコードまでカバーしているのがポイントです。


4. 点群処理で効いてくる数学

4.1 線形代数:回転と平行移動は「行列」で書く

3D 点群の基本操作である「回転」と「平行移動」は、線形代数そのものです。

  • 点 p = (x, y, z) を回転行列 R と平行移動ベクトル t で変換するとき、

    p' = R p + t

  • ICP(Iterative Closest Point)では、「対応する点同士の距離を最小にする R, t を求める」問題を繰り返し解く

このとき、「回転行列の条件(直交行列)」「最小二乗問題」「特異値分解(SVD)」といった線形代数が登場します。

4.2 確率・統計:ノイズと外れ値をどう扱うか

点群には、必ずノイズと外れ値が含まれます。

  • センサの精度限界による measurement noise
  • 反射の悪い材質・遠距離などによる欠損
  • 物体の動きによるブレ

これらを扱うには、

といった統計的な視点が重要です。

例えば、平面検出に RANSAC を使う場合、

  1. ランダムに 3点を選んで平面モデルを仮定
  2. その平面から一定距離以内にある点を「インライア」とみなす
  3. インライア数が最大になるモデルを採用

という確率的アルゴリズムになります。
ここでも、確率・統計の基礎があると「なぜランダムでうまくいくのか」が理解しやすくなります。

4.3 深層学習:PointNet からその先へ

最近の 3D 認識では、

  • PointNet / PointNet++
  • PointCNN
  • Graph Neural Networks (GNN) ベースのモデル

など、「点群を直接ニューラルネットに入れる」アプローチが主流になりつつあります。

これらは、

といった、データサイエンス数学の上に立っているモデルです。

「詳解 3次元点群処理」では、最終章で PointNet を含む深層学習による 3D 処理にも触れており、
「クラシカルな点群アルゴリズム」と「ディープラーニング」を両方おさえる足場を作ってくれます。


5. 学習の進め方:数式 → Python → 点群アルゴリズム

2冊の本をベースに、「数理 × 点群 × Python」の学び方をざっくり組み立ててみます。

ステップ1:Python で数学の手触りを身につける

  • NumPy でベクトルや行列をいじる
  • Matplotlib で簡単な散布図・回帰直線を描く
  • scikit-learn で線形回帰・ロジスティック回帰・k-means を一通り動かす

この段階では、「厳密な証明」よりも「コードと出力を見て納得する」ことを優先してOKです。

ステップ2:3D 点群の基本操作を覚える

  • Open3D で PCD/PLY ファイルを読み込む
  • ダウンサンプリング・外れ値除去を試す
  • 法線推定・平面検出・簡単なICPを回してみる

ここで、線形代数(行列変換)と点群処理が頭の中でつながってくるはずです。

ステップ3:点群アルゴリズムを「自分の言葉」で説明できるようにする

  • ICP の1ステップで何をしているかを、擬似コードに落としてみる
  • RANSAC の流れを、図とコメント付きコードで説明してみる
  • 特徴点と特徴量抽出(FPFH 等)の意味を、付き合いのある2D特徴量(SIFT/SURF 等)との類似で整理してみる

このあたりまで来ると、「3D データサイエンス」の雰囲気がかなり掴めてくるはずです。


6. キャリアの視点から見た「数学 + 点群 + Python」

実務のキャリアという観点で見ても、この組み合わせは悪くありません。

  • 自動運転・モビリティ
  • ロボティクス
  • 建設・土木・測量
  • XR(VR/AR/MR)
  • 製造業のデジタルツイン

これらの業界で共通して求められているのは、

  1. Python でのデータ処理・モデリングのスキル
  2. 数学的な背景を理解する力(線形代数・確率・統計)
  3. センサから出てくる 3D データを扱える知識(点群処理)

です。

「データサイエンスのための数学入門」でコードと一緒に数学の基礎を固め、
「詳解 3次元点群処理」で 3D データの具体的な処理パイプラインを学ぶ、という流れは、
この3つをバランスよく伸ばすのにちょうど良い組み合わせになっています。


7. まとめ:2冊でつなぐ「数理基盤」と「3D現場力」

最後に、本記事のポイントを整理します。

  • データサイエンスに必要な数学は、線形代数・微積・確率・統計が中心
  • 数式だけでなく、Python(NumPy / SymPy / scikit-learn)で手を動かして理解すると、現場で使える形で身につく
  • 3次元点群処理は、
    • フィルタリング
    • 特徴抽出
    • レジストレーション
    • セグメンテーション
    • 認識(PointNet など) といった一連のステップで構成されており、その多くが線形代数と統計の応用
  • Open3D を使えば、Python から点群を簡単に触れる
  • 数学+Python+点群処理の組み合わせは、自動運転・ロボティクス・建設・XR など、これから伸びる分野で強い武器になる

「式を読むだけ」でもなく、「ライブラリを黒箱のまま使うだけ」でもない、
その中間にある “コードと数式を行き来できるエンジニア” を目指すうえで、
ここで紹介する2冊は、かなり心強い伴走役になってくれるはずです。


参考書籍

データサイエンス数学 × Python

3次元点群処理 × Python

ゲームプログラミング入門

C++ゲームプログラミング入門

――ゲームループからデータ構造、WebAssemblyまで

1. ゲームプログラムは「1本のループ」から始まる

ゲームプログラミングをこれから学ぶとき、まず押さえておきたいのは、

ゲームの本質は「ループ」である

ということ。

どんなジャンルのゲームでも、内部ではだいたい次のような処理が、画面の裏側で延々と繰り返されている。

  1. 入力を読む(キーボード / マウス / コントローラ / ネットワーク)
  2. ゲーム世界の状態を更新する(物理 / AI / スクリプト)
  3. 画面に描画する(2D / 3D / UI)

この3ステップを 1 秒間に 30 回〜 60 回以上回し続ける。
これが「ゲームループ」と呼ばれるものだ。

C++ で書くと、極端に単純化すれば次のようなイメージになる。

while (isRunning) {
    processInput();  // 1. 入力
    update();        // 2. 状態更新
    render();        // 3. 描画
}

このたった数行のループを、いかに破綻させず、いかに高速に、いかに保守しやすく構築するか。
ゲームプログラミングの多くは、この一点に集約されていると言ってもいい。


2. C++でゲームを書く理由と向き合い方

「ゲームと言えば C++」という印象を持っている人も多いと思う。
実際、大手ゲームエンジン(Unreal Engine など)や、家庭用ゲーム機向けタイトルの多くは C++ をベースにしている。

理由はいくつかある。

  • ネイティブコードで動き、高速・低レイテンシを出しやすい
  • メモリレイアウトを制御しやすく、CPU/GPU をぎりぎりまで使える
  • 既存のゲームエンジン・ミドルウェアが C++ 前提で設計されている

一方で、C++ は

  • 言語仕様が広く深い
  • メモリ管理の責任がプログラマ側にある
  • 設計を間違えるとコードベースが一瞬で「沼」になる

という側面もある。
ここで重要になるのが「基本」と「設計」だ。

  • ゲームループ・コンポーネント・イベント駆動といったゲーム特有の基本
  • 抽象化・責務分離・依存関係の整理といったコーディングと設計の基本

この2つを同時に身につけていくと、C++ ゲームプログラミングの学習効率は一気に上がる。


3. ゲームループの設計:フレームと時間の扱い

ゲームループを組むときに最初にぶつかる壁が、「時間の扱い」だ。

フレームレートが一定とは限らない環境で、どうやってゲームの動きを安定させるか。
基本的な考え方は次の2つがある。

3.1 デルタタイム方式(可変フレームレート)

1フレームごとに「前のフレームから何秒経ったか(Δt)」を測り、物理やアニメーションを Δt に応じて進める方式。

double previousTime = now();

while (isRunning) {
    double current = now();
    double dt = current - previousTime;
    previousTime = current;

    processInput();
    update(dt);  // dt秒ぶん進める
    render();
}

メリット: - 環境に関係なく時間ベースで進むので、60fps でも 144fps でもゲームの進み具合が揃う

デメリット: - dt が小さすぎたり大きすぎたりすると、物理計算の精度や安定性が崩れる - 固定ステップ前提のライブラリと組み合わせにくい

3.2 固定タイムステップ方式(Fixed Timestep)

「物理は常に 1/60 秒ごとに進める」と決めてしまい、フレームごとに複数回 update したり、逆にスキップしたりする方式。

const double dtFixed = 1.0 / 60.0;
double accumulator = 0.0;
double previousTime = now();

while (isRunning) {
    double current = now();
    double frameTime = current - previousTime;
    previousTime = current;
    accumulator += frameTime;

    processInput();

    while (accumulator >= dtFixed) {
        update(dtFixed);
        accumulator -= dtFixed;
    }

    render();
}

メリット: - 物理やロジックを「固定間隔」で進められるため、安定しやすい - 携帯機・家庭用ゲーム機の古典的なパターンと相性が良い

デメリット: - コードがやや複雑になる - CPU が弱い環境では update が追いつかず、「スローモーション」になることもある

ゲームによって、あるいはゲームの一部分によって、この2つを使い分けたり、ハイブリッドにしたりする。


4. オブジェクト設計:エンティティとコンポーネント

画面に表示されるものは、プレイヤーも敵も弾もエフェクトも、全部「オブジェクト」だ。
これを C++ でどう表現するかは、ゲームプログラミングの定番テーマのひとつになっている。

4.1 典型的なクラスの分解

例えば、シンプルな 2D シューティングゲームを考えると、こんなクラス分割がありえる。

  • Game:ゲーム全体を管理(ループ・シーン切り替えなど)
  • Actor:プレイヤー・敵・弾などの共通基底
  • SpriteComponent:スプライト描画担当
  • InputComponent:入力に応じて Actor を動かす
  • MoveComponent:速度・加速度の更新を行う
  • CollisionComponent:当たり判定

ざっくり C++ のコードでイメージすると、次のようになる。

class Component {
public:
    virtual ~Component() = default;
    virtual void update(float dt) {}
};

class Actor {
public:
    void update(float dt) {
        for (auto& comp : components) {
            comp->update(dt);
        }
        // Actor自身のロジック...
    }

    template <typename T, typename... Args>
    T* addComponent(Args&&... args) {
        auto ptr = std::make_unique<T>(std::forward<Args>(args)...);
        auto raw = ptr.get();
        components.emplace_back(std::move(ptr));
        return raw;
    }

private:
    std::vector<std::unique_ptr<Component>> components;
};

class MoveComponent : public Component {
public:
    void update(float dt) override {
        // ここで Actor の位置を更新する…
    }
};

この「Actor + Component」という設計は、ゲームエンジンでよく使われるパターンだ。

一方で、コンポーネント数が増えすぎると、

  • 関係が分かりにくい
  • 実行時ポリモーフィズムのオーバーヘッド
  • キャッシュの局所性が悪化

といった問題も出てくる。そこをどうバランスするかが、「設計力」の腕の見せ所になる。

4.2 データ指向設計と ECS の話

より大規模なゲームや、CPUキャッシュ効率を極端に追い込むような現場では、

  • Entity Component System(ECS)
  • データ指向設計(Data-Oriented Design, DOD)

といった設計思想が登場する。

  • オブジェクトごとではなく、「ポジションを持つ全エンティティ」「速度を持つ全エンティティ」のようにデータ列をまとめる
  • ひとつのシステムが、対応するコンポーネント配列を一気に処理してループを回す

ことで、メモリアクセスを連続化し、CPU のパイプラインやキャッシュに優しい形にする。

たとえば、次のような雰囲気になる。

struct Position { float x, y; };
struct Velocity { float vx, vy; };

std::vector<Position> positions;
std::vector<Velocity> velocities;

void updateVelocity(float dt) {
    for (size_t i = 0; i < positions.size(); ++i) {
        positions[i].x += velocities[i].vx * dt;
        positions[i].y += velocities[i].vy * dt;
    }
}

「オブジェクト指向 vs データ指向」はどちらか一方を選ぶ話ではなく、

  • 中規模までは「素直な OOP + コンポーネント」
  • パフォーマンスが気になるホットパスだけ「データ指向」

といったハイブリッドも多い。


5. アルゴリズムとデータ構造:ゲームは「リアルタイムな大規模データ処理」

ゲームプログラムは、見た目にはグラフィックとサウンドの世界だが、中身はかなりストイックなデータ処理だ。

  • 物理シミュレーション:空間分割(QuadTree / Octree / BVH)、衝突判定
  • AI:グラフ探索(BFS / A*)、経路探索、ビヘイビアツリー
  • レンダリング:ソート(前後関係)、バッチング、LODの選択
  • オンラインゲーム:イベントログ、マッチング、ランキング

こうした処理には、アルゴリズムとデータ構造の知識がそのまま効いてくる。

5.1 空間分割と衝突判定

例えば 2D シューティングで、敵と弾の当たり判定を考えてみる。

  • 敵 100 体
  • 弾 200 発

がいたとすると、全ペアの衝突判定は 100 × 200 = 20,000 通り。
これはまだ可愛らしい数字だが、3D の広い世界で何千ものオブジェクトが動き回ると、
ナイーブな全探索はすぐ破綻する。

そこで使われるのが、空間分割のアルゴリズムだ。

  • グリッド分割:空間を等間隔のセルに分け、オブジェクトをセルに登録
  • QuadTree / Octree:再帰的に空間を分割して密度に応じて管理
  • BVH(Bounding Volume Hierarchy):包囲体の階層を構築して粗い当たり判定を高速化

これらは「大規模データセットのためのアルゴリズム」とほぼ同じ世界であり、
ゲームでもバックエンドでも知っておくと強い武器になる。

5.2 確率的データ構造とゲーム

ゲームサーバ側の話になるが、

  • プレイヤー数百万人分のログ
  • 圧倒的なイベント数(ログイン・戦闘・課金 etc.)
  • 不正検知・バランス調整のための統計・分析

といった世界では、メモリにすべて載せるのは現実的ではない。

そこで登場するのが、確率的データ構造だ。

  • Bloom Filter:集合に含まれているかどうかを高速にチェック(誤判定許容)
  • Count–Min Sketch:頻度統計を省メモリで近似
  • HyperLogLog:ユニーク要素数を効率的に推定

こうした構造は、オンラインゲームのバックエンドや分析基盤で使われることが多い。
「ゲームクライアントのC++」とは少し距離があるが、
ゲーム全体を「サービス」として捉えると、サーバサイドのアルゴリズム知識も重要になってくる。


6. ブラウザで動くゲーム:Rust × WebAssembly という選択肢

近年、「ブラウザゲーム」と言うと JavaScript / TypeScript だけではなく、
Rust + WebAssembly で書かれたゲームも増えている。

  • Rust でロジックと描画処理を書く
  • WebAssembly にコンパイルしてブラウザで実行
  • ブラウザ側とは最低限のインターフェースだけ JS でつなぐ

といった構成だ。

6.1 C++ ゲームプログラマが Rust/Wasm から学べること

C++ をメインにしていても、Rust + Wasm には学べる点が多い。

  • メモリ安全性:所有権・借用の仕組みで、バグの多い領域をコンパイル時に縛る
  • 非同期処理:Future や async/await を使ったイベント駆動
  • Web プラットフォーム:ブラウザ・DOM・WebAudio との連携の考え方

C++ で書いたゲームロジックを、将来的に Web に持っていきたい場合、

  • コアロジックを「非プラットフォーム依存」に設計しておく
  • プラットフォーム依存の部分(描画 / 入力 / 音声)は抽象化して差し替え可能にする

といった工夫が効いてくる。
Rust + Wasm の本を読むと、「ゲームのコアとプラットフォーム依存部分をきれいに分ける」設計センスが磨かれる。


7. 学習ロードマップの一例

ゲームプログラミングをこれから本格的にやっていく場合、ざっくり次のようなステップが考えられる。

  1. C++ の基礎文法と標準ライブラリ

    • 変数・関数・クラス・テンプレート・スマートポインタ
    • std::vector, std::array, std::unique_ptr など
  2. 簡単な 2D ゲームの実装

    • SDL2 や SFML といったライブラリを使って、
      • ゲームループ
      • 入力処理
      • 簡単な当たり判定
      • スプライトアニメーション
        をひと通り体験する
  3. オブジェクト設計とリファクタリング

    • Actor/Component パターン
    • 責務分離・抽象化・依存関係の整理
    • テストしやすいコードへの書き換え
  4. アルゴリズム・データ構造・パフォーマンス

    • リスト / 配列 / マップなどの基礎
    • 空間分割・グラフ探索・確率的データ構造のさわり
    • プロファイリングとマイクロベンチマーク
  5. エンジンやバックエンドへの拡張

    • 既存のゲームエンジン(Unity / Unreal / Godot)の内部構造を覗いてみる
    • オンラインゲームのサーバサイド(API / DB / 分散処理)に触れてみる
    • Rust + Wasm でブラウザ側にゲームを持っていく実験をする

本や記事で理論を学びつつ、「小さいゲームを実際に作る」ことを繰り返すのが一番身につきやすい。


8. まとめ

ゲームプログラミングは、「ループを回しているだけ」と言ってしまえばそれまでだが、そのループの中には膨大な要素が詰まっている。

  • 時間の扱い(デルタタイム / 固定ステップ)
  • オブジェクト設計(Actor/Component, ECS)
  • アルゴリズムとデータ構造(空間分割・グラフ探索・確率的構造)
  • プラットフォームの違い(ネイティブ / ブラウザ / サーバ)
  • 設計とコーディングスタイル(読みやすさとパフォーマンスの両立)

C++ はそのすべてを、かなり低いレベルから高いレベルまで触らせてくれる言語だ。

まで視野に入る。

最初は「ゲームループ + 矢印キーで動くキャラクター」くらいの小さなところからでいい。
そこに少しずつ、物理・AI・ネットワーク・データ構造・設計テクニックを足していくことで、
ゲームプログラミングの世界は一気に立体的になっていく。


9. 参考書籍

C++でゲームを作る「基本」と「実装」を学ぶ

  • ゲームプログラミングC++
    C++ を使って、シンプルな 2D ゲームから 3D・サウンド・AI までを一通り実装していく教科書。ゲームループや Actor/Component など、本記事で触れたような構造も丁寧に解説されている。
    ゲームプログラミングC++

ゲーム開発者としての「設計力」と「コーディング力」を鍛える

大規模データとアルゴリズムのセンスを身につける

Rust + WebAssembly でブラウザゲームに挑戦する

  • RustとWebAssemblyによるゲーム開発
    Rust で書いたゲームロジックを WebAssembly にコンパイルし、ブラウザ上で動かす方法を、実際のミニゲーム開発を通して学べる本。C++ メインの人でも、「安全性の高いシステム言語で Web とどう付き合うか」のヒントが得られる。
    RustとWebAssemblyによるゲーム開発

これらの本を行き来しながら、自分の手で小さなゲームを作り、少しずつ規模を大きくしていく。
それが、ゲームプログラミングを長く楽しみながらスキルを磨いていく一番の近道だと思う。

C++ × CPU マイクロアーキテクチャで見るメモリアクセス性能

C++ × CPU マイクロアーキテクチャで見るメモリアクセス性能

はじめに

「同じメモリを何度も読むだけなんだから、速いはずだろう」

最初にこのベンチマークを書いたとき、自分もそう思っていました。
しかし実際に C++ で計測してみると、同じアドレスを延々と Read/Write するコードが、アドレスを毎回変えるコードよりも桁違いに遅い、という直感に反した結果が出ます。

この挙動は「キャッシュに乗る / 乗らない」といった単純な話ではありません。
CPU 内部の

  • Store Buffer(ストアバッファ)
  • Load Queue(ロードキュー)
  • Store-to-Load Forwarding(ストア→ロード転送)
  • Out-of-Order Execution(アウト・オブ・オーダ実行)

といったマイクロアーキテクチャ機構が、メモリアクセスの「アドレスの性質」に強く反応している結果です。

この記事では、以下を狙いどころにします。

  • C++ による小さなベンチマークで「同一アドレス vs 異なるアドレス」を実測する
  • CPU マイクロアーキテクチャの観点から、それぞれで何が起きているのかを整理する
  • 実際のコード最適化で何に気をつけると効くのか、ヒントをまとめる
  • 最後に、理解を深めるための日本語の書籍をいくつか紹介する

1. 2つのアクセスパターン

今回扱うのは、あえて極端に単純化した2パターンです。

  1. 同一アドレスへ連続アクセス

    • 例:同じ int value; に対して value += 1; をひたすら繰り返す
    • Load と Store が常に同じアドレスを指す
  2. 異なるアドレスへ順次アクセス

    • 例:array[i] += 1; を i = 0,1,2,... と変えながら実行
    • Load / Store は毎回別のキャッシュライン上(あるいは少なくとも別のアドレス)を触る

キャッシュに全部乗るサイズであっても、両者の性能差は驚くほど大きくなります。
この差は、CPU 内部で行われる メモリ依存性の処理 の違いから生じます。


2. 計測環境と結果のざっくり概要

2-1. 計測環境(例)

  • マシン:Intel MacBook Air(x86_64、2 コア 4 スレッド程度のモバイル向け CPU)
  • コンパイラ:Clang / Apple Clang
  • ビルドオプション:-O3 -march=native
  • 計測方法:rdtsc() によるサイクル数の直接計測

rdtsc による測定は Intel が公開している最適化マニュアルでもよく登場する古典的アプローチで、
「あるループの前後で TSC(Time Stamp Counter)を読み、その差を取る」スタイルです。

2-2. 結果の印象

同一条件で 測定したところ、以下のような傾向になりました(あくまで一例です)。

Run毎のメモリアクセス速度

  • 同一アドレスへの Read/Write

    • 1 回あたりの平均サイクル数が 異なるアドレスの 10 倍以上
    • バラツキも少なく、「ずっと遅い」感じの分布
  • 異なるアドレスへの Read/Write

    • 平均サイクル数はかなり小さく、分布も詰まっている
    • CPU が内部でうまく並列化してくれている印象

具体的な数値は CPU 世代やコンパイラによって変わりますが、
「同一アドレスは極端に遅く、異なるアドレスはかなり速い」という傾向自体は、多くの x86 系 CPU で再現されます。


3. C++ ベンチマークコード

ここでは、「同一アドレス版」と「異なるアドレス版」の両方を C++17 相当で書いた例を示します。

3-1. 同一アドレスに対して加算を繰り返す

#include <cstdint>
#include <cstdio>
#include <x86intrin.h>

constexpr std::size_t N = 1'000'000;

inline std::uint64_t rdtsc() noexcept {
    return __rdtsc();
}

std::uint64_t bench_same_address() {
    volatile std::uint64_t value = 0; // 同一アドレスを保証するために volatile
    const auto start = rdtsc();
    for (std::size_t i = 0; i < N; ++i) {
        value += 1;
    }
    const auto end = rdtsc();
    return end - start;
}

int main() {
    constexpr int trials = 50;
    for (int t = 0; t < trials; ++t) {
        const auto cycles = bench_same_address();
        std::printf("same,%d,%llu\n", t, (unsigned long long)cycles);
    }
}

ポイントは以下の通りです。

  • volatile を付けて、コンパイラ最適化でループごと消されないようにする
  • __rdtsc() を使って TSC を読み取る(GCC/Clang なら <x86intrin.h> で利用可)
  • 後で Python から読みやすいように、CSV ライクな形式で出力する

3-2. 異なるアドレスを順番に触る場合

#include <cstdint>
#include <cstdio>
#include <vector>
#include <x86intrin.h>

constexpr std::size_t N = 1'000'000;

inline std::uint64_t rdtsc() noexcept {
    return __rdtsc();
}

std::uint64_t bench_diff_address() {
    static std::vector<std::uint64_t> buf(N, 0);
    volatile std::uint64_t *p = buf.data(); // ベースアドレス
    const auto start = rdtsc();
    for (std::size_t i = 0; i < N; ++i) {
        p[i] += 1; // アドレスが毎回変わる
    }
    const auto end = rdtsc();
    return end - start;
}

int main() {
    constexpr int trials = 50;
    for (int t = 0; t < trials; ++t) {
        const auto cycles = bench_diff_address();
        std::printf("diff,%d,%llu\n", t, (unsigned long long)cycles);
    }
}

buf は L1/L2 キャッシュに十分収まるサイズにしておくと、
「メインメモリが遅いせいではなく、CPU 内部の処理の違い」を見やすくなります。

3-3. 測定上の落とし穴

マイクロベンチマークでやりがちな事故も簡単に触れておきます。

  • 最適化でループが消える
  • ループ展開やベクトル化によって、意図していない命令列になる
  • rdtsc の前後の命令がリオーダされる(lfence などでシリアライズするのが定石)
  • 電源管理(ターボブースト、周波数スケーリング)でサイクルあたりの実時間が変動する
  • OS スケジューラで別コアに飛ばされる

本記事では「傾向」を見るのが目的なので、細かいチューニングは省略しますが、
ガチで数サイクル単位を詰めたい場合は Intel の最適化マニュアルや Agner Fog の文書をチェックするのがおすすめです。

アドレス毎のメモリアクセスサイクル

4. CPU マイクロアーキテクチャのざっくり復習

では、なぜ同一アドレスと異なるアドレスでこんなに差が出るのか。
ここからは x86 系 CPU のマイクロアーキテクチャを簡単におさらいします。

4-1. パイプライン+アウト・オブ・オーダ実行

現代の CPU は以下のような流れで命令を処理します。

  1. 命令フェッチ・デコード
  2. マイクロオペレーション(uop)への分解
  3. 依存関係解析・レジスタリネーミング
  4. 実行ユニット(ALU, Load/Store Unit など)への発行
  5. 実行・結果の一時保持
  6. コミット(リオーダバッファからリタイア)

「アウト・オブ・オーダ実行」は、依存関係がない命令同士であれば順番を入れ替えて同時並行に実行する仕組みです。
これにより、メモリアクセスの待ち時間を隠しながら ALU を休ませないように動いてくれます。

4-2. ロード / ストアまわりの構造

メモリアクセスの周辺には、ざっくり以下のような構造があります。

  • L1 データキャッシュ
    • 数十〜数百サイクルかかるメインメモリよりはるかに速い、1次キャッシュ
  • ロードバッファ(Load Queue)
    • 飛び出していった Load 命令の情報を溜めておく場所
  • ストアバッファ(Store Buffer / Store Queue)
    • Store 命令で書き込む値を、キャッシュに反映するまで一時的に保持しておく場所
  • Store-to-Load Forwarding
    • 「直前の Store が書いた値を、L1キャッシュではなくストアバッファから直接読み出す」仕組み

ここで重要なのは、ストアバッファとロードキューの間で「アドレスの一致を監視している」という点です。
Load が発行されたとき、

  • 直前の Store 群の中に「同じアドレス(あるいは重なるアドレス)」がないかを全部チェックする
  • あれば、その Store の値を Load にフォワードする
  • 条件が合わない/サイズが違う/アライメントがずれているとペナルティが発生し、ロードがリトライになったりパイプラインがストールする

この「アドレスの突き合わせ」が、同一アドレス連打のときに毎回必ず発生するのが本記事の主役になります。


5. 同一アドレス連打が極端に遅くなる理由

さて、value += 1; をひたすら回すループで、CPU 内部では何が起きているのか。

5-1. Store → Load の依存が毎回発生する

以下のような簡略化した命令列を考えます。

; value += 1; をイメージした擬似コード
load  rax, [addr]   ; メモリからレジスタへ
add   rax, 1
store [addr], rax   ; レジスタからメモリへ

これを N 回繰り返す場合、各イテレーションの load は 直前の store が書いた値に依存しています。

CPU は「最新の値は L1 にまだ書き込まれておらず、ストアバッファにあるかもしれない」と判断し、
毎回ストアバッファを検索 → 対応するエントリを見つけて値をフォワードします。

この処理自体もタダではありません。

  • ストアバッファは「対応するアドレスがないか」を全エントリに対して(ほぼ)同時に検索する必要がある(CAM 的な構造)
  • ヒットすると、フォワードされたデータが Load の結果となる
  • 条件が合わなかった場合はフォワード失敗(ペナルティが乗る)

同一アドレス連打の場合、この依存チェックがループのたびに必ず走り、かつ毎回「依存あり」と判定されます。
その結果、Load の実行は Store 完了と強く結びつき、パイプラインはほぼシリアルに近い形で進むことになります。

5-2. Out-of-Order 実行が効かなくなる

Out-of-Order 実行は「依存のない命令を前後に動かす」ことで性能を稼ぎます。
しかし、同一アドレス連打のループでは、

  • load は直前の store と真の依存を持つ(データ依存)
  • add は load に依存し、store は add に依存する

という「がっちり縦に繋がった」依存チェーンができてしまうため、
メモリアクセスを前後にずらして並行実行する余地がほとんどありません。

CPU 的には、

  • 1 回のループをこなす間、他の似た Load/Store を重ねづらい
  • 依存チェックとフォワードを毎回行う必要がある
  • ストアバッファ・ロードキューのエントリも再利用されにくい

といった事情が積み重なって、見かけ上「1回のメモリアクセスに対するレイテンシ」が大きくなってしまうのです。


6. 異なるアドレスが速い理由

対照的に、array[i] += 1; のように アドレスが毎回異なる場合を見てみます。

6-1. 依存関係が“縦”から“横”に広がる

アドレスが十分に離れている状況では、

  • array[i] += 1; と array[i+1] += 1; の間に依存はない
  • 各 Load は「直前の Store が書いたアドレス」とは異なる場所を読みに行く
  • ストアバッファを検索してもヒットしないので、フォワードは発生しない(依存なし)

つまり、CPU から見れば「独立した小さなループ本体」がずらっと並んでいるように見えます。

その結果、アウト・オブ・オーダ実行が効き始めます。

  • 先に発行した Load がメモリ待ちの間に、後続の Load もどんどん発行してしまう
  • Store もストアバッファに溜め込んでおき、キャッシュへの書き戻しを後回しにできる
  • ロードバッファ・ストアバッファに複数の命令が同時に滞在し、メモリレベル並列性(MLP)が高まる

この「複数のメモリアクセスを同時に飛ばしておいて、どれかが返ってきたらすぐ次を実行する」動きが、
平均レイテンシを大幅に隠してくれるわけです。

6-2. キャッシュの局所性も効いてくる

さらに、array[i] を順番に触る場合は

  • 同じキャッシュライン内の要素を連続して読む
  • ハードウェアプリフェッチャがアクセスパターンを検出し、次のキャッシュラインを先読みする

といった恩恵も受けられます。

このため、「キャッシュに全部乗っているのに速い」ではなく、
「キャッシュに全部乗っているからこそ、CPU が全力で並列化できる」と言う方が正確です。


7. 実測結果のグラフの読み方

50回分のサイクル数を CSV に吐き出し、Python で可視化すると違いは一目瞭然になります。

  • 折れ線グラフ(plot_line.png)

    • same(同一アドレス)は上の方でベタっと張り付いた線
    • diff(異なるアドレス)はその 1/10 以下の位置で安定
  • 箱ひげ図(plot_box.png)

    • 中央値も最小値も same が diff より桁違いに大きい
    • 外れ値も少なく、「ずっと遅い」ことが分かる

ここで重要なのは、「単に同一アドレスが遅い」ではなく

  • CPU に“依存あり”と認識されるかどうかで、マイクロアーキテクチャの動きがガラッと変わる
  • その差が、そのまま計測値の差として現れている

という点です。


8. 実務コードにどう効いてくるか

ここまでの話を、普段書く C++ コードに引き寄せてみます。

8-1. 「同じ場所を叩き続ける」コードに注意

例として、以下のようなコードを考えます。

// 例1: グローバルカウンタをひたすらインクリメント
for (int i = 0; i < n; ++i) {
    ++global_counter;
}

// 例2: スレッドセーフなカウンタを spinlock で守る
for (int i = 0; i < n; ++i) {
    lock(mtx);
    ++counter;
    unlock(mtx);
}

どちらも「同じアドレス(counter)を何度も書き換える」典型的なパターンです。

  • キャッシュラインの競合(false sharing)
  • Store Buffer の圧迫
  • Store-to-Load Forwarding まわりの依存

などが重なって、ボトルネックになりやすい部分になります。

これを避けるために、

  • スレッドごとにローカルカウンタを持って最後に集計する
  • ストライド付きの配列に分散して書き込む
  • std::atomic でもフェッチ・アンド・アッドを乱用しない

といった「アドレスをバラして書く」設計をすることで、CPU が持つ並列実行能力を活かしやすくなります。

8-2. 構造体のレイアウトと「偽の依存」

構造体やクラスのメンバを更新するときも、同じキャッシュライン内で頻繁に読んだり書いたりするフィールドが固まっていると、
内部的には「同じアドレス周辺への Store/Load 連打」になります。

  • 読み取り専用のフィールド
  • 頻繁に更新されるフィールド

を分けて別の構造体にしたり、
キャッシュライン境界を意識した配置(alignas(64) など)にすることで、
無駄な依存やキャッシュライン競合を避けることができます。


9. 小さな C++ ベンチでマイクロアーキテクチャを“見る”

まとめとして、本記事のポイントを整理します。

  • 同一アドレスへの連続アクセスが遅い理由

    • Store Buffer 上のデータを参照するため、Store→Load の依存性チェックが毎回必要
    • Store-to-Load Forwarding が常に絡み、パイプラインがシリアライズされやすい
    • アウト・オブ・オーダ実行で並列化できる余地がほとんどない
  • 異なるアドレスが速い理由

    • アドレスの依存が薄いため、Load/Store を大量に飛ばしてメモリレベル並列性を稼げる
    • ハードウェアプリフェッチャやキャッシュ局所性が効きやすく、平均レイテンシを隠せる
  • C++ でのマイクロベンチ

    • rdtsc によるサイクル計測で、CPU の内部挙動を「外から」観察できる
    • コンパイラ最適化や CPU の世代によって数値は変化するが、「傾向」はかなり再現性がある
  • 実務的な示唆

    • 「同じアドレスを叩き続ける」コードは、CPU にとって扱いづらいパターンになりやすい
    • アクセスパターンを設計する段階で、メモリ依存性と局所性を意識しておくと、後からの最適化コストが下がる

最終的には、

「CPU がどう動いているか」を少しだけ意識しながら C++ のコードを書く

だけでも、パフォーマンスの“地雷”を踏む頻度はかなり減らせます。
その感覚を掴むための入り口として、こうした小さなベンチマークはかなりおすすめです。


付録:Python による可視化コード(例)

最後に、C++ 側で吐き出した CSV(pattern,trial,cycles 形式)を可視化する Python スクリプトの例を載せておきます。

import pandas as pd
import matplotlib.pyplot as plt

df = pd.read_csv("result.csv", header=None, names=["pattern", "trial", "cycles"])

# 1回あたりの平均サイクル数(ループ回数で割る)
N = 1_000_000
df["cycles_per_op"] = df["cycles"] / N

# 折れ線グラフ
plt.figure()
for pattern, sub in df.groupby("pattern"):
    plt.plot(sub["trial"], sub["cycles_per_op"], marker="o", label=pattern)
plt.xlabel("trial")
plt.ylabel("cycles per op")
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.savefig("plot_line.png", dpi=200)

# 箱ひげ図
plt.figure()
df.boxplot(column="cycles_per_op", by="pattern")
plt.ylabel("cycles per op")
plt.tight_layout()
plt.savefig("plot_box.png", dpi=200)

参考文献・参考図書

CPU / マイクロアーキテクチャ

  • IT Text コンピュータアーキテクチャ 改訂2版
    命令セット、メモリアーキテクチャ、パイプライン、命令レベル並列などを大学教科書としてコンパクトにまとめた本。3〜7章あたりが本記事の背景理解に直結する。

  • 動かしてわかる CPUの作り方10講
    実際に動く簡易 CPU を作りながら仕組みを理解していくタイプの本。メモリアクセスの流れやデータパスを“手触り”で理解したい人におすすめ。

  • コンピュータの構成と設計 いわゆる定番テキスト。パイプラインやメモリ階層、並列処理などを体系的に学べる。メモリ依存性やアウト・オブ・オーダ実行についても基礎から押さえられる。

C++ と性能最適化

オンライン資料(英語)

  • Intel『Intel 64 and IA-32 Architectures Optimization Reference Manual』
    Intel 64 / IA-32 向け最適化の公式バイブル。メモリ階層、ロード / ストアユニット、Store Buffer、Store-to-Load Forwarding など、本記事で触れたテーマの詳細な解説がまとまっている。

  • easyperf.net「Store forwarding by example」
    x86 の Store-to-Load Forwarding を具体例付きで解説したブログ記事。同一アドレス・サイズ・アライメントの条件や、失敗時のペナルティについて非常に分かりやすい。

これらの書籍・資料をあわせて読むことで、
「C++ のコード」「CPU マイクロアーキテクチャ」「実測ベンチ」の三層を行き来しながら、
より腰の据わったパフォーマンス改善ができるようになるはずです。