Skip to content

_bind() materializes a Python list[float] on every evaluation, including inside the functionals #197

Description

@robertodr

🤖 AI text below 🤖

What

MonomialPropagator._bind builds a new Python list[float] on every call.

src/monoprop/monomial_propagator.py:602-612

def _bind(self, parameters: ParameterValues) -> list[float]:
    if isinstance(parameters, Circuit):
        parameters = parameters.parameters
    if parameters is None:
        return []
    return [float(v) for v in parameters]

Why this is a problem

_bind is on every evaluation entry point — expectation_value, expectation_value_and_gradient,
gradient, contract_partially, evolved_operator, and, most importantly, inside the closures
returned by expectation_value_functional and expectation_value_and_gradient_functional:

fn = self._simulator.expectation_value_functional(pare_threshold)
return lambda parameters=None: fn(self._bind(parameters))

Those functionals exist precisely to be called in an optimizer loop. So each iteration allocates N
PyFloat objects plus a list, then throws them away — for a parameter vector that the caller almost
always already holds as a contiguous float64 ndarray.

Suggested fix

Pass a contiguous float64 ndarray straight through when the caller supplied one, and widen the
return annotation from list[float] to Sequence[float] | np.ndarray:

if isinstance(parameters, np.ndarray):
    return np.ascontiguousarray(parameters, dtype=np.float64)
return [float(v) for v in parameters]

nanobind's stl/vector.h caster accepts any Python sequence, so the C++ signature
(const VecD &) is unchanged. To be clear about the size of the win: this removes the intermediate
Python objects, not the element-by-element conversion on the C++ side — list_caster still
iterates.

If the measured gain is small and the path matters, the larger version is to add an
nb::ndarray<const double, nb::ndim<1>, nb::c_contig> overload for the evaluation entry points in
src/monoprop/bindings/binder.h, which removes the per-element conversion too. That is a bigger
change to the binding surface and probably wants its own issue.

Verification

  • just bench serial before/after, comparing benches/results/REPORT.md; the functional-driven
    benchmarks are the ones to watch.
  • just test — in particular the paths that pass lists, tuples and Circuit objects rather than
    arrays, which must keep working unchanged.

Found by a code-reading review of the repository at 29a8050. No build tree was available, so the
analysis is from source inspection and should be confirmed by measurement.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions