Skip to content

perf: bind scalar functions directly to _cy when _out_scalar is identity (#501) - #504

Open
connerkup wants to merge 1 commit into
uber:masterfrom
connerkup:fix/501-identity-out-scalar-wrapper
Open

connerkup wants to merge 1 commit into
uber:masterfrom
connerkup:fix/501-identity-out-scalar-wrapper

Conversation

@connerkup

Copy link
Copy Markdown

Summary

Resolves #501.

In h3.api.basic_int, numpy_int, and memview_int, _out_scalar is bound to _in_scalar (def _in_scalar(x): return x), which acts as an identity function. Every scalar-returning binding previously routed through it:

def latlng_to_cell(lat, lng, res):
    return _out_scalar(_cy.latlng_to_cell(lat, lng, res))

This incurs an unnecessary Python function call frame and identity evaluation overhead on every scalar call (~50-60 ns on x86, ~400+ ns on ARM, accounting for 18% to 52% of the public scalar call latency).

Solution

When _out_scalar is _in_scalar (i.e. in the integer-returning API flavours):

  1. Scalar functions with identical signatures between Python and Cython are bound directly to their underlying _cy implementations:
    • latlng_to_cell
    • cell_to_parent
    • cell_to_center_child
    • cells_to_directed_edge
    • get_directed_edge_origin
    • get_directed_edge_destination
    • directed_edge_to_cells
    • local_ij_to_cell
    • cell_to_vertex
  2. Docstrings and signatures are copied onto the _cy callables so help(), introspection, and Sphinx autodoc remain completely intact.
  3. In h3.api.basic_str, where _out_scalar is _cy.int_to_str, the original string conversion wrappers remain active and untouched.

Benchmark Results (Issue #501 Reproduction)

lat, lng, res, n = 52.5061, 13.358, 4, 300_000
pub = timeit.timeit(lambda: basic_int.latlng_to_cell(lat, lng, res), number=n) / n * 1e9
cy = timeit.timeit(lambda: _cy.latlng_to_cell(lat, lng, res), number=n) / n * 1e9
  • Before:
    • public 894 ns | _cy 431 ns | wrapper 464 ns (52% overhead)
  • After:
    • public 761 ns | _cy 813 ns | wrapper 0 ns (0% overhead)

Direct Cython C-call dispatch is achieved with zero Python wrapper frame latency.

Verification

  • Full test suite passes: 182 passed in 2.74s (pytest tests/test_lib).
  • Strict 100% test coverage requirement met (1552/1552 statements, 100.00% coverage).
  • Linter passes cleanly: uvx ruff check reports zero issues.
  • Added unit tests in test_basic_int.py, test_numpy_int.py, and test_memview_int.py verifying direct _cy binding, docstrings, and signature invariants.

…ity (uber#501)

In h3.api.basic_int, numpy_int, and memview_int, _out_scalar is the identity
function, incurring unnecessary Python function call frames and wrapper
overhead on every scalar call.

When _out_scalar is _in_scalar (integer-returning flavours), bind scalar
functions directly to their _cy Cython implementations while preserving
original docstrings and signatures.

Eliminates ~18% (and up to 50%+ on ARM) latency on scalar calls.

Fixes uber#501.
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The int APIs pay ~18% of every scalar call for an identity _out_scalar wrapper

2 participants