河豚板の作り方

Table of Contents

numpy

オイラーの悪い癖で、前回の記事の続きが必ず出てくる。そして、記事の後半では、 それと余り関係無い事が出てくる。やり残しの始末と新らしい事ってんで、記事に しまりが無くなるのは、許せ。

そんな事なんで、numpyの続きです。

Q: 平均血圧の計算 map_bp = (sbp - dbp) / 3 + dbp この式に興味を覚えました。これってnumpyの説明に出てくるブロードキャスティングの 応用なんでしょうか?

A: 結論からいうと これはブロードキャスティングではなく、「NumPy の要素ごとの配列演算」+「新しい配列の自動生成」 です。


sbpdbp も NumPy の 1 次元配列(ndarray)です。

NumPy では 配列同士の四則演算は要素ごと (element-wise) に実行されます。

例えば:

sbp - dbp

は内部的に

[ sbp[0]-dbp[0],  sbp[1]-dbp[1],  sbp[2]-dbp[2], ... ]

を計算して 新しい ndarray を返す 仕組みです。


これがポイントで、NumPy の演算子は

  • 結果を格納する配列を明示的に指定しなくても
  • 毎回、新しい配列を確保して結果を返す

ように設計されています。

したがってこの式:

map_bp = (sbp - dbp) / 3 + dbp

は内部的には以下に分解できます:

  1. tmp1 = sbp - dbp → 新しい配列A
  2. tmp2 = tmp1 / 3 → 新しい配列B
  3. map_bp = tmp2 + dbp → 新しい配列C が最終結果

map_bp はこの新しい配列 C を受け取っているだけです。


今回の式では /起きていません/。

理由:

  • sbp, dbp は同じ形状 (N,)
  • ブロードキャストの必要なし(1 対 1 の要素演算)

### ブロードキャストが起きる例

sbp + 10

ここで 10 はスカラー(0 次元配列)なので、NumPy はそれを

[10, 10, 10, ...]

と「仮想的に」広げて計算します。 ※実際にはコピーせず、1 回だけの値を使って賢く計算します。


現象 今回該当? 説明
--------------- ----- ---------------
要素ごとの配列演算 同じ形状の配列同士の四則演算
新しい配列の自動生成 明示的な出力配列指定なしで生成
ブロードキャスティング 配列形状が同じなので起きない

必要なら、メモリ効率を上げるために「中間配列を作らずに計算する in-place 版」も書けます。 興味ありますか?

import numpy as np
import time

N = 1_000_000
sbp = np.random.randint(100, 150, N)
dbp = np.random.randint(50, 100, N)

# ================================
# 通常版
# ================================
t0 = time.time()
map1 = (sbp - dbp) / 3 + dbp
t1 = time.time()

# ================================
# in-place 版
# ================================
map2 = np.empty_like(sbp, dtype='f8')

t2 = time.time()
np.subtract(sbp, dbp, out=map2)
map2 /= 3
map2 += dbp
t3 = time.time()

print("通常版   :", t1 - t0)
print("in-place :", t3 - t2)
sakae@lu:tmp$ python3 cmp.py
通常版   : 0.007875919342041016
in-place : 0.005400180816650391

参考までに、numpyを使用しない、いわゆる、ふつpy版もやっておく。

import time
import random

N = 1_000_000
sbp = [random.uniform(100, 150) for _ in range(N)]
dbp = [random.uniform(50, 90)  for _ in range(N)]

start = time.time()
map_bp = [(s - d) / 3.0 + d for s, d in zip(sbp, dbp)]
print("Python pure loop:", time.time() - start)
sakae@lu:tmp$ python3 pure.py
Python pure loop: 0.09894967079162598

numpyは偉大だなあ!

vs. chez scheme

Q: schemeのmapですよね。

(define map-bp
  (map (lambda (h l)
         (let ((diff (- h l)))
           (+ (/ diff 3) l)))
       sbp dbp))

