perf: Optimize resolver hash option intersection - #14106
Conversation
|
I added a targeted benchmark for the changed resolver path. It constructs real Environment: macOS, Python 3.14.6.
The benchmark is intentionally scoped to the resolver merge operation this PR changes. It models hash-checking scenarios where requirements/constraints carry many Benchmark script: from __future__ import annotations
import gc
import statistics
import time
from pip._vendor.packaging.requirements import Requirement
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._internal.req.req_install import InstallRequirement
from pip._internal.resolution.resolvelib.base import Constraint
from pip._internal.utils.hashes import Hashes
def make_ireq(values: list[str]) -> InstallRequirement:
return InstallRequirement(
Requirement("example"),
comes_from=None,
hash_options={"sha256": values},
)
def run_case(name: str, *, base_size: int, incoming_size: int, overlap_start: int, rounds: int, repeat: int) -> None:
base_values = [f"{i:064x}" for i in range(base_size)]
incoming_values = [f"{i:064x}" for i in range(overlap_start, overlap_start + incoming_size)]
constraint = Constraint(SpecifierSet(), Hashes(), {"sha256": base_values}, frozenset())
incoming = [make_ireq(incoming_values) for _ in range(rounds)]
expected = [value for value in incoming_values if value in set(base_values)]
assert (constraint & incoming[0]).hash_options == {"sha256": expected}
samples = []
gc.disable()
try:
for _ in range(repeat):
start = time.perf_counter()
for ireq in incoming:
constraint & ireq
samples.append(time.perf_counter() - start)
finally:
gc.enable()
print(f"{name}: min={min(samples):.6f}s median={statistics.median(samples):.6f}s rounds={rounds}")
run_case(
"large-constraint-small-incoming",
base_size=5000,
incoming_size=20,
overlap_start=4500,
rounds=2000,
repeat=7,
)
run_case(
"similar-sized-overlap",
base_size=1000,
incoming_size=1000,
overlap_start=500,
rounds=500,
repeat=7,
) |
|
The change is behavior preserving, but the benchmark doesn't reflect real world scenarios.
The benchmark also passes an empty I'm not sure this is worth the churn, in general I would like to see benchmarks that reflect real world scenarios if you are going to make many performance PRs. |
|
Thanks, you’re right. The original benchmark bypassed the real I pushed With the updated |
Fixes #14105.
Constraint.__and__()intersects per-algorithm hash options by iteratingother.hash_options[alg]and checking each value againstself.hash_options[alg]. Since both are lists, that membership check is linear, making the intersection O(n*m) for each shared hash algorithm.This keeps the existing result order from
other.hash_options[alg], but builds a temporary set fromself.hash_options[alg]so membership checks are O(1), reducing the intersection to O(n+m).Tests run: