「ローレイヤー勉強会」というイベントでのLT。
HDDを壊さないためにACアダプタにシール貼った
区別のつかないACアダプタ
ACアダプタ、好きですか。私はだいっっっきらい!です(何
ハードウェアごとに別々のACアダプタが必要で、使いたいのはハードウェアなのに大きなACアダプタばかりが部屋を占拠していってしまいますし、何より種類が無数にあるACアダプタの区別が全然付きません。
パナソニックやソニーのような大企業(?)の作っているデバイスでは、ACアダプタを見れば何のハードウェア用なのかすぐに分かるのです。たぶん、このACアダプタならPSPの充電用でしょう、とかね。
しかし、独自のラベルを付けたりせず、ACアダプタのメーカが製造したものをそのまま同封している場合、本当に区別がつかなくなります。このACアダプタは何用のACアダプタでしょう。電圧と電流は分かるのですが…。
こたえ:HDDケース。
…わからないですよね!
ACアダプタ間違えてHDDが壊れた
単純に区別がつかないだけなら、仮にどのデバイスがどのACアダプタか区別が付かなくなっても、トライ&エラーで動くまで試せばよいのですが、それをやると壊れてしまうことがあります…。
今回、HDDのケースに、誤って正しい12vのアダプタではなく、間違った24vのACアダプタを接続してしまったところ、HDDケースは無事だったのですが、HDD本体が壊れてしまいました。
ううう…。保護回路とかは流石に無いんだろうなぁ…。
原始的解決法:シール
一回失敗したら、それはもう仕方がないので「次がない」ように対策しましょう。やはりココは原始的にシール…で…。
メモックロールテープというのがたまたま手元にあったので、これを使いました。テプラとか普通のセロハンテープとかとは違って、べとつかずに綺麗に剥がせるので精神衛生によい気がします、が、もしかするとすぐ取れちゃうかも…(この辺は要検討)
なんか他にいい方法あったら教えてね!!
二条項BSDとのデュアルライセンス。さきゅばす2.0b4を公開しました
久しぶりの更新です。。。。
久しぶりにさきゅばすのアップデートを行いました。またマイナー更新ですけど…。
リリースノート
デュアルライセンスになりました
今回のver 2.0b4のソースから、
- GPL v3 or later
- 2-clause BSD License
のデュアルライセンスになりました。派生物のライセンスはこのどちらか一方か、両方(デュアルライセンスのまま)を選ぶことができます。また、改造されたffmpegに関しては、勝手にBSDにすることはできないので、従来どおりGPL v3のままとなります。
また、GPL v3のライブラリがリンクされているので、配布バイナリも従来どおりGPL v3のままです。
ライブラリの更新とバグの修正を行いました
その他の更新はこんな感じです。
- 2chのスレで見つかってたいくつかのバグを修正しました。
- x264とかffmpeg本体のライブラリの更新を行いました。
- ビルドシステムがcmakeからwafになりました。pythonで書きやすい。
- このためにwafにバグ修正パッチまで投げました…(1.7.10でaccepted)
- 未踏ソフトウェア事業で開発していたもののうち、基礎的な部分をライブラリ「しなもん」として切り出し、そちらを利用することにしました。
- 上記の影響でlibxml2への依存が消えています。icuにも陽には依存しなくなりました(しなもんを通して陰に依存はしています)。
追記
2ちゃんで報告されてた「【第10回MMD杯本選】騒がしいゆーじょー【超遅刻カオス】 」の音ズレ問題ですが、たぶんacodecがaac(ffmpegの内蔵aacエンコーダ)が問題を引き起こしてるのではないかと思います1。外部ライブラリであるlibvo_aacencにすると解決するようです。
今までは変換レシピでaacを使うものがいくつかありましたが、今回のバージョンからlibvo_aacencだけにしました。
- スレに書こうかと思ったらまた規制掛かっててファッ!? [↩]
2012年度未踏事業でスーパークリエイター認定されました
1月まで独立行政法人 情報処理推進機構(IPA)の未踏IT人材発掘・育成事業の支援で「プログラミング言語ど~なっつ」と、それを用いた「ファミコンを題材にした電子教材のような何か」を作っておりましたが、本事業でスーパークリエータに認定されました。わーいわーい(謎
5/29にはスーパークリエータ認定授与式があり、そこで再度プレゼンテーションを行います。でも平日だよ!
今後の展開なのですが、とりあえず時間操作ができるプログラミング言語、ど~なっつの改善をしたいな~と思っておりまして、
- 世界線の分岐
- var式を作って現在のオフサイドルール廃止
- ニワン語処理系「ねこまた」との統合
とかやりたいです。特に最後ですが、これが出来ればニワン語の/seekとかにも対応できるし、現状抽象構文木をそのまま評価している「ねこまた」をど~なっつVMへのトランスレータにすることが出来れば、リアルタイム再生も夢でないくらいには高速化できる…かも??
本業(学生)の方があるので支援期間中ほどの速度は出せませんが、ぼちぼち書けていければな~と思います。進学する学科、間違えちゃったかなあ…。
エラー処理の地学史、もしくはあなたがMaybeモナドを使うべき理由。
(以下、カノッサの屈辱(テレビ番組)のノリでお願いします)
コンピュータの理論を確立したチューリング。彼は、無限のテープの長さを持ち、単なる計算を行うだけのチューリングマシンを夢想した。
しかし、我々の生きる現実世界におけるコンピュータでのプログラムに於いては、計算の失敗=エラーが発生するのは避ける事ができない宿命である。ネットワーク接続失敗、ファイルが見つからない、メモリが確保出来なかった…等々。すべてのエラーを書き出すには、それこそ無限の長さの紙が必要であろう。プログラム進化の歴史は例外との戦いであると言っても過言ではない。今回の講義では、プログラミング言語の様々な進化のうちの「エラー処理」に着目し、その長い戦いの歴史を概観する。
まずは、いにしえの先エクセプション紀におけるエラー処理を見てみよう。
先エクセプション紀:エラーコード-1の時代
エクセプション(exception)と呼ばれる隕石が衝突する以前の時代は先エクセプション紀と呼ばれている。この時代での例外処理の特徴は、エラーコードという概念の存在である。数学の関数を模したプログラム言語の「関数」とよばれる生物は、値を一つだけ戻すことができる。当時地球を支配していたAPI関数たちはこの「戻り値」を用いてエラーが発生した旨を伝えることで、上位のプログラマにその処理を託した。
//int readNumberFromFile(const char* filename); // ファイルを読んで、そこに書かれた数字を返す。失敗した場合は-1を返す。 // ex) number.txtに"12"と書かれていれば、12を返す。 int status = readNumberFromFile("number.txt");
これがこの時代における典型的な「失敗する可能性のある関数」の姿である。戻り値として計算が失敗した場合に特別な値を返すことでその旨を表現し、多くの場合、-1などマイナスの値が好んで使われた。これを「エラーコード」と呼ぶ。
しかし、上記の例を見れば分かる通り、正常な結果と失敗した結果を同じint型の変数一つで表してしまうことで、いくつか問題が発生した。例えば、上記のプログラムでは、もしもファイル中に「-1」と書かれていた場合、このAPI関数は「正常な結果として」-1を返してしまうかもしれない。プログラマにはそれを区別する事ができない。必ず非負の数値が書かれていることを要請し、マイナスの場合はすべてエラーとすれば問題は解決するが、APIに要求とされる機能によってはそれは許されないかもしれない。そのような場合、このように苦肉の策として追加のポインタを使うケースがあった。
// ファイルを読んで、そこに書かれた数字を返す。 // 失敗したか否かの情報がstatus内に格納され、失敗したら-1が格納される。 // int readNumberFromFile(const char*filename, int* status); int status = 0; int result = readNumberFromFile("number.txt", &status); if(status == -1)....
このポインタを用いて間接的に値を返す手法によって、プログラマに結果の値を一つしか伝えることができない、という関数の問題を回避することができた。しかし、この方法には明らかに欠陥がある。「処理が失敗したか否か」も、「成功した場合のその結果」もどちらも「関数から返ってくる値」のはずであるが、この方法ではその対称性が崩れてしまっており、あたかもstatusは入力値のようにも見えてしまっている。また、どちらをポインタとして渡して格納させるかはライブラリごと、関数ごとに違い、勘違いして双方を入れ替えてしまう悲劇も発生していた。また、Javaなどのように「参照型」を渡すことができない言語においては、この方法を(直接は)使うことができない。
Windowsの一部のAPIやOpenGLなどでは、別の関数を呼びエラーの発生を確認するという方式も考えだされた。
// int readNumberFromFile(const char* filename); int result = readNumberFromFile("number.txt"); int status = getError(); if(status == -1) ....
この方法では、失敗する可能性のあるAPI関数の呼び出しの直後に特定の関数を呼ぶことでエラーの発生を確認した。当然、エラーを確認するまえに別のAPI関数を呼び出してしまうと、エラーを正しく処理することができない。
// int readNumberFromFile(const char* filename); int result = readNumberFromFile("number.txt"); int result2 = anotherFunction(); /* !ここでもエラーが発生するかもしれない ! */ // readNumberFromFileのエラーをもはや反映しない int status = getError(); if(status == -1) ....
保守や機能拡張時などにおいては、その事をすっかり忘れてしまい、明後日なエラーメッセージを前に途方に暮れるという事態も珍しいことではなかった。
また、別の関数をわざわざ呼ばなければならない点はやはり手間が大きく、この方式では輪をかけてエラーが無視されることとなった。
エラーコード-1の亜種として、nullを返すAPIが棲息する地域もあった。Java大陸や、C++海溝の一部などに見られる化石などから明らかになっている。このような関数呼び出しの場合、一見エラーを返さないような計算に見える:
String data = client.readDataFromFile();
dataがnullであれば失敗である事を示し、そうでない場合は成功した結果を表すという方式である。
この方式も、エラーコードと同様、正しい結果としてnullが返ってくる可能性がある場合にうまく対処できない。さらに「null」というただひとつの値しか返すことができないため、結局失敗した理由を他の方法を用いてAPI使用者に伝えなければならないという問題があった。多くのAPIでは実装工数がないの魔法の一声で、その方法は提供しないこともあった。
エラーコードの体系や伝え方(nullを返す・関数を呼ぶ・エラーコードを使う・ポインタを渡す)は当然、APIの創造主や、APIごとにまちまちであり、APIを消費するプログラマはそれらを個別に理解し、正しく対処することが求められた。そしてもちろん、そこには言語処理系のサポートは一切ない。
結果、そのエラー判定コードが正しいことをコンパイラで事前に判定することができないため、プログラマのその日の体調や気分によってはバグが発生することとなった。
エクセプション隕石の衝突
地球上でエラーコードを持つ多様な生物が棲息していたある日、ついに事件は起こった。エクセプション隕石の衝突がそれである。エクセプション隕石は地球上の多種多様な生物に深刻なダメージを与えたが、特に構造化の三要素へのダメージは深刻であった。
当時のプログラミング言語の構文細胞において、順次・反復・分岐の三生物が共生し、細胞小器官となる進化が発生していた。この三つの器官をまとめて「構造化の三要素」と呼ぶ。
/* 順次(上から下に実行) */ int a=10; int b=a*10; /* 反復(繰り返し) */ int sum = 0; for(int i=0;i<100;++i){ sum += i; } /* 分岐 */ if( isOdd(x) ){ printf("odd number!\n"); }
しかし、一部の言語はそれよりも原始的なgotoと呼ばれる器官を退化させることなく持ち続け、APIを消費する一部の生物種は、例外の処理に好んでgotoを用いた。
if( mayFail() == ERROR_FAIL ){ goto fail1; } ... fail1: /* 失敗時の処理 */
エクセプション隕石は、この原始的なgotoと構造化の三要素との悪魔融合合体進化を発生させた。なぜこのような奇妙な進化が起こったのかは、未だに議論のあるところである。この合体進化した新たな器官は、隕石の名前にちなんで「エクセプション」と呼ばれた。
try { int result = mayFail(); int result2 = mayFail2(); .... } catch (Error const& e) { /* 例外時の処理 */ }
エクセプションは構造化の三要素のような姿を持ちつつ、goto文のような文脈を跳躍する特性も有する。APIを消費する上位のプログラマは、エラーの発生があたかも無いかのように処理を記述しつつ、最後にエラーに纏めて対処する事となった。
構造化の三要素とエラーコードを用いたAPI消費の多くのケースでは、ひとつでもエラーが発生した場合、次の処理を行うことは通常不可能であったため、多くのプログラムではエラーが発生したら、すぐにgoto文で失敗時の処理に移るということが見られた。
if( mayFail() == ERROR ){ goro error; } if( mayFail2() == ERROR ){ goro error; } if( mayFail3() == ERROR ){ goro error; } error: return -1;
エクセプションでは、これらをcatch節 という仕組みを使うことで共通化した。すなわち、エラーが発生したら移行の処理は全てスキップし、catch節の中でそのエラーに纏めて対処するということである。
try { int result = mayFail(); int result2 = mayFail2(); int result3 = mayFail3(); .... } catch (Error const& e) { /* 例外時の処理 */ }
エクセプションを持つ言語は地球上で広く繁栄することとなった。エクセプション器官を持つプログラミング言語は、オブジェクト指向と呼ばれる骨格も同時に持つことが多く、オブジェクト指向とエクセプションには関係があると考えられているが、先述の議論から構造化プログラミングとの関係の方が深いという異論も存在する。
しかし、このエクセプションは悪魔融合合体であるがゆえに、黒魔術的弱点を有する。
多くのプログラムでは、構造化の三要素が守られる事を暗黙の前提としてソフトウェアが構築されていくが、エクセプションの仕組みではエラーが発生するとそれがgoto由来の文脈跳躍生によってほぼ無化されてしまう。
またエクセプションの仕組みは処理の流れを変えることに依存しているため、処理の流れが複数存在するスレッド環境などではさらに技巧を用いることが必要とされ、多くの場合それはうまく動作することは無かった。
これらの事に端を発した多臓器不全で亡くなる生物は少なくなく、「More Effective C++」などの当時を研究する古医学書では1章をこのエクセプション由来の疾患に割かれている。
さらに、「で、結局纏めて対処しろって言われてもどうすれば良いんだ…」との声も根強く、catch節には何も書かれない事態も珍しい物では無くなった。これを「例外潰し」と呼ぶ。
エラーコードの復権:地殻内空洞での”数学”との遭逢、そしてMaybe種とEither種
エクセプション隕石はエラーコード種を概ね絶滅させたが、地下3000mではほそぼそと生きながらえる事となった。地下3000mには酸素がほぼ存在しないため、好気性生物であったエラーコード種には進化が求められた。そのための遺伝子を提供したのが、地殻内空洞に棲息する「数学」と呼ばれる、一連の生物群である。
この生物種がエラーコード種に提供したのは、「モナド」と呼ばれる強力な遺伝子である。この特徴を詳しく見てみよう。
エラーコード種に起こった問題である「同じ型で成功と失敗を表されるので、正しい結果の”-1″と失敗を表す”-1″の区別がつかない」という欠点を、値の上位に成功か失敗かを表す仕組み1である「Maybe」や「Either」を加えることで表現。さらに、nullの持つ「失敗としてただひとつの値しか返すことができない」という問題も、失敗時の型にその情報を付加できるようにすることで解決することとなった。
-- Leftが失敗、Rightが成功 data Either a b = Left a | Right b --引数に応じて処理、失敗するかもしれない mayFail :: ArgType -> Either String Result -- ---------------- case mayFail arg of Left a -> ... --aにはエラーの理由が Right b -> ... --bには成功した結果が
思わぬ副作用として、エラーを無視することが出来なくなった。戻り値のMaybe/Eitherから結果を取り出すには、必ずエラーか否かをチェックする必要がある2。
これは今までのプログラミング言語種でも可能であった。Cなら構造体と共用体で、C++ならクラスの仕組みを用いることで実現できる。しかし、それだけではエラーコードかどうかをチェックする今までの方法とあまり変わらず、煩雑なエラーチェックが必要になる。
すなわち、MaybeやEitherの導入だけでは、次のような非常に煩雑な記述が必要になってしまう、ということである。
--mayFailとmayFail2とmayFail3を使った処理 --この三つの関数は、どれも失敗するかもしれないのでEitherを返す。 func = case mayFail arg of Left err -> return (Left "error on maybeFail") Right ok -> case (mayFail2 ok arg2) of Left err2 -> return (Left "error on maybeFail2") Right ok2 -> case (mayFail3 ok2 arg3) of Left err3 -> return (Left "error on maybeFail3") Right ok3 -> return (Right ok3)
この状況に力を与えたのが、かの有名な「モナド」遺伝子である。
モナド遺伝子のbindの力とMaybeやEitherが共生することで、上のような煩雑なエラー処理は次のように至極単純化された。
func = do ok <- mayFail arg ok2 <- mayFail2 ok arg2 ok3 <- mayFail3 ok2 arg3 return (Just ok3)
表層を見ると、エラー処理が全て消え去ってしまっている。エクセプションではエラーを処理するために存在するcatch節が、完全に消滅している。しかし、エラーを「潰した」りはされない。もし、この関数を実行した際にmaybeFail2でエラーが発生した場合、その下位関数のエラー値がそのまま返され、続くmaybeFail3はスキップされる。
-- 途中でエラーが発生すれば、その結果が返される。潰れされない。 func -- -> Left "error occur on maybeFail2" -- 最後まで成功すれば、最後にreturnした値が返される func -- -> Right result
エクセプションにあった、構造化の三要素が無化されてしまう事によって生じうる問題はここでもまだ残っているが、MaybeやEitherは制御構造ではなく、単なる値であるため、エクセプションに比べて柔軟である。例えば、エクセプションを用いたプログラミング言語種では「何度か実行し、成功を試行する」といった仕組みを作るのは一般的に面倒であるが、Maybeを使えばこのように簡単である。
-- 渡された関数を何度か実行し、一度でも成功すればその結果を、 -- n回実行しても失敗するならNothingを返す tryFunc :: Int -> ( Int -> Maybe Result ) -> Maybe Result tryFunc n thunk = if n <= 0 then Nothing else case (thunk n) of Nothing -> tryFunc (n-1) thunk Just x -> Just x
この関数を用いてラップすることで、既存のMaybeを返す関数も簡単に「N回試行」させることができるようになる。
func = do ok1 <- mayFail -- maybeFail2だけは10回だけ試行する。一回でも成功すればよい。 ok2 <- tryFunc 10 mayFail2 .... return (ok3)
このように、「失敗するかもしれない」処理の内側と外側の行き来が非常に簡単になったことで、エクセプションの問題も対処しやすくなっている。
まとめ
- エラーコードでは…
- 正しい結果の-1と、エラーコードの-1を区別するのが困難
- エラーコードのチェックが面倒くさい
- 大体エラーコードのチェックなんてしなくなってくる
- JavaやC++の例外機構では…
- 例外が発生すると以降の処理が全部スキップされてしまうことに起因するバグ
- 例外は潰されるもの
- MaybeやEitherでは…
- エラーを無視できない
- 下位で発生したエラーをそのまま上位に渡すのが簡単
- エラーが発生する処理とその周囲との行き来が簡単
私が現在理解したところによるMaybe/Either/Optionalの利点はこんな感じです。あと文中のHaskellがIOがどこでも起こせるかのような書き方ですが、IOと組み合わせると論点がぼやけそうなんです、ごめんなさい。ツッコミ、お待ちしています。
ちなみに
JavaでもC++でも、匿名クラスやlambda式を多用することでMaybe/Eitherモナドは使うことができます。C++だとこんな感じで実装できます。Javaでもバイトで書いたことがあるのですが、バイトで書いたので手元にコードはありません(ぉ
More Effective C++ ー 読むならC++を何万行も書く前に読みましょう。
有名な「Effective C++」の続編の本です。未踏の期間が終わったこともあり、読む時間を作ることができました。
この本は英語版で、一応日本語版もあるのですが、訳の評判はあまり良くありません。英語版の方も非常に読みやすい英語で、JavaDocレベルの英語が読めればなんとかなると思います。気のきいたジョークに使われる日常単語がいまいち分からなくて全部調べてたので、いまいち笑えませんでしたが…(ーー;
Effective C++に比べて枝葉の話が多い
続編ということもあり、前編であるEffective C++では言語の根本的な部分のと比較して、Moreの方では細かい話が多いです。
前著の方では、例えば
- コピーコンストラクタやデストラクタ、operator =を定義しないと勝手にコンパイラが定義してしまいますよ
- 値渡しよりconst参照渡しを考えた方がいいですよ
- newしたものはdelete、new [] したものはdelete[]、と対応させないといけません。
- private継承や多重継承は注意深く使ったほうがいいですよ
といった内容が多く、他言語を使っていたプログラマがC++の世界を理解するには丁度良かったと思うのですが、今回のMoreの方では、
- 関数の引数で暗黙的な変換が起こるのは値渡しかconst参照渡しの時だけで、しかも変換は一度しか起きません
- 参照カウントなスマートポインタ
- デストラクタは例外が起こってもちゃんと実行されるのでリソースリークを避けるのに使えます。
- 多重継承・virtual継承する時のオブジェクトのデータフォーマットはこうなっている(事が多い)ですよ
といった内容です。最後の項目はvtableやvptrの実態がよくわかったので参考になったのですが、その他の項目は今ではインターネット上のC++に関する文章や、STLをある程度使っていると「そらそうよ…」「せやな…」と思ってしまう内容が多く、Effective C++を読んだ時に感じた、あの驚きをもう感じることはできませんでした。Effective C++を読んで少しC++を書き始めた時期に読めばもっと参考になったのでしょうが、数万行書いた後となると…。
改訂が繰り返されているEffective C++と違い、Moreは95年から一度も改訂されていません。STLは出たばかりの時期だったようです。
本を読むよりソースを読んで書いてWebのリソース読むのが好きな人は、まあ読まなくても…。
この本の中で特に一番大きく扱われているのは、参照カウント・スマートポインタです。sharedされている値そのものが参照カウント数を管理する、boostでいうところのintrusive_ptr相当のものをじっくり実装したあと、階層を一個上げてshared_ptr相当のものを実装します。が、相当のものがある事からも分かる通り、どちらも既に有名なもので、いまいち読んでも驚きがありません。しかもweak_ptrを扱っていませんし、参照カウントで問題になる「自分へのshared_ptrを基本的に持てない」という問題も扱っていません。
もしも読むなら、Effective C++を読んでちょっとC++を書いたらすぐに読むことをお勧めします!
JavaScriptのTypedArrayで高速にmemsetするには
JavaScriptでパフォーマンスを出すことを…強いられるんだ!!!(カッ
で、今回はその関係で色々試したことの一つについて、昔やった結果を纏めたのを書きました。
TypedArray
最近のJavaScriptにはTypedArrayというのがあります。通常のJavaScriptの配列は、要素にどんな型でも入れられる上、厳密には配列ではなくハッシュであるため効率が悪いのですが、このTypedArrayでは配列の型を指定した本物の配列であるため、高速・効率的に処理できます。
もともとはJSでOpenGLを使うWebGLという技術向けに策定された仕様なのですが、別にWebGLでなくても使用できて、以前開発したJSのファミコンエミュレータでも、ファミコンのRAMやフレームバッファをこれで表現しています。
さて、このTypedArray、色々とメソッドが定義されていて、よく使う処理は大体網羅されているのですが、memset(メモリ全体を特定のデータで塗りつぶす)を行うようなメソッドがありません。結構使う処理だと思うのですが、無いのでなんとかJSで塗りつぶさないといけない、という事になります。
今回はTypedArrayのmemset処理について色々と試してみた結果を書きます。
条件
- Let’s note SX-1 SSD Edition CF-SX1GETDR
- Intel Core i5-2540M
- Windows 7 64bit
- Firefox 18 x86
方法1:普通にループで回す
var memory = new Uint8Array(1024*1024); log("normal: "+cycloa.probe.measure(function(){ var m = memory; for(var i=0;i<5000;++i){ for(var j=0;j<1024*1024;++j){ m[j] = i; } } }));
一番遅そうですが、一番オーソドックスな方法です。5000回、1MBのデータを0で埋めてみると、5回試行してそれぞれ
5008ms, 4950ms, 4955ms, 4949ms, 4957ms
になりました。
ということで、5000ms前後がベースラインということになります。これから遅くなったらむしろ逆効果ということでです。
方法2:小さい配列をループで初期化してsetで回す
setというメソッドがあって、これを使うと複数の要素を一気に上書きすることができます。
var mem = new Uint8Array(1024*1024); var another_mem = new Uint8Array(1024); var offset = 100; mem.set(another_mem, offset); -> 100バイト目から1,024バイトがanother_memの内容に上書きされる
さらにこのanother_memの作り方として、上のようにnewで作る方法とsubarrayを使って元の配列を部分的に共有したものを使う方法があります。
ver another_mem = mem.subarray(100,100+1024); -> memの100から100+1024バイト目までを共有したメモリ配列を作
全部ネイティブで0にするより、一旦小さな配列を0で埋めてからsetでセットしたほうが速い気がしませんか。私はそう思いました。
全部JSのループで回すのは遅いというのがわかりましたし、かといってあんまり小さな配列を使って何度もsetを呼び出してもネイティブ呼び出しのオーバーヘッドが重いだろうと思われるので、このバランスをどうするかが問題になります。今回は、1KBから1MB(意味なし)まで変化させて測定しました。
new Uint8Arrayで配列を作ってコピーする方法
for(var z=10;z<=20;++z){ log(""+cycloa.probe.measure(function(){ var m = memory; var bsize = 1<<z; var copy_times = 1024*1024/bsize; var buff = new Uint8Array(bsize); for(var i=0;i<5000;++i){ for(var j=0;j<bsize;++j){ buff[j] = i; } for(var j=0;j<copy_times;++j){ m.set(buff, j*bsize); } } })+","); }
1 | 2 | 3 | 4 | 5 | 平均 | |
1024 | 557 | 559 | 547 | 553 | 543 | 551.8 |
2048 | 399 | 397 | 395 | 399 | 398 | 397.6 |
4096 | 340 | 343 | 340 | 342 | 342 | 341.4 |
8192 | 322 | 319 | 319 | 328 | 331 | 323.8 |
16384 | 367 | 369 | 373 | 369 | 372 | 370 |
32768 | 477 | 480 | 478 | 483 | 482 | 480 |
65536 | 630 | 630 | 631 | 632 | 638 | 632.2 |
131072 | 919 | 928 | 926 | 929 | 939 | 928.2 |
262144 | 1598 | 1583 | 1584 | 1589 | 1619 | 1594.6 |
524288 | 2854 | 2831 | 2821 | 2834 | 2935 | 2855 |
1048576 | 5624 | 5396 | 5326 | 5437 | 5418 | 5440.2 |
subarrayを使って小さな配列を作る方法
for(var z=10;z<=20;++z){ log(""+cycloa.probe.measure(function(){ var m = memory; var bsize = 1<<z; var copy_times = 1024*1024/bsize; var buff = m.subarray(0, bsize); for(var i=0;i<5000;++i){ for(var j=0;j<bsize;++j){ buff[j] = i; } for(var j=1;j<copy_times;++j){ m.set(buff, j*bsize); } } })+","); }
1 | 2 | 3 | 4 | 5 | 平均 | |
1024 | 640 | 564 | 553 | 565 | 557 | 575.8 |
2048 | 417 | 404 | 401 | 405 | 404 | 406.2 |
4096 | 343 | 348 | 342 | 347 | 341 | 344.2 |
8192 | 319 | 320 | 319 | 320 | 318 | 319.2 |
16384 | 369 | 377 | 367 | 362 | 369 | 368.8 |
32768 | 468 | 471 | 470 | 467 | 465 | 468.2 |
65536 | 611 | 614 | 610 | 610 | 607 | 610.4 |
131072 | 891 | 882 | 873 | 876 | 888 | 882 |
262144 | 1516 | 1502 | 1498 | 1493 | 1520 | 1505.8 |
524288 | 2678 | 2653 | 2652 | 2667 | 2658 | 2661.6 |
1048576 | 4993 | 4934 | 4922 | 4972 | 4923 | 4948.8 |
グラフ
new Uint8Arrayで配列を作ってコピーする方法
subarrayを使って小さな配列を作る方法
やっぱり小さな配列を作ったほうが速い
と、いうわけで、8192くらいの配列を作ってsetで回したほうが大体17倍くらい速いです。subarrayの方が配列生成コストが変わってくるのかなぁ、と思ったのですが、あんまり変わらないようですね。
ただこの8192という数字は2013年のFirefox18のWindows7でCore i5(Ivy Bridge)というひっっっっっっじょうに限られた環境での値なので、あんまり鵜呑みにしないほうがいいと思います。もし、JSの実行速度がネイティブ関数呼び出しのコストに対して相対的に速い環境があればこの値は大きくなるかもしれませんし、ネイティブ関数呼び出しのコストがJS実行速度よりも相対的に安い環境があれば、この値は小さくなってくると予想されます。
結論
- HTML5のTypedArrayでは、メモリの内容を特定の値で塗りつぶすとき、subarrayでも新しくつくるのでも、他のTypedArrayを用意してそれを特定の値で塗りつぶしてからsetを繰り返す方がとても速い
- 8192bytesの配列を用意すると一番速かったけど、他の環境では分からない。
- ターゲットの環境で調べれば良いのでは(クロスプラットフォームを無視した言い方)
JSは大変ですね…。
Fedora18にあげたら.xmodmapが読み込まれ無くなった
こんにちは。最近なぜかFedora17が動かなくなった(えっ)ので、Fedora18にアップデートしました。
私は~/.Xmodmapにこんな内容を置いて、キーボードのCtrlとShiftを入れ替えたりZenkaku_HankakuをEscapeにしたりしているのですが、
keysym Zenkaku_Hankaku = Escape remove Lock = Caps_Lock keysym Caps_Lock = Control_L add Control = Control_L
Fedora18にアップデートしたところ、うまく動かなくなってしまいました。それを今回対策したので、メモっておきます。
原因は?
どうやら大本の原因は、どうやらgnome3.6になってから文字入力を担ってるあたりがgnomeに統合されたのが問題の原因(?)らしいです 参考
直接的な原因はxmodmap ~/.Xmodmapが自動で実行され無くなったことで、何もせずともコンソールで自分で
% xmodmap ~/.Xmodmap
って入力して実行すると今までどおりちゃんと設定が反映されるようになってるはずです。というわけで、これを自動で実行するようにしましょう。
gnome-session-propertiesを起動する
コンソールからgnome-session-propertiesを実行します。
% gnome-session-properties
そうすると、自動的に実行されるプログラムの一覧が表示されるので…
Addを押して、新しいエントリを追加し、
/usr/bin/xmodmap ${YOUR_HOME}/.Xmodmap
と入力してSaveして、再ログインすれば反映されるはずです。
ここで重要なのが、xmodmapを絶対パスで指定しておくことです。どうやらPATH変数がうまく設定されてないのか、xmodmapとだけ書いても動きません。
余談:SSH key agentはここで実行されている
他のエントリを見てみると中々面白いです。ssh key agentとかどこで実行されてるのか割と不思議だったのですが、この中だったのですね。
Linux版Dropboxのクライアントなどもここで実行されています。
.bash_profileとかはターミナルを起動した瞬間でないと自動実行されない(たしか)ので、Xからログインしてすぐに発動させたいコマンドはここを使うと良さそうです
GoogleTestでマクロを使ってテストを自動生成するバッドノウハウ
C++のテストフレームワークのGoogleTest、活用してますか!!私は大好きです!!(何
テストフレームワークを使ってると、マクロを使って似たようなテストをまとめたくなる時があると思います。前後のセットアップだけ同じで型名とテストする値だけが違うとか。できればテンプレートを使ったほうがいいと思いますが、そうもできない場合もあるでしょう。
#define _MY_TEST(TYPE, VALUE, EXPECTED) \ { /* plus */\ TYPE result;\ ASSERT_NO_THROW( result=do_sth<TYPE>(VALUE) );\ ASSERT_EQ( EXPECTED, result );\ }\ { /* minus */\ TYPE result;\ ASSERT_NO_THROW( result=do_sth<TYPE>(-VALUE) ); /* マイナス */\ ASSERT_EQ( EXPECTED, result );\ } TEST(MyTest, TEST_A) { _MY_TEST(int, 0x7fffffff, 0x80000000); // do_sthは何するんだろう… _MY_TEST(unsigned short, 0x7fff, 0x8000); ... }
でも、コレは動かないのです。こんなエラーメッセージが出ちゃいます。
****.cpp: In member function ‘virtual void MyTest_TEST_A_Test::TestBody()’: ****.cpp:719:1140: error: duplicate label ‘gtest_label_testnothrow_719’ ****.cpp:720:1171: error: duplicate label ‘gtest_label_testnothrow_720’
さて…GOTOのラベルがかぶっているようです。なんでなのかは、ASSERT_NO_THROWの先を見ればわかります。
#define GTEST_TEST_NO_THROW_(statement, fail) \ GTEST_AMBIGUOUS_ELSE_BLOCKER_ \ if (::testing::internal::AlwaysTrue()) { \ try { \ GTEST_SUPPRESS_UNREACHABLE_CODE_WARNING_BELOW_(statement); \ } \ catch (...) { \ goto GTEST_CONCAT_TOKEN_(gtest_label_testnothrow_, __LINE__); \ } \ } else \ GTEST_CONCAT_TOKEN_(gtest_label_testnothrow_, __LINE__): \ fail("Expected: " #statement " doesn't throw an exception.\n" \ " Actual: it throws.")
というわけで、GOTOのラベルに__LINE__マクロで取得した行数を使っているのですが、関数風マクロで展開するソースは一行で展開されるので、先ほどの_MY_TESTの複数のASSERT_NO_THROWで同じ__LINE__が使われてしまい、GOTOのラベルがかぶるのでさっきのようなエラーでコンパイルエラーになってしまったのでした。
むー。困りましたね。
マクロ内でマクロのディレクティブは使えない
たとえばこんな事はできません。
#define _MY_TEST(TYPE, VALUE, EXPECTED, x) \ { /* plus */\ TYPE result;\ \ #line (x*1000) /*lineディレクティブで書き換えてしまえばよいのでは!? */\ ASSERT_NO_THROW( result=do_sth<TYPE>(VALUE) );\ ASSERT_EQ( EXPECTED, result );\ }\ { /* minus */\ TYPE result;\ #line (x*1000)+1 /*上とは違う番号を使っている */\ ASSERT_NO_THROW( result=do_sth<TYPE>(-VALUE) ); /* マイナス */\ ASSERT_EQ( EXPECTED, result );\ }
#lineというディレクティブを使うと__LINE__での行番号がこの行以降差し替わるので、これで変えてあげればかぶらなくなるのでは?と思ったのですが、やはり一行に展開されてしまうので、このように展開されてしまい…
{.... #line (x*1000)+1 ASSERT_NO_THROW(.... /*←改行なし*/
これでは文法エラーにしかなりません。
仕方ないのでファイルを分ける
何かいい方法無いのかなーと考えていたのですが、プリプロセッサには関数風マクロ以外にもソースの繰り返しを行う方法がありました。includeです。
さっきのマクロの内容そのものの別ヘッダファイルを作り…
/* inc.h */ #define _MY_TEST(TYPE, VALUE, EXPECTED, x) { /* plus */ TYPE result; ASSERT_NO_THROW( result=do<TYPE>(VALUE) ); ASSERT_EQ( EXPECTED, result ); } { /* minus */ TYPE result; ASSERT_NO_THROW( result=do<TYPE>(-VALUE) ); /* マイナス */ ASSERT_EQ( EXPECTED, result ); }
それをincludeして繰り返しすれば…
TEST(MyTest, TEST_A) { #define TYPE int #define VALUE 0x7fffffff #define EXPECTED 0x80000000 #include "inc.h" #undef TYPE #undef VALUE #undef EXPECTED }
別ファイル内でのgoto扱いになるので、さっきのエラーは出なくなります。若干不恰好ですが、手動で繰り返すよりは…。
できればテンプレートを使う方が良いと思う
マクロではなくテンプレートを使ってテストをパラメータ化すれば、このような問題は起きません。テンプレートではマクロの##(シンボル連結)とか#(シンボルの文字列化)みたいな機能は使えませんが、もっと安全だし楽なので、出来ればテンプレートで出来ないか先に考えるほうが良いと思います。。。
FirefoxでJITコンパイルの「正しさ」を担保する”Invalidation”
なかなか感動した次の記事を翻訳しました。翻訳とか初めてなので、意訳とかがところどころあれかも知れません。
The Ins and Outs of Invalidation | JavaScript
Firefox18で追加されたJITコンパイラ「IonMonkey」で、どうやってJITコンパイル時の型についての仮定を守りつつ、効率的にJITコンパイルを行うかの話です。「型推論w どうせJSのJITコンパイルとか、『大体Intが来るっぽいからInt向けにコンパイルしとこう』、みたいな感じでしょ?w」って思ってませんか?もっともっと、凄まじいですよ。
この記事のライセンスは元記事と同じ、「Creative Commons Attribution Share-Alike License v3.0 or any later version.」となります。
The Ins and Outs of Invalidation
動的言語のJITエンジンの主な目的の一つは、高水準な言語を効率的な機械語にコンパイルすることだ。しかしながら、コンパイルされたコードはすべて、元のJavaScriptのコードがインタプリタで実行されたときと同じように振舞わなければならない。つまり、コンパイルされたコードの「正しさ」を保つことがJITコンパイラに求められる基本的な問題なんだ。
しかし、効率的かつ正しいコードを生成するのは難しい。なぜなら、JavaScriptは非常に高い多態性を持つからだ。すべての変数はどんなタイプの値でも持つことができるし、一般的に言って実行時にどんな型の値が代入されるかを事前に知ることは出来ない。たとえば、常に2つのintegerを足し合わせるようなコードがプログラム上のどこかにあったとしても、実行エンジン自体はintegerじゃない値が来ることを許さなければならないんだ。
この問題に対処するためのテクニックが、大まかに言って2つある。ひとつは「guarding」だ。実行エンジンが実行前に型について仮定したことが守られているか実行時にチェックするようなコードを生成するんだ。さっきの例で言えば、JITコンパイラは2つのintegerを足し合わせる機械語を生成するけど、そのコードが実行される前に本当にそれらの2つの値がintegerであることを確かめるような機械語も追加する。もしそのチェックが失敗してしまったら、機械語は処理を明け渡し、稀なケースを処理するための、汎用的で遅い方法で実行を続ける。もしチェックが成功すれば、高速な(あるいは最適化された)機械語が実行される。このチェックは大体成功するから、速い方法がたいていは実行される。
このguradingはよい方法だけど、パフォーマンス上のペナルティがある。仮定が守られてるかのチェックには時間が掛かるし、何度も実行されるコードではそれがパフォーマンス上の重大な損失になってしまう。もうひとつの効率的なコードを生成しつつ正しさを守る方法は、invalidationだ。それが今回の記事で私が話したいこと。これからSpider Monkeyの新しいJITであるIonMonkeyを例として使うよ。でも、細かい点を除けば全体的な戦略は殆どのJITに適用できる。
Invalidation at 10,000 Feet
Invalidationの鍵となるアイデアは「毎回仮定が守られているかチェックする機械語を生成する代わりに、その仮定が正しく無くなったら生成した機械語を無効化(invalidate)してしまう」ということだ。ただし無効化された機械語が決して実行されないようにするために、型に対するガードとは別のガードを追加しないといけない。まずは簡単な例を見てみよう。ここにいくつかの点の間の距離の和を求める、簡単なJavaScriptプログラムがある。
function Point(x, y) { this.x = x; this.y = y; } function dist(pt1, pt2) { var xd = pt1.x - pt2.x, yd = pt1.y - pt2.y; return Math.sqrt(xd*xd + yd*yd); } function main() { var totalDist = 0; var origin = new Point(0, 0); for (var i = 0; i < 20000; i++) { totalDist += dist(new Point(i, i), origin); } // The following "eval" is just there to prevent IonMonkey from // compiling main(). Functions containing calls to eval don't // currently get compiled by Ion. eval(""); return totalDist; } main();
このプログラムを実行するとき、dist関数は大体10000繰り返し実行された後、IonMonkeyによってコンパイルされる。その時の中間言語(intermediate representation; IR)はこれだ。
[1] Parameter 0 => Value // (pt1 value) [2] Parameter 1 => Value // (pt2 value) [3] Unbox [1] => Object // (unbox pt1 to object) [4] Unbox [2] => Object // (unbox pt2 to object) [5] LoadFixedSlot(x) [3] => Int32 // (read pt1.x, unbox to Int32) [6] LoadFixedSlot(x) [4] => Int32 // (read pt2.x, unbox to Int32) [7] Sub [5] [6] => Int32 // xd = (pt1.x - pt2.x) [8] LoadFixedSlot(y) [3] => Int32 // (read pt1.y, unbox to Int32) [9] LoadFixedSlot(y) [4] => Int32 // (read pt2.y, unbox to Int32) [10] Sub [8] [9] => Int32 // yd = (pt1.y - pt2.y) [11] Mul [7] [7] => Int32 // (xd*xd) [12] Mul [10] [10] => Int32 // (yd*yd) [13] Add [11] [12] => Int32 // ( (xd*xd) + (yd*yd) ) [14] Sqrt [13] => Double // Math.sqrt(...) [15] Return [14]
上のコードは本当のコードじゃなくて、分かりやすくしたものだ。本当の、詳しい低レベルのコードが見たかったら、ここをクリックしてね。
命令の[3]と[4]で、Ionは引数をオブジェクトへのポインタであり、プリミティブでない事を一切チェックすることなく、値からオブジェクトのポインタを取り出し(unbox)てしまう。命令[5][6][8][9]では、オブジェクトからロードしたxとyをInt32に変換してしまう。
(注意:”unbox”っていうのは、この場合は汎用的なJavaScriptの値を、生の具体的な値、例えばInt32とか、Doubleとか、Objectポインタとか、Booleanとかにデコードすることだ。)
もし私達が仮定が守られていることをチェックするgurdingを使っていたら、次の余計なチェックがコードに現れていただろう。
- 2つの型チェック:[3][4]で生の値を取り出す前に「引数がオブジェクトポインタであること」を確かめる追加のチェック
- 4つの型チェック:pt1.x, pt2.x, pt1.y, and pt2.yがすべてInt32であることを確かめるチェック
でも、このIRではこの6つのチェックをスキップしていて、とても詰まったコードになっている。ふつうなら、こんなことは出来ない。もしこの仮定が守られなかったら、コードは「間違った」ものになってしまうからだ。もしdist関数がオブジェクトでない引数で呼ばれてしまったら、このJITコードはクラッシュしてしまう。もし、distがxとyのInt32プロパティをもつPointのインスタンスを引数にして呼ばれなかったら、やっぱりクラッシュしてしまう。
この生成された効率的なコードを実行を正しさを保ちつつ使うために、仮定が無効(invalid)になったら実行しない事を徹底しないといけない。そのために、SpiderMonkeyの型推論システムが必要だ。
型推論
SpiderMonkeyの型推論(type inference; TI)は動的にJavaScriptのソースとオブジェクトの型情報と、コードの所々でどういったタイプが現れるかを追跡する。このTIによってメンテナンスされてアップデートされていくデータは、プログラムについての型の知識を表している。そのデータが変更された時、実行エンジンは特定の(型に対する)仮定が壊れてしまっていないかをチェックするトリガーを発動させる。
次の図は、先述までのプログラムの、非常に単純化した型モデルに関する略図だ。
上のTypeScriptはdist関数に関する型情報を持っている。一つ目と二つ目の引数(pt1とpt2)に関連付けられた型の集合などだ。
下のTypeScriptは、Pointに関連付けられた型の情報を持っている。xとyのフィールドに関連付けられた型の集合などだ。
このデータはTIによって必要に応じてアップデートされる。たとえば、dist関数がPointのインスタンスでない引数に対して呼ばれたとすると、TIはTypeScript上の引数に対する型集合を正しくアップデートする。同様に、もしPointがInt32以外のxとyで出来たインスタンスが作られたら、TypeScript上のフィールドに対する型集合は正しくアップデートされる。
Ionが関数をJITコンパイルして、型に関する暗黙の仮定を行う時、無効化フック(invalidation hook)を適切な型集合に対してセットする。こんな感じ:
新しい型が型集合に追加されたときはいつでも、関連した無効化フックが実行され、JITコンパイルされた機械語は無効化される。
Ionで実験
上のコードを書き換えて、実験的にこれらの無効化を発生させてみよう。私はこれから、Mozillaのソースからビルドされた、スタンドアローンのJavaScriptシェルを用いる。あなたも自分でJavaScriptシェルのデバッグバージョンをビルドすることで、以下の実験結果を実際に試すことができる(詳しくはthe SpiderMonkey build documentationを見て!)。
はじめに、簡単なのからやろう。上にスクリプトを実行して、無効化が一切起こってない事を確かめるんだ。
$ IONFLAGS=osi js-debug points.js [Invalidate] Start invalidation. [Invalidate] No IonScript invalidation.
(これから出てくる、余計な嘘の「Start invalidation」は無視してほしい。これはガベージコレクションの開始によるもので、GCによって起こされる可能性のある無効化を実行エンジンがチェックするものだ。今回の記事とは関係ない)
環境変数にIONFLAGS=osiを渡すことで、Ionにすべての無効化や関連するイベントの発生を実行中にコンソールに出力させることができる。出力が示すように、このプログラムでは一切無効化が起こっていない。何一つとして型に関する仮定がコンパイルされた後に壊されていないからだ。
例 2
二番目の実験として、「dist関数の引数はすべてPointのインスタンスである」という仮定を壊して、何が起こるか見てみよう。これが新しいmain関数だ。
... function main() { var totalDist = 0; var origin = new Point(0, 0); for (var i = 0; i < 20000; i++) { totalDist += dist(new Point(i, i), origin); } dist({x:3,y:9}, origin); /** NEW! **/ // The following "eval" is just there to prevent IonMonkey from // compiling main(). Functions containing calls to eval don't // currently get compiled by Ion. eval(""); return totalDist; } ...
このプログラムでは、ループ内でdist関数を何度も呼び出して「ホット」にすることで、distがJITコンパイルされるようになっている。また、dist関数に渡される引数はループ内では常にPointのインスタンスであるので、「この関数は必ずPointのインスタンスに対して実行される」という仮定でコンパイルされることになる。ループが実行されたら、もう一度dist関数が呼ばれる。しかし、今度は最初の引数はPointのインスタンスではないので、JITコードでの仮定が壊されてしまう。
$ IONFLAGS=osi js-debug points.js [Invalidate] Start invalidation. [Invalidate] No IonScript invalidation. [Invalidate] Start invalidation. [Invalidate] Invalidate points.js:6, IonScript 0x1f7e430
(もう一度言うけど、最初のStart invalidationはGC実行によって引き起こされたもので、今回の記事とは無関係)
おお!dist関数(points.js, line 6)に対しての無効化を起こせたね!すばらしいことだっ。
下の略図は何が起こったのかを示している。dist関数の最初の引数に対するTypeScriptが変更され、新しい型のTypeObjectに対する参照が追加されている。これが無効化フックを発動させ、IonScriptが無効化された:
例3
3つ目の例として、Pointのインスタンスが必ずint32の値を持っている、という仮定を壊してみよう。今度のmainはこんな感じ:
... function main() { var totalDist = 0; var origin = new Point(0, 0); for (var i = 0; i < 20000; i++) { totalDist += dist(new Point(i, i), origin); } dist(new Point(1.1, 5), origin); /** NEW! **/ // The following "eval" is just there to prevent IonMonkey from // compiling main(). Functions containing calls to eval don't // currently get compiled by Ion. eval(""); return totalDist; } ...
それで、これがその結果だ:
$ IONFLAGS=osi js-debug points.js [Invalidate] Start invalidation. [Invalidate] No IonScript invalidation. [Invalidate] Start invalidation. [Invalidate] Invalidate points.js:6, IonScript 0x1f7e430
Invalidation vs. Guarding
Invalidationとguradingは与えられた操作に対してJITコードを生成する、はっきり異なったアプローチだ。与えられた任意の関数に関してJITコードを生成することについて、どちらのテクニックが使われたとしても、どちらかが必ず優っているわけではない。
今回のInvalidation-basedな最適化のアドバンテージは、仮定が守られているかの毎回のガードがなくなることでhotなコードをかなり速く実行できることだ。しかしながら、もちろん欠点もある。仮定が壊れたとき、その壊れた仮定に頼っているJITコードはすべて無効化され、実行できなくなってしまう。これはつまり、新しい仮定の元で再度JITコンパイルされるまで、遅いインタプリタで実行することになる。もし不幸にも頻繁に型に関する仮定が壊されてしまうような環境でInvalidation-basedな最適化を行なってしまった場合、悲しい事に逆に高い実行時コストがかかってしまう。
Gurad-basedな最適化であれば、生成されるコードのパフォーマンスを犠牲にするかわりに、仮定の変更につよい機械語が生成される。仮定が壊れてしまってもJITコードを捨てなくてもいい。その場合、単に遅い方法で実行されるだけだ。無効な仮定で関数が呼ばれる回数が少なければ(でも0よりは大きければ)、gurdingはinvalidationによる方式よりもよい選択だ。
どちらの方式を使うべきかという問題には簡単に答えることはできない。仮定がたびたび壊れて、高いコストを払って再コンパイルされるとしても、何度も何度も実行されるのでInvalidation-basedな最適化の方が速いかもしれない。もちろん、そうでないかもしれない。どこかの点を超えると、この2つのアプローチのどちらを選ぶかは経験とチューニングの問題となるだろう。
On-Stack Invalidation
今回の記事では無効化の基本的な原理に関する一般的な考え方を書いた。とはいうものの、すべてのJITエンジンが気をつけないといけない、たまにしか起きないが非常に重要なケースがある。
それはon-stack invalidationと呼ばれるものだ。それはコールスタック上にある関数(=今実行中の関数を呼び出している関数)がちょうど無効化されてしまうケースだ。これはつまり、今の関数の実行が終了した後、すでにもう安全に実行できない機械語の実行に戻ろうとする、ということだ。
このケースにはすこし曲芸めいた対処が必要で、発生する可能性のある沢山の捉えづらいエラーを考慮する必要がある。続く記事でこれらに関してさらに書くことにする。
感想
なんというか凄まじいですね。ここまで型を追跡してるとは…。ちなみに最後の「これらに関してさらに書くことにする」の記事は現状ポストされてないようです。読みたいなぁ。
無効化の仮定が壊れてしまったとしても、別の型に対する新しいJITコードを作って使い分ける、とか出来ないですかね。例えば、例として出てきたdist関数ですが、int32に対してJITコンパイルしたバージョンと、floatに対してコンパイルしたバージョンの2つを作って、使い分けるとか。C++のテンプレートみたいな感じ?ここまで徹底的に型を追跡してるなら、やれてもおかしく無い気がします(適当な事を言っています)。
この仮定が壊れてしまった事のメッセージを、ブラウザで書いてる最中からも見れると嬉しいのですが…。上のdistでdoubleが指定されるのはたぶん仕様の範囲内ですが、例えばファミコンエミュレータでint32ではなくfloatで実行された場合、ただのバグなので、それは教えてほしいです。。。