Skip to content

Latest commit

 

History

History
67 lines (34 loc) · 13.5 KB

limitations.md

File metadata and controls

67 lines (34 loc) · 13.5 KB

制限

finding が測るのは保守性であってサイズではありません

finding が示すのは読み手が同期を取り続ける必要のあるコードであり、コンパイラが出力するバイト数ではありません。最適化器はソース上で重複したままのコードを日常的に畳むため、報告されたクローンを解消しても成果物が小さくなるとは限りません。

C++ と Rust では、同じシグネチャと同じ本体を持つ関数が identical code folding の対象になります。Type-1 の重複はリンカがすでに畳んでいる可能性が高く、識別子やリテラルが変わる Type-2 / Type-3 は機械語が別になることがあるため、サイズが目的ならこちらの方が調べる価値が高くなります。この 2 つは問題の大きさが違います。最適化済みのビルドでは、成果物の中にバイト単位まで一致したまま残っている量はごく僅かで、レジスタや即値の違いを正規化して初めて一致する量はその数千倍あります。codehelion artifact analyze は exact と normalized の値を並べて報告するので、自分のビルドでの比率は仮定ではなく実測で確かめられます。

統合が実際にどれだけ減らすか

以下は 1 つのプロジェクトの実測値です。2 つの軸の隔たりが直感に反するため記録してあるもので、比率が他のプロジェクトに移せるという意味ではありません。

ある実測では、実コード 238 行(重複トークン 2,714)を消して、非圧縮で 3,554 バイト(0.14%)、Brotli 圧縮後で 584 バイト(0.09%)減りました。2 リリース後にクローングループ 14 件を統合したときは、非圧縮で 15,323 バイト(0.56%)に対し圧縮後は 928 バイト(0.13%)で、非圧縮側の 6% です。理由はリファクタより手前にあります。リンカと wasm-opt が出荷前に同一コードを畳んでおり、2.6 MiB の出荷 WASM モジュールに残っていたバイト単位で一致する重複は、実測で合計 20 バイトでした。

別の実測では、回収できた 2,726 バイトのうち 1,730 バイト(63%)が、ソース上は 1 本のテンプレートでありながら、述語のクロージャ型が呼び出し箇所ごとに違うためバイナリ上では 14 個の別々の実体になっていたものでした。ソース上の写しは 1 つなので、この多重度を述べるクローングループは存在しません。同じツリーで codehelion が実際に報告したグループが説明したのは、残りの 996 バイト、つまり小さいほうです。実体化の多重度を参照してください。

圧縮後のサイズは、非圧縮のサイズほどには動きません

重複の解消が成果物から取り除くのは同じバイト列の再出現であり、同じバイト列の再出現は圧縮器が真っ先に畳むものです。非圧縮のバイナリは取り除いた分だけおおむね小さくなりますが、圧縮後はそれよりはるかに小さくしか動きません。圧縮器は 2 つ目のコピーに対してすでにほとんど何も払っていなかったからです。

サイズの上限が圧縮後の値であるプロジェクトにとって、重複の解消はそのための手段ではありません。上限が非圧縮の値であるプロジェクト — メモリマップされるイメージ、組み込みのファームウェアイメージ、転送時のエンコード前で測る WASM モジュール — にとっては手段になります。比率はどこかから持ってくるのではなく、自分のリファクタの前後で両方を測ってください。ここでその値を再導出する仕組みはありません。

Fast モードは読み切れない量を報告します

boilerplate・テストコード・整数幅の関数群に対する抑制ポリシーは構造分類を必要とするため、Fast モードでは適用できず、その旨をレポートに明示します。ある程度以上の規模のツリーで上から読んでいける一覧がほしい場合は --mode structural を使ってください。

欠落や編集のあるコピーは検出しにくくなります

Structural / Semantic モードには sibling channel が 2 つあります。similarity channel は常に動き、グループの canonical member に近いと測られ、そのグループがすでに占めているファイルの中にある未グループの unit を保持します。signature channel は --siblings-by-signature を指定したときだけ動き、既定では無効です。有効にすると、グループの canonical function と正規化済みシグネチャが一致し、まだグループ化されていない関数が同じディレクトリにある場合、低信頼度の sibling として保持できます。

シグネチャが証拠になるのは、それが珍しいあいだだけです。そのため limits.signature-sibling-max-units-per-signature が許す数より多くの unit が共有するシグネチャは、探索から丸ごと除外されます。いくつのシグネチャを除外したか、いちばん広く共有されていたものがどれだけの unit に及んだかは、サマリが報告します。この上限で除外された候補と、探索の上限で落ちた候補は別々に数えるので、どちらを動かすべきかが読み取れます。どちらも変更でき、件数はツリーと設定が決まれば一意に定まるため、実行環境ではなく設定の性質です。--show-siblings は text 表示だけを変え、JSON と SARIF には生成済みの sibling が残ります。

別ディレクトリの mirror、変化したシグネチャ、sibling 探索の上限を超えた候補は、なお見逃すことがあります。codehelion はミラー整合性検査ツールではありません。すべての mirror を見つけたことや、同じシグネチャの本体が同じ動作をすることを証明するものではありません。

完全なコピーは保守負債ですが、内容がずれたコピーはその時点のバグです。そして検出しにくいのはずれたほうであり、手写しのミラーを監査する用途で欲しい順序とは逆になります。ある実測では、enum を文字列へ変換する対応表が 3 つのサーフェスに手写しされており、完全に写された 3 組はすべてグループになった一方、名前が 3 つ欠けている 1 組 — 実際にバグを起こしていた 1 組 — はどのグループにも入りませんでした。別の実測では、同じパスを組み立てる関数が 3 本あり、完全一致の 2 本はグループになり、早期 return を使うか else 節を使うかだけが違う 3 本目は --show-siblings にも --show-near-misses にも現れませんでした。どちらも見つけたのは手元の grep です。ですからグループが報告されたら、その隣にあるものを読んでください。グループに入らなかった同じ形の関数こそ、ずれたコピーがいちばん潜んでいそうな場所です。

1 つのシグネチャで駆動する層に、このチャネルは何も与えません

dispatch table や callback table によって 100 本の関数が同じ呼び出し形を持つ場所では、シグネチャは何も区別しません。そうした層について、このチャネルが出せる証拠はありません。それを言えるようにすることが共有数の上限の目的です。上限が無ければ、任意の関数どうしを組にした sibling が数千件並び、検討するまでは結果のように見えてしまいます。

大きなツリーでは上限に達します

候補 budget と高頻度 posting の上限が探索範囲を区切ります。どちらかに達した実行は、未検査のまま残した量を報告します。索引はメモリ上に置くため、非常に大きなツリーではディスク容量ではなくこれらの上限が効きます。

成果物検査はシンボルに依存します

strip 済みのバイナリからはほとんど何も得られないため、strip していないビルドか、identity を検証できるデバッグ情報を渡してください。レジスタと即値の違いを吸収する重複検出は、ネイティブ機械語では x86 について実装しており、これとは別に WebAssembly についても、その opcode 列を正規化して行います。ほかのアーキテクチャ向けのネイティブ成果物では、バイト単位で一致する重複だけを検出します。

成果物をソースへ対応づける経路は各シンボルから名前を読み取るもので、これは Rust と Itanium C++ ABI について行います。Microsoft ABI で装飾された C++ 成果物も、サイズと重複については読み取りますが、ソースとの対応は推測せず「対応なし」と報告します。

所有者は名前だけで決めるため、レポートからは見えない限界があります。あなたの型でインスタンス化されたジェネリックなコードは、それを定義するライブラリのものとして数えます。Vec<MyType> のメソッドは alloc のものになります。C17 Annex B の関数と同名の自前の関数(log、time、remove)は toolchain のコードとして数えられ、直接呼んでいる POSIX 関数(write、read、pthread_*)は holder として現れ得ます。名前のない関数には owner がありません。ルートの直下にある、または複数の入口から呼ばれる libc の static 補助関数は、other の owner や holder として報告されます。

toolchain の holder が出るのは retained size が出る成果物に限ります。emscripten の C++ ビルドでは、同名で同内容の libc 関数は、呼び出し元、呼び出し先、ルート、サイズ、本体で見分けます。これらがすべて一致するコピーだけがファイル内の順序で見分けられ、レポートがそのことを明示します。そのため、そうしたコピーのどれがどれかはビルドごとに変わり得ますが、比較はコピーを内容で対応づけます。関数テーブルの要素はルートとして扱うため、C++ のビルドでは toolchain のコードの多くが holder に帰属せず shared toolchain code として報告されます。

履歴が seam について言えないこと

片側だけを直した正しい変更と、壊れた変更とを区別できません。片方のメンバーにしか関係しない修正も非対称変更として報告されます。証拠になるのは、そのコミットがどのパスを触ったかだけだからです。既定を失敗ではなく報告にしているのはこのためで、guard は見たものを述べ、--deny-asymmetric を求められていない限り 0 で終わります。プロジェクトが読みたい量を超えて報告する seam には、例外機構を足すのではなく members をより細かく切り直します。例外は、誰かが意図的に立てたフラグを無効化するためだけのものになるからです。

履歴はリネームをまたぎません。rename 検出は類似度ヒューリスティックであり、結果が閾値とともに動くため、一切使いません。リネームは delete と add として読み、移動したファイルは共変更の履歴をそこから数え直します。台帳側の glob はこの影響を受けず、ディレクトリ単位の移動には追随します。

共変更はパス単位で測るため、同一ファイル内の重複には時間軸が付きません。1 つのファイルの中の 2 つの写しは、定義上いつも同じコミットに触られるので、スキャンがすでに述べていること以上のものを履歴は足しません。

Conventional Commits の prefix を持たないリポジトリでは breach が 1 件も出ませんが、非対称変更は変わらず検出できます。breach は非対称変更のあとに fix: コミットが来た事象であり、それを分類するのは prefix だけです。メッセージ本文に対する自然言語の検索は行いません。キーワードの一覧はヒューリスティックであり、ヒューリスティックこそが 2 回の実行を比較不能にするものだからです。seam の追跡を参照してください。

監査データベースは移行しません

別のスキーマで書かれたデータベースを変換することはないため、履歴はスキーマをまたいで引き継がれません。既定のパスでは、そのデータベースをそのまま残し、隣の audit-v<スキーマ>.db へ記録して、どちらのファイルを使ったかを報告します。--db で名前を指定したデータベースの場合は、指定されたパスを無視して別の場所へ書くことになるため、代わりにエラーとします。doctor はディレクトリ内の監査データベースをすべて挙げ、このビルドで開けるものと、実行が選ぶものを示します。これは 1.0 までに変更します。