キャッシュアルゴリズムの比較

アプリケーションなどOSより上に作られる高水準のプログラムではハードウェアの速度と容量を考慮しない数学的キャッシュアルゴリズムが使われ主にこれを本稿の対象とする。キー探索用ハッシュマップと明示的キャッシュサイズ(対となる値が保持されているキーの数)は計算量に含まれない。

LRU

最も単純かつ高性能な基礎的アルゴリズム。そのため性能比較のベースラインとして常に使用される。逆に言えば最低水準の性能である。スキャン耐性皆無でスキャン一発でキャッシュとヒット率がリセットされゼロからやり直しになるため非常に脆く不確実な性能となりベンチマークにおける性能が表面上さほど悪くなく見えても実際の性能はこのような外乱により大きく低下しやすい。このためLRUより高度な主要アルゴリズムはすべて大なり小なりスキャン耐性を備えている。ちなみにプログラミング言語最大のパッケージマネージャであるJavaScript(NPM)はパッケージ数100万以上を誇るにもかかわらずLRUしかまともなキャッシュの実装がなくnodeやTinyLFUのような人気のフレーズを付けた名前に名前と無関係の実装を貼り付けたクリックバイトの詐欺パッケージとすぐエラーを吐く壊れたガラクタばかり豊富なスラムみたいな状態となっている。後発のRust(10万パッケージ未満)とGoは理論的に優れた実装がガンガン追加されてるのに全体的に理論的に優れた実装に乏しくキャッシュも最も初歩的で原始的なLRUしか使えない超低空飛行プログラミングに疑問も不満も感じず理論的教養を持つ天上界のプログラマに仕事か気まぐれで運よく優れた実装を恵んでいただけるまで刃の潰れた斧を延々使い続け時間ができても流行りのスコップで飽きもせず同じ穴を何度も掘っては埋め続けるか偽ブランド作りに勤しむ、それがJSerクオリティー。注目を集めるために著名な名前を偽装して名前空間と検索結果を汚染する最低水準のプログラマが集まってろくに管理されてないのでとにかく質と治安が悪い。質は量から生まれるとは何だったのか。

Clock

主にOSがページングのために使用する、LRUの高速な近似アルゴリズム。エントリの追加がO(n)であるためアプリケーションなどで使用する一般的なキャッシュアルゴリズムとしてはLRUより低速と認識されその領域では使用されていないが広く蔓延した誤った認識である。Clockは最悪計算量O(n)をビット演算により少なくとも32倍高速化できこれはGCに依存する低速なリストによるLRU実装よりも高速になりうる。JavaScript(V8)における筆者の実装では最も人気かつ主要実装の中で最速のLRU実装であるIsaacsのLRU(ISCCache: lru-cache。LRUCache: 筆者によるより高速な、JavaScriptにおける最速のLRU実装。DW-Cache: Dual Window Cache)の等倍から2倍の速度を実現しており他の多くの言語でも同様にO(n)のClockのほうがO(1)のLRUより高速になる可能性がある(ClockProなどは履歴を要するためLRUより低速になる可能性が高い)。また近似というと低性能の印象があるが後述の主要ベンチマークで比較するとClockのほうがLRUより5-10%前後ヒット率が高い場合が多くヒット率の観点からもLRUよりClockのほうが優れている。なぜ我々は長年LRUに甘んじていたのか。

LOG: 'Clock    simulation 10 x 12,779,179 ops/sec ±2.35% (117 runs sampled)'
.
LOG: 'ISCCache simulation 10 x 7,267,869 ops/sec ±4.24% (118 runs sampled)'
.
LOG: 'LRUCache simulation 10 x 9,223,949 ops/sec ±2.42% (119 runs sampled)'
.
LOG: 'DW-Cache simulation 10 x 4,994,747 ops/sec ±1.92% (118 runs sampled)'
.
LOG: 'Clock    simulation 100 x 10,104,492 ops/sec ±2.46% (119 runs sampled)'
.
LOG: 'ISCCache simulation 100 x 7,963,071 ops/sec ±2.13% (117 runs sampled)'
.
LOG: 'LRUCache simulation 100 x 8,647,715 ops/sec ±2.87% (116 runs sampled)'
.
LOG: 'DW-Cache simulation 100 x 5,776,189 ops/sec ±2.12% (121 runs sampled)'
.
LOG: 'Clock    simulation 1,000 x 7,571,243 ops/sec ±2.03% (118 runs sampled)'
.
LOG: 'ISCCache simulation 1,000 x 7,113,500 ops/sec ±2.10% (119 runs sampled)'
.
LOG: 'LRUCache simulation 1,000 x 7,694,779 ops/sec ±2.64% (117 runs sampled)'
.
LOG: 'DW-Cache simulation 1,000 x 6,387,276 ops/sec ±1.95% (120 runs sampled)'
.
LOG: 'Clock    simulation 10,000 x 8,317,990 ops/sec ±1.97% (121 runs sampled)'
.
LOG: 'ISCCache simulation 10,000 x 6,184,336 ops/sec ±2.30% (119 runs sampled)'
.
LOG: 'LRUCache simulation 10,000 x 6,950,151 ops/sec ±2.60% (116 runs sampled)'
.
LOG: 'DW-Cache simulation 10,000 x 4,550,682 ops/sec ±1.80% (120 runs sampled)'
.
LOG: 'Clock    simulation 100,000 x 4,668,820 ops/sec ±2.01% (112 runs sampled)'
.
LOG: 'ISCCache simulation 100,000 x 2,364,001 ops/sec ±1.92% (113 runs sampled)'
.
LOG: 'LRUCache simulation 100,000 x 2,592,217 ops/sec ±2.42% (109 runs sampled)'
.
LOG: 'DW-Cache simulation 100,000 x 2,245,278 ops/sec ±2.29% (111 runs sampled)'
.
LOG: 'Clock    simulation 1,000,000 x 2,208,450 ops/sec ±3.90% (107 runs sampled)'
.
LOG: 'ISCCache simulation 1,000,000 x 1,504,206 ops/sec ±3.53% (107 runs sampled)'
.
LOG: 'LRUCache simulation 1,000,000 x 1,452,530 ops/sec ±4.64% (102 runs sampled)'
.
LOG: 'DW-Cache simulation 1,000,000 x 1,396,011 ops/sec ±2.58% (112 runs sampled)'

SLRU (Segmented LRU)

LRUを固定比率で分割しキャッシュヒットの有無で格納先を変える。適切な比率であればLRUより高い性能が期待できるが適切な比率は用途と状況により異なり当然動的な変化には対応できず汎用のLRU、特化のSLRUの関係にある。しかし試用区間(基本20%)を超える再利用距離のデータは間に他のデータへのヒットが区間超過分ない限り絶対にヒットせずそのようなワークロードは案外あるので案外使いどころが限られ事前調査を要する。

ARC (Adaptive Replacement Cache)

SLRUの比率をキャッシュサイズの(本体と合わせて)2倍の履歴とキャッシュヒットにより動的に変更することで動的な変化に対応する。高いヒット率から人気があったが特許により使用が制限されていることと4つのリストを操作するオーバーヘッドの高さから忌避されている。そのため多くのプロダクトがLRUに甘んじるかSLRUを劣化版ARCとして任意の固定比率で妥協して使うか粉飾欠陥クソアルゴリズムのLIRSを騙されて採用し当然クラッシュするもいまさら低性能な(S)LRUに戻すこともできず惰性で騙し騙し使うはめになった(プロプライエタリでは特許に抵触しないよう改変したARCを使う場合もあるようだ)。その後はすっぱい葡萄もあってかキャッシュアルゴリズムの性能はワークロードに依存しARCはそれほど優れたアルゴリズムではないとディスられ続けている(代わりの優れたアルゴリズムを提示できるわけでもないのに)。実際ARCはスキャン耐性が低くループ耐性もないため現在では時代遅れのアルゴリズムと言っても過言ではなく実装に使うビルトインAPIの制限によりLRUより履歴の分少ないサイズしか指定できない場合もある(JavaScriptのV8のMapは最大224エントリだがより少ない1000万でもLRUが落ちる場合が見られARCはわずか500万で落ちた)。なおARCの基本特許はすでに切れている*1がすでに上位互換のDWCがあるため用済みでありいまさら戻ってこいと言われてももう遅い!

DWC (Dual Window Cache)

時間空間ともに定数計算量でありながら高水準のヒット率と耐性を実現する最高性能の定数計算量キャッシュアルゴリズム。DWCよりヒット率の高い主要アルゴリズムはLIRSと(W-)TinyLFUしかないが後述するようにLIRSは性能を粉飾したために致命的欠陥が生じており(W-)TinyLFUも用途に制限があるためDWCがすべての主要汎用キャッシュアルゴリズムの中で最小の計算量でありながら最高の性能となる(マイナーな他のアルゴリズムは共通のベンチマーク結果を公開していないため比較不能、W-TinyLFUは動的ウインドウとインクリメンタルリセットを備えたものはおそらく最高性能の汎用アルゴリズムだが現在該当する実装はCaffeine以外見当たらず論文そのままの固定ウインドウと一括リセットの実装は汎用性が低い)。またLRUより高性能なアルゴリズムの多くがエントリ削除後もキーを保持し履歴として使用する線形空間計算量のアルゴリズムであり標準ライブラリなどの標準的なキャッシュAPIではLRU同様エントリ削除と同時にメモリ解放可能な定数空間計算量のアルゴリズムが第一に求められるだろう(LRUを一般的に置換・陳腐化可能なアルゴリズム、(S)LRUより大幅に高性能な定数計算量アルゴリズム、ループ耐性のある定数計算量アルゴリズム、たぶんすべてにおいて史上初)。ただし情報量(履歴)の不足を補うため全体的に統計精度への依存度が上がっており標本サイズが小さくなるほど情報量と統計精度の低下により性能低下しやすくなる。メインアルゴリズムの統計精度が1パーミルであるため推奨キャッシュサイズは1,000以上となる。LRUしかないJavaScript界で絶対にLRUなんて使いたくない作者により作られた。主に3つの新機構による複合構成となっている。名前はメインアルゴリズムにおいて概念上キャッシュ比率の計算に2つのSliding windowを使用することに由来するが実装上は1つのウインドウから両方の計算結果を算出するよう簡略化している。単にSliding Window Cacheとしたほうが簡潔だが多くの論文で異なるセマンティクスでSliding windowが用いられているため混乱を避けるためこの名前を避けている。上記のグラフは主要ベンチマークにおける主要アルゴリズムの比較でありDWCが定数計算量でありながら線形計算量のARCを超越し最先端の線形計算量アルゴリズムと同等の性能を実現している革命的アルゴリズムであることを示している。このような定数計算量アルゴリズムは他になくDWCが唯一である。

LIRS

キャッシュサイズの最大2500倍の履歴を保持するため採用プロダクトをメモリ不足でクラッシュさせてきた詐欺アルゴリズム。どこも他のアルゴリズムに変えるか履歴サイズをパラメータ化して責任転嫁して無理やり使っているため本当の性能は誰にもわからない。論文が著名で多数の有名プロダクトで採用実績があっても信頼できる優れたアルゴリズムである証明にはならないのである。履歴はLIR末尾またはHIRエントリにヒットするか最大値に達するまで拡大し続けヒットすると条件を満たすまで履歴をたどり一括削除するためレイテンシスパイクの原因ともなりメインメモリのランダムアクセスレイテンシ約100nsを基準とすると履歴含め10,000,000エントリの一括削除で1秒の停止とスパイクとなる。またスループットもリストなどの非常に長い参照のGCには急速に所要時間が増える転換点がある場合があり数倍の履歴でもスループットが急速に低下するためそのようなGCを持つ言語ではメモ化などのキャッシュによる高速化が50%以下と低い場合履歴サイズが転換点を超えるとLRUやキャッシュなしのほうが速くなるリスクが高い(JavaScriptのV8ではDoubly Linked Listは1万から10万ノード、Mapは10万から100万エントリ付近からスループットがノード数10倍あたり約1/2から1/3に急速に低下するよう変化する)。キーの検索にハッシュマップを使っていれば当然ハッシュマップも履歴の分肥大化および低速化し、LIRSの実用的な履歴サイズの推奨値は3−10倍である。クラッシュの実害とこれらの問題にもかかわらず使われ続けたのは当時ループ耐性のある高性能キャッシュアルゴリズムが他になくいまさら(S)LRUに戻せなかったからであろうが現在は他にもループ耐性を持つDWCとW-TinyLFUがありとっくに唯一の選択肢ではなくなっているためあえて危険で性能もわからないLIRSを使う必要はない。なお近年の研究ではキーの履歴サイズはキャッシュサイズの2倍が規範的標準となっている様子が伺え今後適切な履歴サイズが定まるまでせいぜい3倍が限度だろう。

TinyLFU

非常に高性能な新キャッシュアルゴリズムとして知られているが実際には本当に性能が高いのはW-TinyLFUだけでTinyLFUのヒット率は総合的にはLRUより悪いことが論文に明記されており、人気の高いRistrettoは特に論文通りのSLRUでなくLFUをメインキャッシュに使用しているためヒット率が大きく低下するワークロードが生じていることに注意しなければならない(OLTPではLRUより大幅に悪く、LFUの特性によりトレンドの変化が大きいワークロードでは同様に性能が低い可能性がある)。ブルームフィルタ(CountMinSketch)は削除操作不可能であるためキャッシュエントリを削除するほど擬陽性が増え一定期間内の任意または有効期限超過による削除数に比例して性能が低下する。またブルームフィルタがバーストアクセスで飽和し性能がSLRUの水準にまで低下する可能性があるため性能劣化の脆弱性を抱えているに近い状態であり適切にオートスケールできなければ攻撃や肝心な大量アクセスで想定以上の急速な性能劣化に耐えられない可能性がある。スキャン耐性皆無のLRUほどではないがW-TinyLFUの危険(不安全)な未完成品というのがTinyLFUの実態であり用途に対してトレードオフが許容可能か検討と検証なしに使うべきでないアルゴリズムである(SLRUをDWCに置き換え最悪性能を底上げすることで相当程度軽減できるだろう)。さらに定期的にブルームフィルタの一括リセットのために非常に大きなループ処理が定期的に走りレイテンシスパイクが生じる。ブルームフィルタのサイズはキャッシュサイズ(最大エントリ数)の10倍であるためキャッシュサイズが10,000,000で100,000,000回(Ristrettoなど実装とサイズによっては最大でさらに2x4で8倍の800,000,000回、メモリ消費は1要素8bit(1byte)であるため400MB(100MBx4)から800MB(200MBx4))のループ処理となり、1回10nsでも1秒(8秒)の停止とスパイクが生じ、キャッシュサイズ、言語、実装により容易に悪化する。(W-)TinyLFUのレイテンシは許容限度を超えており少なくとも現在の実装ではオンラインサービスなどレイテンシが考慮される用途ではエンタープライズレベルではスケーラビリティが低すぎて使い物にならないだろう(にもかかわらずオンラインで使ってるところはおそらくキャッシュサイズがさほど大きくないかスパイクに気づいてないか放置している)。(W-)TinyLFUは近年人気の高いキャッシュアルゴリズムだがレイテンシに重大な問題が存在するにもかかわらずこれを認識せず盲目的に使われているように見える。

W-TinyLFU

最もヒット率の高いキャッシュアルゴリズムだが削除頻度が高いかレイテンシ要件がある場合は前述の問題のため採用できないだろう。よく見られるW-TinyLFUのヒット率はおそらく動的ウインドウのもの(まさか比率指定を隠したチェリーピッキングではなかろう)で固定ウインドウの実装を使うならば適切なウインドウ比率を自分で設定しなければならない。動的ウインドウのW-TinyLFUはキャッシュを頻繁に作り直さずレイテンシに寛容またはキャッシュサイズが小さくエントリを頻繁に削除しないのであれば非の打ち所のない性能だろう。ただしCaffeineではインクリメンタルリセットが実装されておりヒット率は明らかでないが2回目以降のリセットは相対間隔が等しいことから本質的に等価と考えられる。この場合残る考慮事項は削除頻度の制限と初期化コストの高さのみであろう。W-TinyLFUは形式的にはエヴィクションアルゴリズムなしには機能しないアドミッションアルゴリズムだがウインドウ比率を動的に変更する最も高度なW-TinyLFUの実装(Caffeine)は本質的にはARCやDWCと同様にウインドウLRUと(S)LRUの2つのキャッシュ比率を適応的に調整するエヴィクションアルゴリズムであると言える。

*1:https://patents.google.com/patent/US7167953B2/en、もう一つ追加の特許https://patents.google.com/patent/US20070106846A1/enがあるがどう関係するかよくわからない

地球火星人学の論理的誤謬 (脅威インテリジェンスの教科書/石川朝久)

脅威インテリジェンスの教科書(初版1刷)の中に論理的誤謬を発見したので記録しておく。本書コラムの地球火星人学において著者石川朝久はヘンペルのカラスの代わりに説明する論理的に同一の説明として地球火星人学(初出はおそらく村上陽一郎教授)を提示している。地球火星人学は火星に行けない地球人が火星人が四本足であることを地球上で証明するには四本足でなければ火星人でないことを証明すればよいと説き著者はこれをすべての星を調査しなければならず非現実的であると解説するがここまでに3つもの誤謬を犯している。

  1. 四本足でないものをすべて調査するためには火星の四本足でないもの(火星にいる五本足の火星人は火星を調査しなければ発見できない)も調査しなければならず火星を調査範囲から除外して四本足でなければ火星人でない(火星を含め四本足でない火星人がおらず四本足でない火星人が存在しないことから火星人は四本足でしか存在しえない)ことは証明できないことから火星を調査しないというこの証明の制約を満たすことは不可能であることが証明される。
  2. 火星に行けない代わりの証明方法でありながら火星も調査しなければならないことから他のすべての星を調査する証明コストは論ずるに及ばないものであり他のすべての星の調査をするまでもなく証明方法に誤りがあることを指摘しなければならない。
  3. ヘンペルのカラスは本来の証明対象こそ調査せずともよいが特定範囲外すべてにおいて調査証明すれば特定範囲内においては調査証明せずともよしとする除外範囲を認める論理ではないため非網羅的調査範囲という要素を追加する地球火星人学はそもそもヘンペルのカラスと論理的に全く異なる
  4. 大目に見て数に含めていないがそもそも火星を調査する代わりに他のすべての星を調査しなければならないことを自ら解説した時点で火星に行けない代わりという前提から破綻していることに気づかなければならない。

以上の致命的誤謬があることから読者はこのコラムを正しいと誤解してはならず著者はこのコラムを削除または全面改訂すべきである。

Reverseパターン

非純粋関数を合成可能な擬似純粋関数にするデザインパターン。非純粋関数の返り値を逆操作関数にすることで疑似純粋化し逆操作関数を無引数無返り値に統一することで合成可能化する。操作は逆操作により副作用を残さず中止および終了され複数の操作はArrow演算により単一の操作に合成される。

