
祝日特別記事 Hello Worldのために、117万回も関数を呼んだ 効率原理主義へのアンチテーゼ コンピュテーショナル・アートへの挑戦
Hello Worldのために、117万回も関数を呼んだ

こんにちは黒パグです🐾
GPTチャット名:新国道2号を移動していたら、Xで各言語のHello Worldを並べた画像を見かけた。Pythonなら1行。なるほど、短い。
そこで、わざわざ遠回りするプログラムを作った。素数を用意し、フィボナッチを再帰で求め、余分な処理を追加する。できたものを、さらに無駄にリファクタリングした。
「ヨシ!!!無駄にリファクタリングしてきた!!」
お忙しい方のために!→ ハローワールドを表示させるまでに壮大なる回り道。
以上です。
この記事の要約はこれだけです。
出来上がった原本は154行。名前はDetourEngine。寄り道エンジンである。
上記のポッドキャストは無駄に洗練された解説が聞ける。むしろこの本編はこのポッドキャストを聴きながら読むべきであるが、おそらく無駄に壮大である。

比較対象はこちら。
```python
print("Hello World")
```
もう終わった。向こうはまだエンジンの材料を集めている。
原本を開くと、最初から答えが置いてある。
```python
target_codes = [72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]
```
この数を文字へ変えればHello Worldになる。探す必要も、推測する必要もない。ここにある11個を、そのまま使えばいい。

Hに当たる72を見てみる。
エンジンは素数とフィボナッチから別の値を計算し、それを72に足す。その直後、足した値を引く。素数の積から作った余分な値も、同じように足して引く。
手は動いている。72は動いていない。

続いて72を素因数分解する。2×2×2×3×3までばらし、全部掛け直して72へ戻す。
ここで使う素数は、エンジンを作るたびに128以下から生成している。原本では31個。表から取り出して分解し、分解した部品をまた組み立てる。部品の交換はない。
フィボナッチの表も使う。72を55+13+3+1に分け、それを足し直す。こちらも72へ戻るが、表を用意する仕事がずいぶん多い。

フィボナッチ数は、直前の二つを足して求められる。計算済みの答えを保存しておけば、同じ計算を繰り返さずに済む。
原本のコメントは「メモ化は甘え」。保存しない方針だった。
0から24までの25項を、それぞれ二重再帰で求める。呼び出し監査では、表を一つ作るだけでfib関数が392,809回呼ばれていた。エンジンを3個作るので、プログラム1回につき1,178,427回になる。
原本には「約24万回」とも書いてあった。25項を別々に計算した表全体では約39万回なので、展示側に訂正の注釈を付けた。作品のコードは変えていない。
無駄を増やしたら、説明まで直す仕事が発生した。

72はまだ旅行中である。
コラッツの工程では、偶数なら半分、奇数なら3倍して1を足す。1へ着くまで、通った操作を記録する。
着いたら、その記録を逆順に読む。半分にしたところは2倍、3倍して1を足したところは1を引いて3で割る。足跡をたどって72へ帰る。
ここで扱うのは、原本に固定された文字コードだ。任意の整数についてコラッツ予想を証明したわけではない。

チャーチ数という寄り道もある。たとえば7を「ある関数を7回適用するもの」として組み立て、そこへ1を足す関数を渡すと、また7が得られる。
スライドの装置には入力にも出力にも7がある。中で何が起きているかを見せるため、ずいぶん大きな設備になった。
原本では各文字コードにこの変換をかけている。72なら72回分。整数を関数の形へ移したあと、すぐ整数へ戻して次の工程へ渡す。

猿の担当は乱数だ。
32から126までの整数を引き、欲しい文字コードと一致するまで待つ。当たるまでの上限は設けていない。スライドの「無限に待つ」はこの上限なしのループを指している。
当たったら、どうするか。
```python
return code
```
元から渡されていた値を返す。
乱数で得た値を次へ渡さない。外れた回数はself.logに保存するが、作品の後段はそのログを読まない。原本のコメントにも「猿の成果物は信用しないので、元の値を返す」とある。

この組み立てを3回行い、Counterで多数決を取る。
別の実装を3種類用意したのではなく、同じDetourEngineを3個作っている。三者とも同じ仕事をして、同じ11文字を提出する設計だ。全員が同じように間違う場合を、この多数決だけで見つけられるわけではない。
最後には、答えが本当にHello Worldかをassertで確認している。

その3票を作る組み立ての中にも、まだ余分な往復がある。
文字コードは、加工へ出す前に逆順にしてある。帰りは再帰関数で反転し直す。リストの先頭を外して残りを渡し、戻ってきたところへ先頭の要素を付け直す。
毎回リストを切り出し、連結するので、文字数nを増やせばこの帰り道はO(n²)になる。今回は11文字。それでも、最初に逆順へ出した荷物を元の順番へ戻す仕事は、ちゃんと増えた。

では、どれだけ余分に働いたのか。
保存されている測定は、同じLinux環境のCPython 3.12.14で、通常版と原本を毎回新しいプロセスへ渡したものだ。時間は各40回、メモリの追跡は別に各12回。比較に使った104回では、どちらも同じ標準出力で、終了コードは0だった。
| 測定項目・中央値 | 通常版 | DetourEngine |
| --- | --- | --- |
| 本体の実行時間 | 0.032 ms | 46.513 ms |
| 起動などを含む全体の時間 | 31.838 ms | 81.118 ms |
| Pythonの追加確保ピーク | 376 bytes | 62,991 bytes |
| 時間測定時のプロセスRSSピーク | 13.750 MiB | 13.750 MiB |
本体の時間は、コンパイル済みコードの実行開始から標準出力のflushまで。Pythonの起動などを含めた時間とは分けている。
本体の中央値の比は約1471倍。ただし、表の値は丸めてある。比に使った元の値は0.031617 msと46.513152 msで、表示値同士を割った結果とは少し違う。
通常版が極端に短く、測定の手間に近い値であることも残る。起動などを含めた全体なら、この環境で約2.55倍だった。「どの端末でも1471倍」とは言えない。
メモリは少し拍子抜けした。Pythonが追加で確保した量は増えたのに、プロセス全体のRSSピークの中央値は同じだった。しかも、メモリ追跡時のRSS中央値は双方13.875 MiB。スライドの13.750 MiBは、時間測定時の値を載せている。

コードを長くしただけ、と言われると、そこは少し違う。
原本に定義した関数は、通常の実行経路ですべて呼ばれる。点火の計算も、猿の抽選も、実際に動いている。
点火では結果を足して引く。猿は当たるまで待つが、返す値は元からある。素因数分解やチャーチ数の工程は、別の形を経由して同じ値へ戻る。
スライドの「未使用のコード」欄は比較用だ。この原本で読むのは、「出力に寄与しない計算」と「恒等変換」の二つの欄になる。

最終出力はこうなった。
```text
Hello World
```
表示は空白込みの11文字。標準出力には末尾の改行が付くので12 bytesになる。1,178,427回はfib関数の呼び出し数であって、出力した文字数ではない。

これを無駄なもの博物館の本館、展示022に収めた。
展示ページでは文字を選んで工程を追ったり、猿に抽選をやり直させたりできる。画面で触る工程模型はJavaScript製で、Python原本をその場で動かしているわけではない。原本と実測資料は、別に見られるようにしてある。
操作を重ねると、余分な操作回数の入った鑑賞証を持ち帰れる。最後のスライドでは0回になっている。展示で実際に触った分は、自分の証に残る。

#ハローワールド #HelloWorld #コンピュテーショナルアート #計算機芸術 #Python #プログラミング #アルゴリズム #再帰 #無駄なプログラム #無駄の美学
[展示022「Hello Worldのための壮大なる遠回り」](https://muda-mono-museum.llm48.chatgpt.site/apps/022-hello-detour/index.html)
おまけ


よかったらサイトに訪れて体感してみてくださいね!