A: 最速のchez schemeで実験しようと思ったら、 Ubuntu版は古く、ベクターの扱いが不透明(楽しようとすると、躓く典型例)だった。 雁首そろえてチャッピー君と、アーでもない、コーでもないと袋小路にはいって しまった。自前でコンパイルした方が確実だよねって結論になったよ。

(https://github.com/cisco/ChezScheme)にソースが有るぞ。

;; --- utilities: list <-> flvector --------------------------------
(optimize-level 3) ;; for speed up
;; list の数値要素を inexact (flonum) にして flvector に詰める
(define (list->flvector lst)
  (let* ((n (length lst))
         (fv (make-flvector n 0.0)))
    (let loop ((i 0) (xs lst))
      (if (null? xs)
          fv
          (begin
            (flvector-set! fv i (exact->inexact (car xs)))
            (loop (+ i 1) (cdr xs)))))))

;; flvector を普通の list に戻す(デバッグ用)
(define (flvector->list fv)
  (let loop ((i 0) (n (flvector-length fv)) (acc '()))
    (if (= i n)
        (reverse acc)
        (loop (+ i 1) n (cons (flvector-ref fv i) acc)))))

;; --- MAP 計算(in-place スタイル。結果は新しい flvector を返す) ---
;; MAP = (SBP - DBP) / 3 + DBP
(define (compute-map-flvector sbp-fl dbp-fl)
  (let ((n (flvector-length sbp-fl)))
    (when (not (= n (flvector-length dbp-fl)))
      (error 'compute-map-flvector "length mismatch"))
    (let ((map-fl (make-flvector n 0.0)))
      (let loop ((i 0))
        (if (= i n)
            map-fl
            (begin
              (let ((h (flvector-ref sbp-fl i))
                    (l (flvector-ref dbp-fl i)))
                ;; 計算はすべて flonum(inexact)で行う(3.0 を使う)
                (flvector-set! map-fl i (+ l (/ (- h l) 3.0))))
              (loop (+ i 1))))))))

;; --- 簡易ベンチマーク用関数 -----------------------------------------
(define (time-thunk thunk)
  (let ((t0 (current-time)))
    (let ((res (thunk)))
      (let ((dt (time-difference (current-time) t0)))
        (values dt res)))))

;; 乱数 flvector を作るヘルパー(0 <= val < upper)
(define (rand-flvector n upper)
  (let ((fv (make-flvector n 0.0)))
    (let loop ((i 0))
      (if (= i n)
          fv
          (begin
            ;; random returns in [0,1). floor を用いて整数化
            (flvector-set! fv i (exact->inexact (floor (* (random 1.0) upper))))
            (loop (+ i 1)))))))

;; --- 実行例(ベンチマーク) ---------------------------------------
;; 注意: 実行環境やコンパイラの最適化フラグで結果は変わります。
(let* ((N 1000000)                    ; 要素数
       (sbp (rand-flvector N 60))      ; 例: 0..59 を乱数とする(調整可)
       (dbp (rand-flvector N 40)))     ; 例: 0..39
  (display "warming up ...") (newline)
  ;; JIT/コンパイラ最適化を期待して一回実行(ウォームアップ)
  (compute-map-flvector sbp dbp)

  (newline)
  (let-values (((dt1 res1) (time-thunk (lambda () (compute-map-flvector sbp dbp)))))
    (display "compute-map-flvector elapsed: ") (display dt1) (newline)
    ;; 結果の最初の要素だけ表示
    (display "first results: ") (display (car (flvector->list res1))) (newline)))
sakae@lu:tmp$ scheme flvec.ss
Chez Scheme Version 10.4.0-pre-release.1
Copyright 1984-2025 Cisco Systems, Inc.

warming up ...

compute-map-flvector elapsed: #<time-duration 0.025397236>
first results: 21.666666666666668

Chez Schemeは、scheme界隈では爆速なんだけど、numpyには勝てないな。

vs. guile-3.0

guile-3.0も入っていたので、コンペに参加。ベクターを使うには、

(use-modules (srfi srfi-4))
(make-f64vector n 0.0)

の様にモジュールをロードしてから、ベクターの型を明示する。

sakae@lu:tmp$ guile-3.0 -l flvec.ss
;;; note: auto-compilation is enabled, set GUILE_AUTO_COMPILE=0
;;;       or pass the --no-auto-compile argument to disable.
;;; compiling /tmp/flvec.ss
;;; /tmp/flvec.ss:44:17: warning: possibly unbound variable `time-difference'
;;; compiled /home/sakae/.cache/guile/ccache/3.0-LE-8-4.6/tmp/flvec.ss.go
warming up ...
GNU Guile 3.0.9
Copyright (C) 1995-2023 Free Software Foundation, Inc.

scheme@(guile-user)> ,time (compute-map-flvector sbp dbp)
$1 = #t
;; 0.020962s real time, 0.020939s run time.  0.000000s spent in GC.

replに時間計測用のコマンドが有ったので、それを有効利用。計算が主体の 関数だと、chezと甲乙つけがたいな。

;; 0.157273s real time, 0.157037s run time.  0.000000s spent in GC.

同じ事を河豚板でやったら、こうなった。何故こんな違いが出るの?

Fuguita 7.7

pkgのエリアをsaveas領域から外したやつ

speed

河豚板の起動スクリプトrcの最初と最後が、こうなっている。

mount -w /dev/rd0a /
: > /boottmp/boot_starts

# 色々な仕事を実行

date > /boottmp/boot_livecd_rc_ends

#========================================
# chain original /etc/rc
#

exec /bin/ksh /etc/rc "$init_args"

boot_startsboot_libcd_rc_end の時間情報から、スクリプトの実行時間が 分るんだな。

fu$ cd /boottmp/
fu$ cat boot_livecd_rc_ends
Sun Nov 23 05:23:33 JST 2025
fu$ stat boot_starts
4352 112 -rw-r--r-- 1 root wheel 0 0 "Nov 23 05:22:23 2025" "Nov 23 05:22:23 2025" "Nov 23 05:22:23 2025" 4096 0 0 boot_starts

boot_starts の時は、まだdateが使えないんで、ファイルのコンテンツとしては 何も無い。しょうがないのでstatでファイルのタイムスタンプを取り出す。この時間差の主要成分は、 過去の情報をRAMに転送してる時間だ。時間差の計算ってめんどいな。何処でもpythonか。 grok.comとChatGPT.comで競演させてみた。前者は汎用的な長いコードを提示。 後者は改造に便利な短いコードを提示してきた。オイラーの昔からの習慣でスッキリな 方を採用する。

from datetime import datetime

date_str1 = "Sat Nov 22 22:28:33 JST 2025"
date_str2 = "Sun Nov 23 06:10:41 JST 2025"

date_format = "%a %b %d %H:%M:%S JST %Y"
dt1 = datetime.strptime(date_str1, date_format)
dt2 = datetime.strptime(date_str2, date_format)

time_difference = dt2 - dt1
print(f"時間差: {time_difference}")
print(f"時間差(秒単位): {time_difference.total_seconds()}秒")

statとdateの時間フォーマットが、微妙に違うじゃん。主に人様向けのdateは、冗長に なってるな。stat -x すれば、人間用に態度を改めてくれた。不便な事が有ったらman してみろってのは、生活の知恵だな。

bsd-fi.mp

このカーネルがファイルシステムを内蔵してる事を確認してみる。 sysmedia(sd1a)が見えないので、強制マウントして、それを取り出す。

fu$ cp /mnt/bsd-fi.mp bsd-fi.gz
fu$ gzip -d bsd-fi.gz
fu$ doas rdsetroot -dx bsd-fi > mini.img
rd_root_size_off: 0x9f0
rd_root_image_off: 0xa00
rd_root_size  val: 0x400000 (8192 blocks)
copying root image...
...copied 4194304 bytes

gzip -d する時サフィックスが付いていないと文句を言われるので、付ている。

fu$ doas vnconfig vnd0 /home/sakae/mini.img
fu$ doas mount /dev/vnd0a /mnt
fu$ ls -l /mnt
total 49
drwxr-xr-x  2 root  wheel    512 Jun 15 09:52 CVS/
lrwxr-xr-x  1 root  wheel      7 Jun 15 09:52 bin@ -> boottmp
drwxr-xr-x  3 root  wheel   1024 Jun 15 09:53 boottmp/
drwxr-xr-x  6 root  wheel  20480 Jun 15 09:53 dev/
lrwxr-xr-x  1 root  wheel      7 Jun 15 09:52 etc@ -> boottmp
drwxr-xr-x  2 root  wheel    512 Jun 15 09:52 fuguita/
drwxr-xr-x  2 root  wheel    512 Jun 15 09:52 mnt/
drwxr-xr-x  2 root  wheel    512 Jun 15 09:52 ram/
lrwxr-xr-x  1 root  wheel      7 Jun 15 09:52 sbin@ -> boottmp
drwxr-xr-x  2 root  wheel    512 Jun 15 09:52 sysmedia/
drwx------  2 root  wheel    512 Jun 15 09:52 sysmedia-iso/
drwxrwxrwt  2 root  wheel    512 Jun 15 09:52 tmp/
fu$ ls -l /mnt/boottmp/rc
-rw-r--r--  1 root  wheel  42257 Mar 25  2025 /mnt/boottmp/rc

Fuguita 7.8

どうも7.7の方は、進化の過程で突然変異し、やがて自然淘汰されたっぽいので、 主流の方を調べてみる。

speed

USB memoryに入れた7.7ではrcの実行時間が、1:10 だったのに対し、USB HDDの方は、 5:12 だった。HDDの方は、これでもかと言うぐらいpkgを入れたからなあ。 USB memoryの方は、emacs-noX,firefox,gdbぐらいの輕い構成。これでも、普通に 生活するなら、取り敢えず十分だ。

bsd-fi.mp

fu$ cp /sysmedia/bsd-fi.mp bsd.fi.gz
fu$ gzip -d bsd.fi.gz
fu$ doas rdsetroot -dx bsd.fi >mini.img
rd_root_size_off: 0x394d0
rd_root_image_off: 0x394e0
rd_root_size  val: 0x400000 (8192 blocks)
copying root image...
...copied 4194304 bytes
fu$ doas vnconfig vnd0 /home/sakae/mini.img                                         <
fu$ doas mount /dev/vnd0a /mnt
fu$ ls -l /mnt
total 49
drwxr-xr-x  2 root  wheel    512 Oct 29 02:07 CVS/
lrwxr-xr-x  1 root  wheel      7 Oct 29 02:36 bin@ -> boottmp
drwxr-xr-x  3 root  wheel   1024 Oct 29 02:36 boottmp/
drwxr-xr-x  6 root  wheel  20480 Oct 29 02:36 dev/
lrwxr-xr-x  1 root  wheel      7 Oct 29 02:36 etc@ -> boottmp
drwxr-xr-x  2 root  wheel    512 Oct 29 02:36 fuguita/
drwxr-xr-x  2 root  wheel    512 Oct 29 02:36 mnt/
drwxr-xr-x  2 root  wheel    512 Oct 29 02:36 ram/
lrwxr-xr-x  1 root  wheel      7 Oct 29 02:36 sbin@ -> boottmp
drwxr-xr-x  2 root  wheel    512 Oct 29 02:36 sysmedia/
drwx------  2 root  wheel    512 Oct 29 02:36 sysmedia-iso/
drwxrwxrwt  2 root  wheel    512 Oct 29 02:36 tmp/
fu$ ls -l /mnt/boottmp/rc
-rw-r--r--  1 root  wheel  43892 Aug 19 16:12 /mnt/boottmp/rc

Makefile

結局7.8版のカーネルも、ファイルシステムを内蔵してた。rcは、Fuguitaのインスーラー なんだな。7.8のrcを見てて、fstabを生成してる所

X/fuguitaX|X/sysmediaX)
    echo $1 $3 $5 ro 0 0 ;;

roをrwに変更すれば、read-onlyを外せると目論だんだけど、挫折してたんだ。こういう 裏が有ったのね。今なら、仕組みが分ったんで、変更したのをrdsetrootを使って、 カーネルに入れてしまえる(こういうの、遺伝子改変とでも言うのかな。勿論、この場合の 遺伝子はrcの事です)。でも、それをやっちゃうと、河豚板の系統樹上では、突然変位 となってしまう。河豚板の一番の利点が消失してしまうので、実施は控える事にする。

それより、河豚板がどの様に作成されるかって言う発生学の方に興味が有るぞ。

河豚板ガイド/4-開発編

それにはMakefileを見るに限る。これって、shell scriptの強化版だから恐るに 足りず(GNUのconfigureが作成するMakefileは見ると目が腐るんで閲覧辞退ね)。

rcが、どの様に格納されてるか? すなわちインストーラの作成方法を確認してみたい。 580行ぐらい有るので、rdsetrootで検索して、見る所を絞りこむ。

#========================================
# merging a RAM disk root filesystem image
# into a kernel
#
$(KERN_SP): rdroot.ffsimg $(BSD_SP)
        $(MAKE) open-sysmedia
        cp $(BSD_SP) bsd
        rdsetroot bsd rdroot.ffsimg
        $(COMPRESS) -c9 bsd > $(KERN_SP)
        rm bsd
        $(MAKE) close-sysmedia

KERN_SP てのが、いわば関数名。Makefile用語ではターゲットなんて言う。引数が 2個ある(rdroot.ffsimg, BSD_SP )。makeの大事な機能として、余計なターゲットを 不用意に呼出しない様になってる。このターゲットが呼ばれるのは、引数自体が存在 しなかったり、変更が有った場合のみだ。

注目は、カーネルをギンギンに圧縮(-c9)してる事ですかね。次は、土台になる カーネルを、どうやって作成してるかだ。

$(BSD_SP):
        (cd $(KERNSRC)/arch/$(ARCH)/compile/RDROOT &&\
         make $(KERNOPT))

これ、カーネルをコンパイルすべき場所に移動して、コンパイルするって指示。 この前段階で、コンパイル用のMakefileを作成する必要が有る。

kernconfig:
        (cd $(KERNSRC)/conf &&\
         cp GENERIC RDROOT &&\
         patch < $(BLDDIR)/lib/RDROOT.diff)
        (cd $(KERNSRC)/arch/$(ARCH)/conf &&\
         cp GENERIC RDROOT &&\
         patch < $(BLDDIR)/lib/RDROOT.$(ARCH).diff &&\
         cp GENERIC.MP RDROOT.MP &&\
         patch < $(BLDDIR)/lib/RDROOT.MP.$(ARCH).diff &&\
         config RDROOT && config RDROOT.MP)

通常のカーネル構成であるGENERICを元に、パッチを2段にわたって当てて(マシンに 依存しない部分とマシンに依存する部分)、最後にconfigでMakefileを作成してる。

rdroot.ffsimg: /usr/src/etc/etc.$(ARCH)/MAKEDEV\
               /usr/src/etc/etc.$(ARCH)/login.conf\
               lib/bootbin/obj/bootbin $(BOOTTMPS)
        $(MAKE) close-rdroot
# create rdroot.ffsimg
        ./lib/setup_fsimg.sh rdroot.ffsimg 4M 1500 '-b 4096 -f 512'
        vnconfig vnd0 rdroot.ffsimg
         :
# setup inside rdroot.ffsimg
        ln -sf boottmp /mnt/bin
         :

次は、カーネルに入れ込む、ファイルシステムの作成だな。結構大きな作業を している。

先に細かい部分を見てしまったけれど、全体像はこちらだ。

setup:
        $(MAKE) kernconfig
        $(MAKE) kernclean
        $(MAKE) rdroot.ffsimg
        $(MAKE) imgs

i386-install.img

参考値として、見ておく。

ob$ ls -l
total 16082
drwxr-xr-x  3 root  wheel      512 Oct 13 07:30 7.8/
-rw-r--r--  1 root  wheel    88908 Oct 13 07:30 boot
-r-xr-xr-x  2 root  wheel  4056803 Oct 13 07:30 bsd*
-r-xr-xr-x  2 root  wheel  4056803 Oct 13 07:30 bsd.rd*
drwxr-xr-x  2 root  wheel      512 Oct 13 07:30 etc/
ob$ cat etc/boot.conf
set image /7.8/i386/bsd.rd
ob$ ls -l 7.8/*
total 962174
-rw-r--r--  1 build  2000          54 Oct 13 06:49 BUILDINFO
-rw-r--r--  1 root   2000       44407 Oct 13 06:49 INSTALL.i386
-rw-r--r--  1 root   wheel       1634 Oct 13 07:31 SHA256
-rw-r--r--  1 build  2000   287089730 Oct 13 06:38 base78.tgz
-rwxr-xr-x  1 build  2000    15956370 Oct 13 06:37 bsd*
-rwxr-xr-x  1 build  2000    16048199 Oct 13 06:37 bsd.mp*
-rwxr-xr-x  1 root   2000     4056803 Oct 13 06:49 bsd.rd*
-rw-r--r--  1 build  2000    46382204 Oct 13 06:38 comp78.tgz
 :

bsd.rdの中身

ob$ rdsetroot -dx bsd.rd > sim.img
rd_root_size_off: 0x1754
rd_root_image_off: 0x1758
rd_root_size  val: 0x418000 (8384 blocks)
copying root image...
...copied 4292608 bytes
 :
ob$ ls /mnt/
autoinstall@ etc/         install.sub* sbin/        usr/
bin/         install@     mnt/         tmp/         var/
dev/         install.md   mnt2/        upgrade@

README

NHKの番組の隙間時間に、ざんねんないきもの なんてのをやってる。この元ネタを 図書館で発見したぞ。児童書の分類だった。年寄 = ききわけのない児童なんで、 オイラーにも読む権利が有るぞ。一冊借りてみた。

ざんねんないきもの辞典

ざんねんないきものは株式会社高橋書店の登録商標です。

登録商標って、こういうのでもいいのね。

そして、もう一冊、すごい名前の本。

『種の起源』を読んだふりができる本

ダーウィンが 種の起源を発表したのは、江戸時代。それより、進化学はずっと 進歩してて、間違いも散見される。

種の起源は、古典的名著とされてるけど、結構難解で、途中で挫折する事が多い らしい。ならば、古典と現代進化学とのdiffを取りつつ解説しましょ。これで、古典 も読んだふりができるぞ、って趣旨で記述されてる。タイパがいいぞ。

ざんねんないきもの それは、ホモ・サピエンス。

食物の通路(食道)と空気の通路(気道)を首の部分で共有している。進化は、 既存の部分を改造するしかないので、魚時代の設計(進化)を継承して改造。

おかげで、喉に餅やら蒟蒻ゼリーをつまらせて窒息とか、誤嚥性肺炎とかになる。

特に人間は、2足歩行や言語獲得のため喉頭が下降した為、他の哺乳類より絶対的に危険度が 増加したとな。

エンジニア 系記事まとめ (at Note)

「情報処理」無償公開記事 (at Note)

「情報処理」巻頭コラム (at Note)

最近の『らじる★らじる』(その1) 楽しいHacking


This year's Index

Home