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..a352cde 100644 --- a/src/yape/yape.ts +++ b/src/yape/yape.ts @@ -44,6 +44,17 @@ import { third_check, is_local_maxima, perform_one_point, lev_table_t } from "./ import { matrix_t } from "../matrix_t/matrix_t"; import { keypoint_t } from "../keypoint_t/keypoint_t"; +/** + * 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; + /** * YAPE ("Yet Another Point Extractor") interest-point detector: scores each * pixel by comparing it against a precomputed circle of samples at the given @@ -129,7 +140,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 +155,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..bf8645f 100644 --- a/src/yape06/yape06.ts +++ b/src/yape06/yape06.ts @@ -45,6 +45,22 @@ 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 helpers, mirroring `yape.ts`. + * + * 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; + /** * YAPE06 interest-point detector: thresholds a Laplacian response map, then * rejects edge-like responses via the minimum eigenvalue of the local @@ -109,7 +125,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 +151,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));