ハッカソンにスポンサーとして参加した話
会社に来ているアルバイトの学生(彼は同じゼミの後輩であり、奇遇にも大学院も同じである)が主催したハッカソンに、会社として協賛することになった。協賛している会社は15分間スピーチする権利が与えられ、立場上私が担当することになったのだが、正直何を話すか相当悩んだ。
話す枠はコーディングやプレゼン準備などが終わった後、結果発表会の直前なので、きっと参加者の学生はみな疲れていることだろう、やりきった感で一杯か不完全燃焼でモヤモヤしている人間には、インプットが多すぎてもコップから水が溢れるだけで何も残らない。無難にいけば会社紹介だろうが、それだけだとあまりに普通で面白くない。結局、企業で勉強し続ける難しさ、環境づくりが大事、みたいな話をしたつもりだが、準備の足りなさも相まって自分でも何を伝えたかったのか(そもそも伝えるという行為を望んでいたのか)分からない結果になってしまった。
とはいえ、学生や若い社員に伝えたいことは色々ある。勉強し続けなければならない事実、勉強を楽しいと思えるよう意識を変えること、自己決定からくる責任をうまく利用すること、キャリアの選択肢を大きく広げる英語の重要性、体系的に学ぶ重要さ、転職や be the worst の話、などなど。だが、結局自分の伝えたいことは自分の体験が元になってるので、個々の学生や社員にそれがハマるかは全く分からない。結局、教育する対象の持つ課題によって接し方は変わるんだよなぁ、と一回りしてしまったような感じである。
さて、ハッカソンの発表会では、講師や先生方からなかなか厳しい意見が出て、学生がモチベーションを下げないだろうか、と心配をしてしまったが、みな目的意識の高そうな子だったので杞憂と信じたい。
企業で人材育成、組織作りをしてつくづく思うのは、傑出した能力の人間をどう確保・育成するか、では無く、平均的だったり能力の劣る人材をどう引き上げて、チームとして成果を出すか、の難しさである。学習意欲の高い人材というのは極論を言えば勝手に技術力を伸ばしてくれるのだ(他の能力も同時に伸びる、とは言っていない)。一方で、平均的だったり、それ以下の人材をどう指導するのかは悩ましい。本を借りたら翌日までに読んで返すのがデフォルトな世界を知っているだけに、就業中にタラタラと読んで1週間かけても半分しか進んでない輩を叱咤したくもなるが、それはパワハラにあたるのではないだろうか、とコンプライアンスブレーキがかかってしまうのだ。また、本人が傑出した能力の人間になることを明示的にせよ暗黙的にせよ望んでいないような場合、何が本人と組織にとって良いことなのか、ということも考えてしまう。
厳しい意見を出した講師や先生方は、参加者の学生を傑出した能力の人間に育てること(そして育つ鱗片を見出していること)、要は期待をしているのだろう。ある種の信頼関係が無いと出来ないことである。方や無難なコメントしか出来なかった私は、参加者に特に何の期待もしていない、とも言える(もっとも初対面の人間に何かを期待する、というのもおかしな話ではある)。
という感じで、単にスポンサーとして参加しただけのイベントではあるが、人材育成という観点で色々と思いなおすところがあり貴重な時間を頂いた。
ところで以前の自分だったらキラキラした学生が課題に挑んでいるのを見るだけでモチベーションがアップしていただろうが、歳を取り感受性が弱まったのだろうか、そのような思いに至らなかった。または今の自分のミッションが明確でそれに邁進しているので「オレもがんばらなきゃ!」と易々とならなかったのかは分からない。両方かな。
40%未満キーボード Collide39 を作った話
ここ数か月、自作キーボードの沼にはまってしまい、meishi, crkbd, Gherkin, Collide39, cocoa40, Reviung39s と制作してみたが、自分的に Collide39 がかなりお気に入りなので、キーマップの工夫なども含めて記事にしてみる。
手前から Gherkin, Collide39, Reviung39s
Collide39 は space cat design で販売されている 40% 未満キーボード(30% か、と言われると微妙である)で、Gherkin にマクロパッドを付けるのがコンセプトのキーボードとして設計された、っぽい。
https://spacecat.design/collections/pcbs-cases-kits/products/collide39-pcb-kits
crkbd では多い、Gherkin では少ない、miuni32 ならいけるかもしれないけど、どこで買えばいいか分からない、と考えあぐねいていたところ候補にあがり購入した。Gherkin + マクロパッド的な利用ではなく、左右 6 列× 3 行、B と N の間にスペースを置いた、少し特殊な配列のキーボードとして利用することを考えていた。が、これが私的に正解で、crkbd、Reviung39 といった有能なキーボードを差し置いてメインのキーボードとして数カ月間、仕事を支えていれている。
尚、ケースは strata kb で購入出来る。
https://stratakb.com/store/cases/collide39-case
制作自体は特筆することは無く、表面実装も無いことから簡単な部類に入ると思う。pro micro を表向きに付けないとならない点が唯一のネックだろうか。キースイッチは当初 Gateron の黒軸を付けていたが、最終的に Zeal PC の Zilent v2 67g に落ち着いた。キーキャップは GMK Paperwork。
さて、軸やキーキャップのカタログを見てカスタマイズの夢想をする時間や、遊舎工房、TALP 等のホームページ/Twitter/実店舗を巡回する以上に、自作キーボードの世界で割く必要のある時間があるとすれば、それはキーマップの設計、妄想である(私の場合)。普通のノート PC も仕事で使うため、あまりに特殊な配列にはしたくないし、10年近く使っていた KINESIS advantage のような使用感も欲しい。最初に買い求めた crkbd より延々と試行錯誤した結果、今は以下のコンセプトのキーマップに落ち着いた:
- enter, bs は ctrl と組み合わせて実現する(ctrl + m, ctrl + h)。このため、通常の ctrl キーは "コントロールレイヤ" キーとなる
- 通常の ctrl キーを使う場合はコントロールレイヤキーを素早く2回押す
- raise レイヤを廃止し、lower(数字、一部記号)のみとする。入力のために2つレイヤがあるのは私的に難し過ぎた。従来 raise で入れていた記号は lower + shift で入れる(lower は raise との対義語なので、fn レイヤ、と名前を改めたが、sym のほうがしっくりくるかもしれない)
- カーソルキーは fn ではなくコントロールレイヤに置く。意味的にカーソル移動は「コントロール」だから
- フォームを保つために左右にシフトを設ける。以前の私は左シフトしか使わない人間だったが、右シフトもきちんと使うよう矯正した経緯がある
- マイナスキーはよく使うので、p の隣に置く(これは KINESIS からそうしていた)
- スペースと fn レイヤキーを混合させる
といったところ。コードは以下である:
#include QMK_KEYBOARD_H #define _QWERTY 0 #define _FN 1 #define _MYCTL 2 enum custom_keycodes { KC_QWERTY = SAFE_RANGE, KC_LANG, KC_FN, KC_SP_FN, KC_MYCTL, KC_KILL, KC_LGALT }; #define XTL_Q LCTL(KC_Q) #define XTL_W LCTL(KC_W) #define XTL_E LCTL(KC_E) #define XTL_R LCTL(KC_R) #define XTL_T LCTL(KC_T) #define XTL_Y LCTL(KC_Y) #define XTL_U LCTL(KC_U) #define XTL_I LCTL(KC_I) #define XTL_O LCTL(KC_O) #define XTL_P LCTL(KC_P) #define XTL_A LCTL(KC_A) #define XTL_S LCTL(KC_S) #define XTL_D LCTL(KC_D) #define XTL_F LCTL(KC_F) #define XTL_G LCTL(KC_G) #define XTL_H LCTL(KC_H) #define XTL_J LCTL(KC_J) #define XTL_K LCTL(KC_K) #define XTL_L LCTL(KC_L) #define XTL_Z LCTL(KC_Z) #define XTL_X LCTL(KC_X) #define XTL_C LCTL(KC_C) #define XTL_V LCTL(KC_V) #define XTL_B LCTL(KC_B) #define XTL_N LCTL(KC_N) #define XTL_M LCTL(KC_M) const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = { /* Base Layout * ,------------------------------------------------------------------------------------------, * | TAB | Q | W | E | R | T | ALT | Y | U | I | O | P | - | * |------+------+------+------+------+------+------+------+------+------+--------------------| * | CTL | A | S | D | F | G | LANG | H | J | K | L | ; | ' | * |------+------+------+------+------+------+------+------+------+------+--------------------| * |SHIFT | Z | X | C | V | B |SPC/FN| N | M | , | . | / |SHIFT | * `------------------------------------------------------------------------------------------' */ [_QWERTY] = LAYOUT( KC_TAB, KC_Q, KC_W, KC_E, KC_R, KC_T, KC_LALT, KC_Y, KC_U, KC_I, KC_O, KC_P, KC_MINS, KC_MYCTL, KC_A, KC_S, KC_D, KC_F, KC_G, KC_LANG, KC_H, KC_J, KC_K, KC_L, KC_SCLN, KC_QUOT, KC_LSFT, KC_Z, KC_X, KC_C, KC_V, KC_B, KC_SP_FN, KC_N, KC_M, KC_COMM, KC_DOT, KC_SLSH, KC_RSFT ), [_FN] = LAYOUT( KC_ESC, KC_1, KC_2, KC_3, KC_4, KC_5, KC_KILL, KC_6, KC_7, KC_8, KC_9, KC_0, KC_EQL, KC_MYCTL, KC_F1, KC_F2, KC_F3, KC_F4, KC_F5, _______, _______, KC_GRV, KC_BSLS, KC_LBRC, KC_RBRC, KC_QUOT, KC_LSFT, KC_F6, KC_F7, KC_F8, KC_F9, KC_F10, _______, _______, _______, _______, _______, _______, KC_RSFT ), [_MYCTL] = LAYOUT( KC_TAB, _______, XTL_W, _______, XTL_R, XTL_T, KC_LALT, _______, _______, _______, _______, XTL_P, KC_HOME, _______, XTL_A, XTL_S, KC_DEL, KC_PGDN, _______, KC_BSPC, KC_BSPC, _______, _______, KC_UP, KC_RBRC, KC_END, KC_LSFT, XTL_Z, XTL_X, XTL_C, XTL_V, KC_PGUP, KC_SPC, _______, KC_ENT, KC_LEFT, KC_DOWN, KC_RIGHT, KC_RSFT ), }; static bool sp_keeped = false; static bool lgalt_keeped = false; static bool octl_enabled = false; static bool myctl_pressed = false; uint16_t log_timer = 0; void persistent_default_layer_set(uint16_t default_layer) { eeconfig_update_default_layer(default_layer); default_layer_set(default_layer); } void reset_special_keys(void) { sp_keeped = false; lgalt_keeped = false; octl_enabled = false; myctl_pressed = false; } bool process_record_user(uint16_t keycode, keyrecord_t *record) { switch (keycode) { case KC_SP_FN: if (record->event.pressed) { sp_keeped = true; layer_on(_FN); } else { layer_off(_FN); if (sp_keeped) { register_code(KC_SPC); unregister_code(KC_SPC); sp_keeped = false; } } return false; case KC_LANG: if (record->event.pressed) { register_code(KC_LALT); register_code(KC_GRV); unregister_code(KC_GRV); unregister_code(KC_LALT); return false; } break; case KC_KILL: if (record->event.pressed) { register_code(KC_LCTL); register_code(KC_LALT); register_code(KC_DEL); unregister_code(KC_DEL); unregister_code(KC_LALT); unregister_code(KC_LCTL); } return false; case KC_LGALT: if (record->event.pressed) { lgalt_keeped = true; register_code(KC_LALT); } else { if (lgalt_keeped) { register_code(KC_LALT); register_code(KC_GRV); unregister_code(KC_GRV); lgalt_keeped = false; } unregister_code(KC_LALT); } return false; case KC_MYCTL: if (record->event.pressed) { if (octl_enabled && (timer_elapsed(log_timer) < 100)) { // layer_on(_OCTL); register_code(KC_LCTL); } else { myctl_pressed = true; octl_enabled = false; layer_on(_MYCTL); } } else { // layer_off(_OCTL); unregister_code(KC_LCTL); layer_off(_MYCTL); if (myctl_pressed) { log_timer = timer_read(); octl_enabled = true; } myctl_pressed = false; } return false; default: if (record->event.pressed) { reset_special_keys(); } break; } return true; }
qmk に色々とマクロがあることは知っているが、反応速度が遅いので直接制御している。これは crkbd のコードからの流用である。
数か月、概ねこのコンセプトで落ち着いているが、課題はある。
私は vimmer なのだが、泣く泣くカーソルキーは hjkl ではなくコントロールレイヤの l,./ としている。これは h をバックスペースで使っている為で、コントロールレイヤのコンセプトと共存が不可能で非常に悩ましい問題ではある。ただ、レイヤキーと hjkl でカーソル移動とすると、肝心の vi でもカーソル移動にレイヤキーを押していたりして複雑な思いをしたのも事実。
もっとも、カーソルキー問題は誤入力には繋がらず慣れが解決してくれるため大きな問題ではない。一番の課題はスペースキーが fn レイヤキーと兼ねている点だ。スペースを打ち終わる前に次のキーを押してしまうと、スペースがレイヤキーとして解釈されてしまい誤入力の元となる。特に unix コマンドのオプションをハイフンで入れる時に顕著で、例えば ls -l が ls=l となることが多々ある。スペースの両脇にレイヤキーを配置している Reviung39 はこの点をうまく解消してくれるが、一方でスペース/レイヤキー共存は親指の動きに迷いが無くメリットでもあるため悩ましい。タイプ速度は多少遅くなるがスペースを打った後は一呼吸置くことで誤入力は軽減される。または、fn + - は、スペースと - を出力するような専用キーとして定義しても良い。その場合、= は fn + ' になるであろうか。
尚、このキーマップでは、alt と lang は共存可能である(KC_LGALT がそれ)が、今は別々にしている。すなわちまだ1キー分、余りがある。
キーマップを作っていて思うことは、用途に応じて最適なキーマップは異なるということだ。このキーマップはエクセルでの数値入力に適しているとは言えない。また、Windows キーも無い。無理やりぶっこもうと思えば可能かもしれないが、そうしていない。40% 未満の場合、今のキーマップがチートシート無しで覚えられる私的な限界のようだ。まだ挙げていないキーマップのコンセプトがあるとしたら、欲張らないこと、だろうか。例えば、以前は numpad なんていらねーよ、と numpad の存在を dis っていたが、一つのキーボードで何でもしようと思わず、Attack25 のように数字入力とカーソル移動に特化した専用のキーボードを別途持っても良いだろう、と最近思ったりしている(新たにキーボードを作るための大義名分なのかもしれないが)。実際、仮想デスクトップの切り替えは便利なので、meishi にその役割を担わせている。
この記事は Collide39 で書いた。
Cygwin 環境でのコピー&ペーストをコマンドラインで
Python の動作環境をどうするかは常に悩みどころで、以前は Windows 上の VirtualBox で環境を作り、Cygwin から ssh することが多かった。が、最近は顧客要望もあって Windows の Anaconda を使うことが多くなってきた。とは言えファイルの操作は相変わらず Cygwin で行うので、Cygwin 環境上のワーキングディレクトリへ Anaconda のコンソールから cd するケースが多い。
Cygwin から
$ explorer .
とすると、エクスプローラがカレントディレクトリを開いてくれるので、パスをマウス操作でコピーすることが可能である。
Anaconda のコンソールで律儀に cd するよりは100倍楽だが、もう少し楽をしたいので少し調べてみたところ、/dev/clipboard の存在を知った。
$ echo hehehe > /dev/clipboard
とすると、Windows のクリップボードにコピーが出来る。こいつを cygpath と組み合わせると
$ cygpath -aw . C:\cygwin64\home\suzuki_takaharu\prj\geo_conv\test $ cygpath -aw . > /dev/clipboard
カレントディレクトリをクリップボードにコピー出来る。あとは適当な名前で alias してやれば良い。
$ alias _cpd="cygpath -aw . > /dev/clipboard"
Anaconda のコンソールでは Ctrl+v でペーストしても良いし、マウスの右クリックでもペーストがされる。
この問題は DOS 窓でしか動かないコマンドを動かす際にも付きまとうので、Cygwin を普段使いする人にはお勧めなテクニックである。
HP 35s でプログラミング - 4
前回 HP 35s 向けにそこそこ複雑なアルゴリズムを実装してみたが、なかなかパフォーマンス的に厳しい事実が垣間見えてしまった。ほかに何かネタが無いか考えあぐねいていたところ、出版されたのはだいぶ前だが「ハッカーの楽しみ」という本のことを思い出した。英語版では第二版が 2012年に出ている。
Hacker's Delight (English Edition)
- 作者: Henry S. Warren
- 出版社/メーカー: Addison-Wesley Professional
- 発売日: 2012/09/25
- メディア: Kindle版
- この商品を含むブログ (2件) を見る
C 言語によるコードは以下(分割統治法といっても再帰は使わない):
x = (x & 0x55555555) + ((x >> 1) & 0x55555555); x = (x & 0x33333333) + ((x >> 2) & 0x33333333); x = (x & 0x0F0F0F0F) + ((x >> 4) & 0x0F0F0F0F); x = (x & 0x00FF00FF) + ((x >> 8) & 0x00FF00FF); x = (x & 0x0000FFFF) + ((x >> 16) & 0x0000FFFF);
本の例を以下に拝借させてもらうと、入力に対して以下のステップで x が変化する:
10 11 11 00 01 10 00 11 01 11 11 10 11 11 11 11 | 0xBC637EFF | |
01 10 10 00 01 01 00 10 01 10 10 01 10 10 10 10 | 0x685269AA | |
0011 0010 0010 0010 0011 0011 0100 0100 | 0x32223344 | |
00000101 00000100 00000110 00001000 | 0x05040608 | |
0000000000001001 0000000000001110 | 0x0009000E | |
00000000000000000000000000010111 | 0x00000017 |
一番最後のステップがわかり易い。0x9 と 0xE の足し算は 0x17 である。コードとも直観的に一致するのではないだろうか。
前提として、このアルゴリズムは 32bit 向けで、かつシフト演算器が備わっていないと高速化が見込めない。HP 35s は内部的な演算器は 8bit、シフト命令キーは無しなので割り算で代用する。愚直に HP 35s で実装すると以下の通りになる:
E001 : LBL E E002 : HEX ; ; x = (x & 0x55555555) + ((x >> 1) & 0x55555555); E003 : 55555555h E004 : x<>y E005 : AND E006 : LAST x E007 : 2h E008 : / E009 : 55555555h E010 : AND E011 : + ; ; x = (x & 0x33333333) + ((x >> 2) & 0x33333333); E012 : 33333333h E013 : x<>y E014 : AND E015 : LAST x E016 : 4h E017 : / E018 : 33333333h E019 : AND E020 : + ; ; x = (x & 0x0F0F0F0F) + ((x >> 4) & 0x0F0F0F0F); E021 : 0F0F0F0Fh E022 : x<>y E023 : AND E024 : LAST x E025 : 10h E026 : / E027 : 0F0F0F0Fh E028 : AND E029 : + ; ; x = (x & 0x00FF00FF) + ((x >> 8) & 0x00FF00FF); E030 : 00FF00FFh E031 : x<>y E032 : AND E033 : LAST x E034 : 100h E035 : / E036 : 00FF00FFh E037 : AND E038 : + ; ; x = (x & 0x0000FFFF) + ((x >> 16) & 0x0000FFFF); E039 : 0000FFFFh E040 : x<>y E041 : AND E042 : LAST x E043 : 10000h E044 : / E045 : 0000FFFFh E046 : AND E047 : +
HP 35s でのプログラミングもだいぶ慣れてきたのか、今回は最初から脳内アセンブル & 直接入力し、1発で動いてくれた。4行目でわざわざ x レジスタと y レジスタを交換しているのは、6行目でオリジナルの x を LAST x で取り出すためである。こうすると STO/RCL 命令を使わずに済む。21行目等、16進数のアルファベットを入れるには EQN キーを押し、RCL F とすれば良い。実行するには
BC637EFFh XEQ E ENTER
とすれば良い。ちゃんと 17h が出力された。
さて、気になるのは実行パフォーマンスである。この実行には4秒ほどかかった。HP 35s には HP 16C とは異なりシフト命令キーが無く、割り算をしているのも遅い要因と思ったが、プログラムを使わず普通の電卓として複雑な割り算をしても全く遅く感じない。
内部的にどのような処理になっているのか気になったので少し調べてみたところ、CPU は MOS 8502 というやつで、6510 と命令セットは同じらしい。6510 のデータシートを見てみた。
http://archive.6502.org/datasheets/mos_6510_mpu_nov_1982.pdf
HP 35s の第一回目の記事で「裸のコンピュータ」と書いたが、どうやらそれは誤りのようだ。データシートを見たところ、HP 35s で用意されている"命令"は 6510 のそれに比べると相当リッチである。一見低レベル風な命令が多いのでてっきりアセンブラだと思っていたが、HP 35s で動くプログラムは立派なインタプリタである。
命令セットは以下のほうがわかり易い。
http://www.e-tradition.net/bytes/6502/6502_instruction_set.html
各命令にサイクル数も載っており、例えば SP ( スタックポインタ) の更新には 2 サイクル、スタック領域への値書き込みに 3 サイクル、読み込みに 4 サイクル。スタックとは別に用意されているメモリアクセスはアドレッシング方式により最大 7 サイクルかかる。絶対ジャンプに必要なサイクル数は 3 で、パイプラインや命令キャッシュなんて無いのであろうから、少ないコストで動くのだろう。
HP 35s と 6510 の命令比較の一例を挙げると、HP 35s の ISG( インクリメント後にループ制御変数の値を評価し、条件により次の命令をスキップ) は 6510 には無い命令で、6510 には単純な INC( インクリメント ) のみが用意されている。ISG は INC の実行後に小数部を取り出して CPX/CPY ( 比較 ) 命令を追加実行し、フラグを評価後に「インタプリタ」プログラムの PC を変更するのであろう。尚、I/(I), J/(J) によるアドレッシングは 6510 の X, Y インデックスレジスタによるものだろうが、これらのレジスタは 8bit なので、どのように 35s のアドレス空間にマッピングしているのかは謎である。ページ毎にアクセスする命令を置いておいて、X-indexed で調整しているのだろうか。35s でアクセス可能なアドレス空間は「未初期化」状態というものが存在することから、何かしらの管理領域が存在することは確かと思われる。
RPN で利用する4段のスタックもハードウェア由来のものと思ったが、計算に使えるレジスタは 8bit のアキュムレータ 1 つで、ソフト的に実装されているものだ。6510 のスタック領域は 256 Byte で、RPN スタック領域はこの領域を使っているのかもしれない( 35s で表現可能な数値は 36bit で、3次元ベクトルを扱う場合を考えるとスタック1段あたり 108 bit、4段 + Last X の分で 68 Byte 使う計算となる。35s におけるサブルーチン呼び出しは最大 20 段と規定されており、RTN ( Return ) で戻るアドレスに 2 Byte 消費すると見積もると 40 Byte。残りの領域は、アルファベットの変数に割り当てているのか、または管理領域として使っているのであろうか )。
もはやプログラミングより内部構造のほうに興味が移りつつあるが、もう少しこの電卓で遊んでみようと思う。
HP 35s でプログラミング - 3
前回 RTA に必要な ceil() が実装出来たので、RTA 本体を実装してみようと思う。
RTA に関する詳細な説明は
https://dspace.jaist.ac.jp/dspace/bitstream/10119/13748/5/paper.pdf
の定義1, 例2に譲るとして、ここでは最低限 RTA の定義とアルゴリズムを簡単に説明するに留める。
RTA は以下で定義される:
C_i, T_j はタスクの実行時間と周期で、予め与えられる。R_i が応答時間であるが、右辺と左辺両方に出てくるので式が成り立つまで R_i の値を変えて試す必要がある(C_i, または C_i + R_i-1 から始め、1づつインクリメントすれば良い)。RTA は NP 困難のクラスに属することが証明されているが、疑似多項式時間で計算可能で非力な HP 35s でもそこそこ動いてくれると信じたいところ。
まずは前述の式を C 言語で実装してみる。
int rta( int i, int Ri ) { int r; int j; r = C[i]; for ( j = 0; j <= (i - 1); j++ ) { r += ceil( Ri / (T[j] + 0.0) ) * C[j]; } if ( r == Ri ) { return Ri; } else { return rta( i, Ri + 1 ); } }
C, T はグローバル変数である。
こいつを
C[0] = 1; T[0] = 5; // タスク 0 の実行時間 = 1, 周期= 5 C[1] = 2; T[1] = 8; // タスク 1 の実行時間 = 2, 周期= 8 C[2] = 3; T[2] = 10; // タスク 2 の実行時間 = 3, 周期= 10 R[0] = rta( 0, C[0] ); R[1] = rta( 1, C[1] ); R[2] = rta( 2, C[2] );
のようなコードで呼び出すと、R[0] = 1, R[1] = 3, R[2] = 7 が得られる。
次に、このコードを HP 35s での実装を意識して、多少変えてみる。再帰を使っているのをループ展開にし、さらに i, j といった HP 35s で特別な意味を持つ変数の使用をやめ、それぞれ y と l に置き換える。Ri は R で使うので、小文字 r は x に置き換えた。
int rta( int y, int Ri ) { int x; int l; while ( 1 ) { x = C[y]; for ( l = 0; l <= (y - 1); l++ ) { x += ceil( Ri / (T[l] + 0.0) ) * C[l]; } if ( x == Ri ) { return Ri; } Ri++; } }
HP 35s では変数 I と J にアドレス(に相当するもの)を代入すると、(I)、(J) でそのアドレス(に相当するもの)の値を読み書き出来る。ループ制御変数に I や J を使うことで配列への高速なアクセスを可能にする、ことを狙ったものと思われるが、どうもループ制御変数の使い勝手が私的にしっくりこなくて、今回はループ制御変数(すなわち ISG, DSE 命令)は使わなかった。
C, T はグローバル変数なので、C を 116 番地?131 番地に、T を 132 番地?147 番地に配置する。これらのアドレスへ、前述の
C[0] = 1; T[0] = 5; C[1] = 2; T[1] = 8; C[2] = 3; T[2] = 10;
を設定するには
; C[0] 116 STO I 1 STO (I) ; C[1] 117 STO I 2 STO (I) ...
とすれば良い。
さて、HP 35s での RTA の実装である。便宜上、ラベルをアルファベットで付けたのと、適宜コメントを入れてある。
A001 : LBL A ; Memory Usage: ; R[] ... 100 .. 115 ; C[] ... 116 .. 131 ; T[] ... 132 .. 147 ; ; function rta( y, Ri ) ;; xreg ... y, yreg ... Ri ; output ; xreg ... Response Time of Task #y ; yreg ... y ; ; register usage : ; Y for target task # ; R for Ri ; D for initial value of C[y] ; X for actual response time calculated from Ri ; L, M for loop control A002 : STO Y A003 : 116 A004 : + A005 : STO I A006 : R_dw A007 : STO R ; A008 : RCL (I) A009 : STO D ; ; while ( 1 ) ; LBL' A ; ; X = C[y] A010 : RCL D A011 : STO X ; ; set L as loop counter ; set M as finish value ( y - 1 ) A012 : 0 A013 : STO L A014 : RCL Y A015 : 1 A016 : - A017 : STO M ; ; retrieve C[] from I, T[] from J respectively A018 : 116 A019 : STO I A020 : 132 A021 : STO J ; ; for ( l = 0; l <= (y - 1); l++ ) { ; LBL' C A022 : RCL M ; Yreg A023 : RCL L ; Xreg A024 : X>Y? A025 : GTO A048 ;LBL' D ; ; x += ceil( Ri / (T[l] + 0.0) ) * C[l]; A026 : RCL R A027 : RCL (J) A028 : / A029 : XEQ B001 ; ceil A030 : RCL (I) A031 : * A032 : X A033 : + A034 : STO X ; ; increment I, J, L A035 : RCL I A036 : 1 A037 : + A038 : STO I A039 : LAST X A040 : RCL J A041 : + A042 : STO J A043 : 1 A044 : RCL L A045 : + A046 : STO L A047 : GTO A022 ;LBL' C ; ; LBL' D ; if ( x == Ri ) { A048 : RCL X A049 : RCL R A050 : X=Y? A051 : GTO A053 ;LBL' E A052 : GTO A061 ;LBL' F ; LBL' E ; A053 : 100 A054 : RCL Y A055 : + A056 : STO I A057 : X<>Y A058 : STO (I) A059 : LAST X A060 : RTN ; LBL' F ; ; increment Ri and retry A061 : 1 A062 : + A063 : STO R A064 : GTO A010 ;LBL' A
計64行だが、チェックサムは 0AB4、LN は 207 になった。正直、ハンドアセンブルとデバッグするのは相当しんどかった。
実行するには、前述した C と T をしかるべき値に設定ののち、引数を Ri, y の順にスタックに積んでやらなければならない。例えば C 言語で
R[0] = rta( 0, 1 );
とやっていたのは、以下のようにして呼び出す:
1 ENTER 0 XEQ A
実行が終了すると 1 が X レジスタに返される。同様に、
R[2] = rta( 2, 3 );
を計算するには
3 ENTER 2 XEQ A
7 が得られるのに4秒ほどかかった。
冒頭で紹介した論文の例2では、4つのタスクセットに対して RTA を求めているが、HP 35s では R_4 の計算に 12 秒ほど。
もともと RTA を HP 35s で実装しようと思った理由は、PC で計算させる際のオーバーヘッドを電卓なら上回ってくれるのでは、という淡い期待があったから。机のスペース(つまりキャッシュである)には限りがあり、もっぱら研究中その上を占めるのは方眼の計算用紙と印刷された論文、教科書等である。HP 35s での、超絶に面倒くさいタスクセットの定義(アドレス116 ?の設定)は、入力ヘルプ用のプログラムを書けばむしろ PC でエディタ編集するよりは早そうだし、何より電卓は小型で場所を取らない。
しかし、肝心の計算に10秒以上はしんどい。計算の開始を R_i-1 + C_i にするとか、R_i の増加を単純な線形ではなくする等、多少の工夫の余地はありそうだが、命令実行が絶対的に遅すぎる。タスクセットが 5 以上の世界では、焼石に水だろう。
HP 35s でプログラミング - 2
ここ数年、本腰を入れて取り組んでいるリアルタイムスケジューリングの研究分野では、与えられたタスクセットがスケジュール可能か判断するための様々な手法が提案されている。シングルコアでの手法は確立されており、RTA = Response Time Analysis というのがそれである。RTA はマルチコアの場合にも応用が出来、私はマルチと並行してシングルの研究も進めているので RTA が手元の電卓で計算出来るとうれしい。HP 35s がどれほどのパフォーマンスを示してくれるか謎だが、とりあえず実装してみることにした。
が、それに先立ち必要なのは ceil() 関数である。HP 35s には整数部と小数部を取り出す命令があるので、それらを使い愚直に実装してみた:
A001 : LBL A A002 : ENTER A003 : FP A004 : x=0? A005 : GTO A011 A006 : X<>Y A007 : IP A008 : 1 A009 : + A010 : RTN A011 : X<>Y A012 : RTN
この処理は外部プログラムより呼ばれるサブルーチンとして動作する前提であるのと、今後も色々と関数を作るであろうからコンベンションを決めることにした。func( a, b, c ) がある場合、c から逆順にスタックに積んで EXQ (実行、またはサブルーチンの呼び出し) する、というものである。スタックに積む順は古典的なコンパイラの例に倣った。
( 余談ではあるが、C 言語では引数の評価順は仕様として定められていない。関数にジャンプした後、スタックから引数を取り出すことを考えると、引数を逆順にスタックに積むほうが効率が良い。しかしこれでは左から右に読む自然記法と評価順が逆になる。このギャップをスルーするための仕様と思われる。引数をレジスタで渡す場合はこの限りではない)
やってることは簡単で、2行目の ENTER で X レジスタに収められている引数を Y レジスタにもコピーする(後続命令が X レジスタを破壊するためである。3行目 FP で小数部を取り出し、X レジスタに積む。4行目で X レジスタが 0 か判断する。すなわち小数部が 0 なら、11行目へジャンプし、コピーしておいた Y レジスタを X レジスタと交換し、戻る。
小数部、すなわち X レジスタが 0 でない場合、X=0? 命令は後続命令を1行スキップする。6 行目で X レジスタと、引数をコピーしておいた Y レジスタと交換する。 7 行目の IP で整数部を取り出し、1 を足して戻る。
この ceil() 関数は完全ではなく、負の入力に対しては正しく動かないので符号のチェックが必要であるが、実は、HP 35s には「与えられた数値以下の最大整数を取得する」、floor() に相当する命令がある。INTG がそれで、これを ceil() に適用するには符号を置き換えてやれば良い。
B001 : LBL B B002 : +/- B003 : INTG B004 : +/- B005 : RTN
2, 4 行目の +/- は X レジスタの数値の符号を入れ替える命令である。これで ceil() が実装出来た。
HP 35s でプログラミング
仕事柄、ちょっとした計算や基数変換をする場面が多く、その度に Windows の電卓や、シェルから expr や、ちょっとしたループを伴う計算には awk を使っていたが、起動のオーバーヘッドもあり不便を感じていた。たまたま寄ったオフィス用品店で目に留まった関数電卓を見て、基数変換くらいは出来ても良さそうだよな、と思い幾つかの製品を手に取りキー配列を見てみると、Base やら A, B, C, D, E, F やらのキーが印刷されているものを発見。CASIO の FX-115ES PLUS という電卓である。18 USD 程だった。
基数変換もばっちりでキーが多い割には直観的に使え、機能的にはこれで十分で満足していたのだが、止せばいいのに関数電卓のことを色々調べるうちにプログラム出来る関数電卓、それも HP の製品が欲しくなってしまった。ギーク友達が逆ポーランド記法の電卓の話をアツく語ってくれていたのを思い出した。たしか HP のものだったはず。というわけで、現在手に入る HP 製の中では手ごろな価格の 35s を購入。日本だと1万円弱ほどらしいがアメリカでは 50 USD 程で手に入った。
1年以上 Lisp/Scheme を使っていなかったが逆ポーランド記法には抵抗感は無いし、キー配列が良く考えられているのか操作もすぐ慣れた。で、目的のプログラミングである。
とりあえず 1 ? 10 の合計を計算するプログラムを作ってみた。
A001 : LBL A A002 : 0 A003 : STO S A004 : 10 A005 : STO A A006 : x=0? A007 : GTO A017 A008 : RCL S A009 : RCL A A010 : + A011 : STO S A012 : RCL A A013 : 1 A014 : - A015 : STO A A016 : GTO A006 A017 : VIEW S A018 : RTN
変数 A がカウンタ、S が合計値である。HP の電卓は4つのスタックを備えており、数字を押すとそれが PUSH される。
( スタックの説明は電卓喫茶http://calculator-cafe.com/readings/4LEVEL_RPN/4LEVEL_RPN.html の記事が詳しい)
STO はスタックの先頭にある値を変数に代入する命令で、RCL は変数に格納されている値を取り出しスタックに PUSH する命令である。8 行目から10行目は、変数 S と A の値をスタックに PUSH し、+ 命令でスタックの先頭から二つの値を POP して足してスタックの先頭に PUSH する、ということが行われている( プログラムでも逆ポーランド記法 )。
その値を 11 行目で S に代入することで、
S = S + A
が計算出来たことになる。
で、このプログラムを最適化してみる。まず、ループ制御用の DSE 命令を使ってみる。( DSE は decrement; skip if less than or equal to の略。インクリメント命令として ISG があるが、こちらは increment; skip if greater than の略。最初は DEC とか INC にすれば良いのになぁ、と思っていたが、マニュアルが無くともキートップの印刷で境界が分かるので、良く考えられたものである )
B001 : LBL B B002 : 10.000 B003 : STO A B004 : 0 B005 : STO S B006 : RCL A B007 : INTG B008 : + B009 : STO S B010 : DSE A B011 : GTO B006 B012 : VIEW S B013 : RTN
DSE, ISG 命令はちょっと癖があり、ループ制御用の特殊なフォーマットの値を適当な変数に代入して使う。2, 3行目の 10.000, STO A がそれで、小数点を境に整数部の 10 が初期値、少数部の 000 が最終値である( 初期値は最大7桁、最終値は 3桁固定という制約がある。尚、最終値は少数ではなく、整数として扱われる)。
A だけを見ると値が少数なので 7 行目の INTG で整数に切り詰めているが、最終値が 0 であればこのステップは不要である。最終値が 1 とか、ISG 命令で最終値が 10 とかだと必要。
実はこのプログラムもまだ無駄が多く、A や S の STO はスタックに値が残っている限り不要である。この処理を除いたのがこれ:
C001 : LBL C C002 : 10.000 C003 : STO A C004 : 0 C005 : RCL A C006 : INTG C007 : + C008 : DSE A C009 : GTO C005 C010 : RTN
こちらでは変数 S はもはや使わず、スタックに格納された合計値を利用している。
さて、ループ制御用の命令に DSE を使ったのは、0 との比較のほうが単純で早そうだから、というのと、ISG では最大で 1000カウンタまでのループしか出来ない、二つの理由による。
試しに以下の二つの実行時間を比較してみたが、DSE が19 秒だったのに対して ISG は21秒だったので、DSE のほうが若干効率は良さそうだ。35s では任意の数字同士の比較命令も用意されているが、MIPS の命令セットの例を出すまでもなく、0 の扱いは効率良く行えるようアーキテクチャが設計されているのだろう。
( D002 を 257.001 にしても、実行時間の増加が認められた )
DSE 版 D001 : LBL D D002 : 256.000 D003 : STO A D004 : 0 D005 : 1 D006 : + D007 : DSE A D008 : GTO D005 D009 : RTN ISG 版 E001 : LBL E E002 : 000.255 E003 : STO A E004 : 0 E005 : 1 E006 : + E007 : ISG A E008 : GTO E005 E009 : RTN
という感じで、アーキテクチャに思いを巡らせながら人力で最適化するといった、裸のコンピュータに直に触るような行為は、フリーで手に入る科学計算ライブラリによって高度な数式が一瞬にして解けてしまう便利な世の中になってしまった昨今、崇高か貴重か、というとそうでは無くて、まぁ完全に遊び、おもちゃ、縛りですよね。そもそも前述の CASIO のように構文解析出来る電卓がある以上、逆ポーランド記法自体が誰得かというとコンピュータ得なので。
とはいえ、HP の電卓にはファンが多い、というのも頷ける、魅力を持ったガジェットであるのも事実。しばらく遊んでみようと思う。
( 自分の研究で出てくる数式を解かせてみたい気もするが、256回のループに19秒かかるようだとちょっと厳しいかな、、、 )