function proc(): () => void {
  return aggregate(
    bind(el, type, listener),
    bind(el, type, listener));
}

function bind(el, type, listener): () => void {
  el.addEventListener(type, listener);
  return () => el.removeEventListener(type, listener);
}

function aggregate(...as: (b => c)[]): b => c[] {
  return b => as.map(f => f(b));
}

https://github.com/falsandtru/spica/blob/master/src/arrow.test.ts

ゼロトラストセキュリティの基本概念

境界防御からゼロトラストセキュリティへの変遷を通してゼロトラストセキュリティの基本概念を解説する。

境界防御

水際防御とも呼ばれる境界防御は概ね単一かつ唯一の防衛線を設置しこの最終防衛線を死守するセキュリティモデルである。 一度でも突破されればもはや守るもののない一度たりともミスの許されない脆弱なセキュリティモデルでありトロイの木馬により容易に内部に侵入できるITセキュリティと極めて相性が悪い。

多層防御

セキュリティに何よりも求められるのは侵入の検知から対処までの時間的猶予である。多層防御は複数の防衛線を設置し可能な限り外側の防御線で早期に侵入を検知することでさらに内側の防御線の突破に要する時間を対処までの時間的猶予に変えるものでありその概念は縦深防御との同一性から容易に理解できる。しかしながらトロイの木馬による侵入容易性に対する解決にはならず内側の未突破の防衛線以外に基本的に侵入および展開を遮るものがないため侵入箇所より内側の防衛線より外側すべてが侵害可能範囲となり致命的ではなくとも非常に大きい被害が生じる可能性が高い。

ゼロトラストセキュリティ

ゼロトラストセキュリティは微視的には多層防御の縦の防御に横の防御と内側への防御を加えるものであり、全ノードが全周防御、いわば要塞化することでこれを実現し被害の周辺部への拡大を防ぎ局所化するセキュリティモデルである。巨視的には侵入者が重要部を制圧するまでに突破する必要のある防衛線の数と強度を十分に確保できるノード配置および動線の設計であり、被害の局所化はその効果、全(周辺)ノードのセキュア化はその前提である。

ゼロトラストセキュリティは本土防衛戦争と解釈すると理解しやすい。侵入者は国内に侵攻または国内で武装蜂起した敵軍であり我が方は首都の防衛成功を勝利条件とする。首都防衛を達成するためには侵攻および蜂起箇所から首都までの間に防衛線を設置しここで食い止める必要がある。ゼロトラストセキュリティにおいて防衛線はサーバーおよびネットワーク機器等の各ノードが個々に展開する多層防御でありこれは要塞化された都市に相当する。我が方は敵軍が首都を制圧するまでに可能な限り多くの要塞都市を経由させることで可能な限り多くの防衛線の突破を強いなければならず、よって要塞都市を迂回できる街道のごときいかなる物理的・電子的経路もあってはならない。ここから一部の部門でだけゼロトラストセキュリティを導入して要塞化しても他の要塞化されてない部門を突破して中心部へ侵攻できるため意味がなく、終着地である首都に相当する部門は必ず要塞化し防衛線を設置しなければならないことがわかる。また重要部では防衛線を迂回して都市へ侵入できないよう検問も実施しなければならない。企業の提案するゼロトラストセキュリティおよびその製品は基本的にこうした各都市の要塞化や検問ための装備にすぎず最終目標である首都防衛のための総合的防衛戦略ではないことに注意しなければならない。なお要塞による拠点防衛は軍事的には縦深防御から退化しているがITにおいては距離と補給の制約がないなど各種前提の相違から有効なものである。要塞は実世界では火砲の的だがITにおける火砲であるDDOSはインターネットに接続した一部の端末を潰すだけで陽動や拘束程度にしかならず、事業的にはともかくセキュリティ上の脅威度は低い(社内業務のための予備系統を利用できる場合)。ITにおいて脅威なのは巡航ミサイルによる中枢部への精密攻撃のようなものであり強いて言えば各都市にはこの発見および撃墜も期待される。閑話休題

