Skip to content

Find stored values such as f[1] = a through an index, not by trying every rule - #2013

Open
tombouctou wants to merge 4 commits into
Mathics3:masterfrom
tombouctou:fix-literal-rule-lookup
Open

tombouctou wants to merge 4 commits into
Mathics3:masterfrom
tombouctou:fix-literal-rule-lookup

Conversation

@tombouctou

Copy link
Copy Markdown
Collaborator

Evaluating f[x] tries the down- (or sub-) values of f one by one. With n stored values (f[1] = a, f[2] = b, …), every lookup therefore takes time proportional to n: about 8 ms with 8000 values. Code that memoizes many values becomes quadratic.

This PR gives a rule list an index of its literal rules, keyed by the structure of the left-hand side. A literal rule is an unconditional rule whose left-hand side is made of symbols, integers and strings, under a head that is not Orderless, Flat or OneIdentity. Only the literal rule that is SameQ to the expression, if there is one, is tried. It is tried at its own place among the other rules, which are still tried in order as before, so rule precedence does not change.

  • Where a literal rule's place comes from: the sort keys by which insert_rule keeps the list ordered. A list that is not in that order (for example, after DownValues[f] = {...}), or in which a literal rule shares its sort key with another rule, is not indexed.
  • How the index is kept in step with its list: the index belongs to the list object it was built for. A list that is replaced (by Clear or DownValues[f] = ...) gets a new index when one is needed. Inserting a literal rule updates the index; removing a rule, or inserting any other kind of rule, drops it.
  • Small lists: lists of fewer than 8 rules are scanned as before.

With 1000 stored values, f[500] now takes about 10 µs instead of 1.5 ms.

Checks:

  • Tests: test_stored_values_lookup in test/core/test_definitions.py.
  • Full pytest and doctests also pass in a debug run that compared every indexed lookup with the full scan; there were 0 mismatches.

Context: we found this while running the lambda-algebra homology computation from the Toda-bracket project on Mathics3 (see #2001). That code stores tens of thousands of values like BasisFunction[W][elem] = row. On one core, the change makes loading the definitions about 10% faster, and the gain grows as values accumulate.

@mmatera, you mentioned on #2001 that you are reworking rule ordering and dispatch. If this overlaps with that work, I'm glad to rebase it onto your branch, or to close it in favour of yours.

🤖 Generated with Claude Code

…very rule

Evaluating f[x] tried the down- (or sub-) values of f one by one, so with n
stored values every lookup took time proportional to n: about 8 ms with
8000 values, and code that memoizes many values became quadratic.

A rule list now gets an index of its literal rules (unconditional rules
whose left-hand side is a literal expression made of symbols, integers and
strings, under a head that is not Orderless, Flat or OneIdentity) by the
structure of the left-hand side. Only the literal rule SameQ to the
expression, if any, is tried, at its place among the other rules, which
are tried in order as before.

- The place comes from the sort keys by which insert_rule keeps the list
  ordered. A list not in that order (DownValues set as a whole), or where
  a literal rule shares its sort key with another rule, is not indexed.
- The index belongs to the list object it was built for: a list replaced
  (Clear, DownValues[f] = ...) gets a new one when needed. Inserting a
  literal rule updates it; removing a rule or inserting another kind of
  rule drops it.
- Lists of fewer than 8 rules are scanned as before.

A lookup in a definition of 1000 stored values now takes about 10 us
instead of 1.5 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rocky

rocky commented Oct 22, 2026

Copy link
Copy Markdown
Member

@tombouctou if we'd fix those merge conflicts, I would like to get this fix in. I looked at the conflicts, but resolving these is not something that is easily done using GitHub's web interface.

…okup

# Conflicts:
#	mathics/core/definitions.py
#	test/core/test_definitions.py
@rocky

rocky commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

While this PR is good, as noted elsewhere and in other issues or PRs, the code has a problem with how it handles the order of definitions.

So the top-level question is whether we merge this and iterate, or extend this here to handle that additional situation.

The basic problem is in LiteralRuleIndex.build, where the code tracks literal precedence keys with a set:

literal_keys.add(key)

Duplicate assignments get lost.

An example of this:

f[1] = a
f[2] = b
f[x_] := something

These get the same literal index, right? But WMA needs to know to try rule matching in the order given by the assignments.

@rocky
rocky requested a review from mmatera October 22, 2026 14:47
…ral rules

Rename literal_keys to literal_precedences and explain that many literal
rules share one sort key, which is only checked against the keys of the
other rules. Test stored values assigned both before and after a pattern
rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tombouctou

Copy link
Copy Markdown
Collaborator Author

Thanks for looking closely at this. On literal_keys:

That set doesn't hold the rules. It holds their sort keys (pattern_precedence). The rules themselves go into table, keyed by the structure of the left-hand side, so f[1] and f[2] each keep their own entry. Many literal rules share one sort key, and collapsing them into a set loses nothing, because the order of literal rules among themselves never matters: at most one of them is SameQ to the expression. The set is used for one check only, that no literal rule shares its sort key with a non-literal rule. If one does, the keys can't tell where the literal rule goes among the others, so the list is not indexed and is scanned as before. All non-literal rules, including ties, are tried in the list's own order.

For your example, f[1] and f[2] sort before f[x_] whatever order they were assigned in, which matches WMA:

Do[f[i] = i, {i, 20}]; f[x_] := gen[x]; {f[1], f[2], f[21]}                    (* {1, 2, gen[21]} *)
f[x_] := gen[x]; Do[f[i] = i, {i, 20}]; f[2] = two; {f[1], f[2], f[21]}     (* {1, two, gen[21]} *)

I added both as tests, renamed the set to literal_precedences, and wrote this explanation into the code (77cd32e).

There is an assignment-order bug, but it is on master and the index doesn't touch it. insert_rule uses insort_left, so a newer rule is tried before an older one with the same precedence, while WMA keeps assignment order:

g[x_?EvenQ] := even[x]; g[x_?PrimeQ] := prime[x]; g[2]
(* WMA: even[2]   Mathics3 (master and this PR): prime[2] *)

This PR gives the same output as master here. If the tie-break is changed, the index stays correct: it never decides an order between two non-literal rules, and a literal rule that ties with a non-literal one turns the index off.

So I'd suggest merging this and fixing the tie-break separately, either as a small follow-up PR from me or as part of #1989 if @mmatera is already changing that code.

@rocky

rocky commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

...
This PR gives the same output as master here. If the tie-break is changed, the index stays correct: it never decides an order between two non-literal rules, and a literal rule that ties with a non-literal one turns the index off.

So I'd suggest merging this and fixing the tie-break separately, either as a small follow-up PR from me or as part of #1989 if @mmatera is already changing that code.

Ok. LGTM. Thanks for the follow-up. @mmatera: your thoughts?

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

performance Something is slow and you have an idea of how to fix it rule_dispatch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants