From d56985a41722b67fa606820552468b010cfd1616 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Sat, 22 Aug 2026 16:10:20 +0200 Subject: [PATCH 1/2] perf(yape): alias imported helpers to module-scope consts bench/README.md carried the YAPE detectors as an open finding (8/8 jsfeat, roughly 1.3x) with the hypothesis that importing hessian_min_eigen_value across modules blocked V8 inlining. Profiling refuted that hypothesis: 96.6% of yape06.detect's time is in detect itself and ~0.6% in the helpers, and the two implementations' inner loops are character-for-character identical. A structural probe then replicated detect's body verbatim under four shapes, asserting identical corner counts first. Two effects reproduced across three clean runs: a class method is slower than an object-literal property, and calling an ESM imported binding from a hot loop is slower than calling a plain module-scope const holding the same function -- imported bindings are live, so each access carries an indirection a const does not. The second gained 12-28% every run and is a one-line change; this commit applies it. The result is a split, and only half the finding is fixed: yape 8/8 jsfeat, 1.23-1.53 -> 1.02* / 1.05* / 1.14 / 1.06* RESOLVED yape06 8/8 jsfeat, 1.11-1.45 -> 1.14 / 1.47 / 1.41 / 1.24 unchanged (* = jsfeatNext faster that run.) That split agrees with the profile rather than contradicting it. yape06 calls compute_laplacian once per frame and hessian_min_eigen_value only for candidate pixels -- which is precisely why the helpers measured 0.6%. yape's helpers sit in the per-pixel loop, so aliasing them matters there. yape06's alias is kept for consistency and costs nothing, but it measured no benefit and is not claimed to help. yape06's cause remains unknown; the class-vs-object-literal effect the probe also showed is the untested candidate. Co-Authored-By: Claude Opus 5 --- bench/README.md | 40 +++++++++++++++++++++++++++++++++++++++- src/yape/yape.ts | 9 +++++++-- src/yape06/yape06.ts | 16 ++++++++++++++-- 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/bench/README.md b/bench/README.md index 3217575..8d70b8e 100644 --- a/bench/README.md +++ b/bench/README.md @@ -88,7 +88,45 @@ Judged **against the noise floor above**, not in isolation: Since jsfeatNext is a port of jsfeat, ratios near **1.0× are the expected, healthy state** — the two are doing the same arithmetic. A *sustained* move is interesting; a single-run move is not. -## Open finding: the YAPE detectors are consistently slower +## YAPE detectors: `yape` RESOLVED, `yape06` still open + +> **Partly resolved, and the original hypothesis was wrong.** +> +> This section long carried the theory that importing +> `hessian_min_eigen_value` across modules blocked V8 inlining. Profiling +> `yape06.detect` refuted it: **96.6% of the time is in `detect` itself and +> ~0.6% in the helpers**, and the two implementations' inner loops are +> character-for-character identical. +> +> A structural probe then replicated `detect`'s body verbatim under four +> shapes, verified to find identical corner counts. Across three clean runs, +> two effects reproduced — a class method is slower than an object-literal +> property, and calling an ESM **imported binding** from a hot loop is slower +> than calling a plain module-scope `const` holding the same function +> (imported bindings are live, so each access carries an indirection). The +> second gained 12–28% every run, and is a one-line change. +> +> Aliasing the helpers to module-scope consts in `yape06.ts` and `yape.ts` +> gave a **split result**: +> +> | case | before (8 samples) | after (4 samples) | status | +> | --- | --- | --- | --- | +> | **`yape`** | 8/8 jsfeat, 1.23–1.53 | 1.02\* / 1.05\* / 1.14 / 1.06\* | **resolved** — sign now flips | +> | `yape06` | 8/8 jsfeat, 1.11–1.45 | 1.14 / 1.47 / 1.41 / 1.24 | **unchanged, still open** | +> +> \* = jsfeatNext faster in that run. +> +> The split is consistent with the profile rather than at odds with it: +> `yape06` calls `compute_laplacian` **once per frame** and +> `hessian_min_eigen_value` only for candidate pixels — which is exactly why +> the helpers showed 0.6%. `yape`'s helpers sit in the per-pixel loop, so +> aliasing them matters there. `yape06`'s alias is kept for consistency but +> measured no benefit; **its cause remains unknown**, and the class-vs-object +> effect the probe also showed is the remaining untested candidate. +> +> The measurements below are the pre-fix record. + +### Original finding (pre-fix record) The first real signal this harness produced. Eight samples, pooled from two separate sessions on an idle machine (power connected, browser and editor diff --git a/src/yape/yape.ts b/src/yape/yape.ts index 9f1f832..407ed00 100644 --- a/src/yape/yape.ts +++ b/src/yape/yape.ts @@ -41,6 +41,11 @@ */ import { third_check, is_local_maxima, perform_one_point, lev_table_t } from "./yape_utils"; + +/** Module-scope aliases for the per-pixel helpers -- see the note in `yape06.ts`. */ +const thirdCheck = third_check; +const isLocalMaxima = is_local_maxima; +const performOnePoint = perform_one_point; import { matrix_t } from "../matrix_t/matrix_t"; import { keypoint_t } from "../keypoint_t/keypoint_t"; @@ -129,7 +134,7 @@ export class yape { if (im < img[rowx + R] && img[rowx + R] < ip && im < img[rowx - R] && img[rowx - R] < ip) { scores[rowx] = 0; } else { - perform_one_point(img, rowx, scores, im, ip, dirs, opposite, dirs_count); + performOnePoint(img, rowx, scores, im, ip, dirs, opposite, dirs_count); } } } @@ -144,7 +149,7 @@ export class yape { // if this pixel is 0, the next one will not be good enough. Skip it. (++x, ++rowx); } else { - if (third_check(scores, rowx, w) >= 3 && is_local_maxima(scores, rowx, score, hw, R)) { + if (thirdCheck(scores, rowx, w) >= 3 && isLocalMaxima(scores, rowx, score, hw, R)) { pt = points[number_of_points]; ((pt.x = x), (pt.y = y), (pt.score = abs_score)); ++number_of_points; diff --git a/src/yape06/yape06.ts b/src/yape06/yape06.ts index 76aef9c..ee0f7df 100644 --- a/src/yape06/yape06.ts +++ b/src/yape06/yape06.ts @@ -45,6 +45,18 @@ import { matrix_t } from "../matrix_t/matrix_t"; import { keypoint_t } from "../keypoint_t/keypoint_t"; import { compute_laplacian, hessian_min_eigen_value } from "./yape06_utils"; +/** + * Module-scope aliases for the two per-pixel helpers. + * + * Calling an ESM *imported binding* directly from a hot loop measured + * consistently slower than calling a plain module-scope `const` holding the + * same function -- imported bindings are live, so each access carries an + * indirection a `const` does not. Measured under #86 across three runs: the + * alias form gained 12-28% every time. See `bench/README.md`. + */ +const computeLaplacian = compute_laplacian; +const hessianMinEigenValue = hessian_min_eigen_value; + /** * YAPE06 interest-point detector: thresholds a Laplacian response map, then * rejects edge-like responses via the minimum eigenvalue of the local @@ -109,7 +121,7 @@ export class yape06 extends jsfeatNext { while (--x >= 0) { laplacian[x] = 0; } - compute_laplacian(srd_d, laplacian, w, Dxx, Dyy, sx, sy, ex, ey); + computeLaplacian(srd_d, laplacian, w, Dxx, Dyy, sx, sy, ex, ey); row = (sy * w + sx) | 0; for (y = sy; y < ey; ++y, row += w) { @@ -135,7 +147,7 @@ export class yape06 extends jsfeatNext { lv > laplacian[rowx - w + 1] && lv > laplacian[rowx + w + 1]) ) { - min_eigen_value = hessian_min_eigen_value(srd_d, rowx, lv, Dxx, Dyy, Dxy, Dyx); + min_eigen_value = hessianMinEigenValue(srd_d, rowx, lv, Dxx, Dyy, Dxy, Dyx); if (min_eigen_value > eigen_thresh) { pt = points[number_of_points]; ((pt.x = x), (pt.y = y), (pt.score = min_eigen_value)); From 1c78a797849e4018d03226a66764deb0275f656c Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Sat, 22 Aug 2026 16:22:13 +0200 Subject: [PATCH 2/2] fix(yape): restore contiguous imports, correct the yape06 rationale Two issues from Copilot review of #166. yape.ts had its import declarations split by the new alias consts, which breaks the repo's grouped-imports pattern and would trip `import/first`. Imports are contiguous again, with the aliases below them. The yape06.ts comment was inaccurate in two ways and contradicted this PR's own README changes: it called both helpers "per-pixel" when compute_laplacian runs once per frame, and it quoted a 12-28% gain that came from the probe -- while bench/README.md states plainly that the alias measured NO benefit for yape06. Rewritten to say what is actually true here: the effect is real where a helper runs per pixel (it resolved yape), it does not apply to yape06's call frequencies, and the alias is kept for consistency rather than speed. Specific deltas are left to bench/README.md so they cannot go stale in a source comment. Co-Authored-By: Claude Opus 5 --- src/yape/yape.ts | 12 +++++++++--- src/yape06/yape06.ts | 16 ++++++++++------ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/yape/yape.ts b/src/yape/yape.ts index 407ed00..a352cde 100644 --- a/src/yape/yape.ts +++ b/src/yape/yape.ts @@ -41,13 +41,19 @@ */ import { third_check, is_local_maxima, perform_one_point, lev_table_t } from "./yape_utils"; +import { matrix_t } from "../matrix_t/matrix_t"; +import { keypoint_t } from "../keypoint_t/keypoint_t"; -/** Module-scope aliases for the per-pixel helpers -- see the note in `yape06.ts`. */ +/** + * Module-scope aliases for the three helpers `detect` calls in its per-pixel + * loop. Calling an ESM imported binding is slower than calling a plain + * `const` holding the same function -- imported bindings are live, so each + * access carries an indirection. Measured under #86; see `bench/README.md` + * for the numbers, which are deliberately not repeated here. + */ const thirdCheck = third_check; const isLocalMaxima = is_local_maxima; const performOnePoint = perform_one_point; -import { matrix_t } from "../matrix_t/matrix_t"; -import { keypoint_t } from "../keypoint_t/keypoint_t"; /** * YAPE ("Yet Another Point Extractor") interest-point detector: scores each diff --git a/src/yape06/yape06.ts b/src/yape06/yape06.ts index ee0f7df..bf8645f 100644 --- a/src/yape06/yape06.ts +++ b/src/yape06/yape06.ts @@ -46,13 +46,17 @@ import { keypoint_t } from "../keypoint_t/keypoint_t"; import { compute_laplacian, hessian_min_eigen_value } from "./yape06_utils"; /** - * Module-scope aliases for the two per-pixel helpers. + * Module-scope aliases for the two helpers, mirroring `yape.ts`. * - * Calling an ESM *imported binding* directly from a hot loop measured - * consistently slower than calling a plain module-scope `const` holding the - * same function -- imported bindings are live, so each access carries an - * indirection a `const` does not. Measured under #86 across three runs: the - * alias form gained 12-28% every time. See `bench/README.md`. + * Calling an ESM imported binding is slower than calling a plain `const` + * holding the same function -- imported bindings are live, so each access + * carries an indirection. That effect is real and measurable where a helper + * runs per pixel (it resolved the `yape` finding), but it did **not** measure + * any benefit here: `compute_laplacian` runs once per frame and + * `hessian_min_eigen_value` only for candidate pixels, so neither is called + * often enough for the indirection to matter. Kept for consistency with + * `yape.ts`, not because it speeds this module up. `yape06`'s own gap against + * original jsfeat is still unexplained -- see `bench/README.md`. */ const computeLaplacian = compute_laplacian; const hessianMinEigenValue = hessian_min_eigen_value;