このように構築した都市と国家を俯瞰すると敵軍にはより多くの都市を経由させなければならず、都市はすべて防衛線を持つよう要塞化されていなければならず、もって敵軍が首都を制圧するために突破しなければならない防衛線を最大限多くかつ強固にするのがゼロトラストセキュリティの本質たる基本概念であることがわかる。逆に首都制圧までに突破を強いる防衛線の数と強度を勘案せず単にセキュリティ製品を購入し装備するとそのセキュリティ強度は首都との間に偶然存在する防衛線の数および強度に依存する確率的で非効率な信頼性の低いものとなる。ゼロトラストセキュリティを導入し全ノードをセキュア化したが結局少なくともいくつの防衛線および検問が必ず機能する機会が確保されているかおよびこのセキュリティレベルで十分であるかが明確でない場合がこれに該当する。

Haskellの非実用性

  1. IDEなどの開発支援機能を使用できない場合が少なくない
    • .chsファイルへの非対応
    • 実際的な広いユースケースで現代的なプログラミングを実現できておらずインタラクティビティの低い原始的なプログラミングを強いられる
  2. ビルドが長すぎる(1時間前後)
    • 脆弱性修正の数分以内の緊急デプロイといったセキュリティ要件を満たさない
    • ダウンタイムがビルド時間に応じて長時間化することで繁忙期などに発生した重大問題が甚大な機会損失および信用失墜に発展するリスクが非常に高い
    • アップデートやデバッグなどにともなう長時間のリビルドによる人員設備等の稼働率低下による損失が大きすぎる
    • 勤怠状況の不明瞭化による勤怠管理の困難化が怠業を誘発し周辺人員への悪影響などの周辺問題の連鎖的発生と拡大につながる構造的問題が生じる
  3. レコードフィールド名に非現実的な制約がある

以上の理由からサービス開発へのHaskellの使用を放棄した。

Haskellは以前はプロダクションレディだったかもしれないが今は少なくともサービス開発においてはそうではない。一度プロダクションレディになれば永遠にプロダクションレディでいられるわけではない。実用言語であり続けるためにはプログラミングの進歩とこれにともなう要件の変化に継続的に追従する必要がある。

補足
当然ながらコンテナOSで実行するためスタティックリンクが必須となる。 オプションを変えてテストする成果物とデプロイする成果物を変えるなど論外。 CIサーバーのキャッシュは小規模な実験的プロジェクトでも簡単に数GBに達してあふれるので無意味。 キャッシュできたとしても依存関係やStackのアプデごとに1時間かけて再構成しなければならず変化に極めて脆弱で気休め程度にしかならない。 ローカルビルドとリモートビルドに1時間ずつかかるような試行錯誤が困難で触りたくないと思われるコードベースや開発環境のプロダクトは死んだプロダクトだということを念頭に置かなければならない。

Haskellの差分リストはなんちゃって差分リストではないか?

Haskellの差分リストは一般に([1,2,3] ++) . ([4,5,6] ++)のようにセクションで示される。しかしこれは連結の際に左側のリストの要素をたどって末尾を見るので計算量がリストの長さに比例して増加しO(n)となる。一方CTMCP(コンピュータプログラミングの概念・技法・モデル)では要素をたどらず直接末尾を見るので計算量がリストの長さに比例して増加せずO(1)となる実装がOzにより示され、そしてこの効率性が差分リストの特徴とされている。Haskellも遅延評価の場合は連結コストが使用時のコストに同化されO(1)となるかもしれないが正格評価の場合は連結時に直ちにO(n)のコストを支払わなければならず、さらにこの状況は完全な正格評価でなくともデータが正格あるいは評価済みであるだけでも生じるため遅延評価の中でも無縁な話ではない。これは本当に差分リストとして説明してよいのだろうか?どうも過日のなんちゃってクイックソートと同じ種類の誤りを含んでいるように見える。

そういえばHaskellの差分リストについて気になることがあったのを思い出したのでアドカレのネタに書いてみた。


議論の結果、Haskellの差分リストがO(n)で非効率となる状況は連結演算まで正格となる状況まで限定できそうであるという結論になった。つまり差分リストの問題点は指摘のとおりだが正格性依存な分クイックソートよりはマシ。

Haskellの差分リストはなんちゃって差分リストではないか? : haskell_jp

AsyncIteratorとPromiseによるObservablePromise抽象

ObservablePromiseはAsyncIterableインターフェイスを実装することで途中状態を取得可能にしたPromiseである。ObservablePromiseの最大の利点は非同期の途中状態と最終状態という意味的に異なる状態の分離をfor-awaitとasync/awaitという言語抽象レベルで行うことで標準的な形でこれを区別して扱えることであり、その内部表現もまた言語抽象であるAsync generatorで標準にのっとり簡潔に記述できる。

ObservablePromise抽象はコルーチンと親和性が高いためここではObservablePromiseをコルーチンとして実装した。ただしコルーチンの操作的基本機能であるsuspend/resumeはJavaScriptでは実用性が低いためプロパティを通してポートオブジェクトの形で追加的に提供する*1。ObservablePromiseは次のような表現を可能にする。なおこのコルーチンはCancelableでもあるためより正確にはCancelableObservablePromiseであり、単なるCancelablePromiseとしても有用性が高い。また基底のPromiseには同期Promiseを使用しておりコルーチンの生死判定とその事後処理は遅延なく同期的に行われる。

    it('terminate', done => {
      let cnt = 0;
      const co = new Coroutine(async function* () {
        assert(cnt === 0 && ++cnt);
        return 0;
      });
      co.finally(() => {
        assert(cnt === 1 && ++cnt);
      });
      co[Coroutine.terminator]();
      assert(cnt === 2 && ++cnt);
      co[Coroutine.terminator](1);
      co.catch(done);
    });

    it('iterate', async () => {
      let cnt = 0;
      const co = new Coroutine<number, number>(async function* () {
        assert(++cnt === 1);
        assert(undefined === (yield Promise.resolve(2)));
        assert(undefined === (yield Promise.resolve(3)));
        await wait(100);
        assert(undefined === (yield Promise.resolve(4)));
        return Promise.resolve(5);
      });
      assert(cnt === 1);
      for await (const n of co) {
        assert(n === ++cnt);
      }
      for await (const _ of co) {
        assert(false);
      }
      assert(await co === ++cnt);
      assert(cnt === 5);
    });

https://github.com/falsandtru/spica/blob/master/src/coroutine.test.ts

XMLHttpRequestはObservablePromiseで表現することで非常に簡潔になる。

    it('basic', async () => {
      const co = cofetch('');
      for await (const ev of co) {
        assert(ev instanceof ProgressEvent);
        assert(['loadstart', 'progress', 'loadend'].includes(ev.type));
      }
      assert(await co instanceof XMLHttpRequest);
    });

https://github.com/falsandtru/spica/blob/master/src/cofetch.test.ts

このようにObservablePromiseは優れた抽象表現でありその標準性から一般に推奨できるものである。ただしNode.jsが非同期APIにPromiseを採用しなかったようにパフォーマンス上の問題が生じる場合はこの限りではない。また2018年12月時点ではEdgeでAsyncIteratorが未実装であるためブラウザではもうしばらく待つ必要がある。そのほか、自身はコルーチンの管理にSupervisorを使用してプロセス管理の煩雑さを低減している。そしてこのSupervisorもまたコルーチンである。

*1:このコルーチンは入力のキューのサイズが0のときは入力を取らず自動で非同期イテレーションを回し、1以上のときは入力の都度イテレーションを回すことで自動手動両方のイテレーション方法に対応している。