Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add no-hash-lookup way to retrieve the default interned value #13035

Merged
merged 4 commits into from
Aug 30, 2024

Conversation

jakelishman
Copy link
Member

Summary

This makes it possible to do Interner::get_default() without any value, in order to retrieve a key pointing to the default allocation without making any hash lookups. While the hashing and equality check of the default allocation is typically very cheap (like the empty slice), acquiring it still generally required a function call, which often needed to be paid frequently.

Details and comments

Built on top of #13033, so depends on it.

@jakelishman jakelishman added on hold Can not fix yet Changelog: None Do not include in changelog Rust This PR or issue is related to Rust code in the repository labels Aug 25, 2024
@coveralls
Copy link

coveralls commented Aug 25, 2024

Pull Request Test Coverage Report for Build 10623649976

Warning: This coverage report may be inaccurate.

This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.

Details

  • 11 of 11 (100.0%) changed or added relevant lines in 2 files are covered.
  • 263 unchanged lines in 4 files lost coverage.
  • Overall coverage decreased (-0.04%) to 89.176%

Files with Coverage Reduction New Missed Lines %
crates/accelerate/src/two_qubit_decompose.rs 1 90.82%
crates/qasm2/src/lex.rs 3 91.48%
crates/qasm2/src/parse.rs 18 96.69%
crates/circuit/src/dag_circuit.rs 241 89.02%
Totals Coverage Status
Change from base Build 10579458520: -0.04%
Covered Lines: 71633
Relevant Lines: 80328

💛 - Coveralls

@kevinhartman kevinhartman self-assigned this Aug 26, 2024
@jakelishman jakelishman removed the on hold Can not fix yet label Aug 27, 2024
@jakelishman jakelishman marked this pull request as ready for review August 27, 2024 14:17
@jakelishman jakelishman requested a review from a team as a code owner August 27, 2024 14:17
@qiskit-bot
Copy link
Collaborator

One or more of the following people are relevant to this code:

  • @Qiskit/terra-core
  • @kevinhartman
  • @mtreinish

This makes it possible to do `Interner::get_default()` without any
value, in order to retrieve a key pointing to the default allocation
without making any hash lookups.  While the hashing and equality check
of the default allocation is typically very cheap (like the empty
slice), acquiring it still generally required a function call, which
often needed to be paid frequently.
Copy link
Contributor

@kevinhartman kevinhartman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is again, very cool.

I don't think it's a problem that the type needs to implement Default. When Rust has specialization, it'd be neat to have two new impls that either initialize a default or not, and then constrain the get_default method to be applicable only for T: Default, like you mention in the docstring. But T: Default at the struct-level feels entirely fine to me.

crates/circuit/src/interner.rs Show resolved Hide resolved
@jakelishman
Copy link
Member Author

Another API alternative here is to add an implementation of Default / explicit method to Interned<T> where T: ?Sized + ToOwned, <T as ToOwned>::Owned: Default that constructs the same struct. For now I just put the method on the interner for the symmetry with get, and because I wasn't entirely sure how the type inference would play out if it was on Interned (I didn't want the caller to have to turbo-fish the default constructor). Feels like it could easily go either way (or both ways).

@kevinhartman
Copy link
Contributor

Another API alternative here is to add an implementation of Default / explicit method to Interned where T: ?Sized + ToOwned, ::Owned: Default that constructs the same struct. For now I just put the method on the interner for the symmetry with get, and because I wasn't entirely sure how the type inference would play out if it was on Interned (I didn't want the caller to have to turbo-fish the default constructor). Feels like it could easily go either way (or both ways).

I see what you mean. I think having both could be nice, but the explicit method seems like it'd be easier for folks to find, which may result in us seeing less of .insert(&[]) in new PRs.

@jakelishman
Copy link
Member Author

Added the symmetric method in 7cc72b1.

@kevinhartman
Copy link
Contributor

What was the reason not to impl Default itself for Interned<T> with constraints on T?

Otherwise, looks good.

@jakelishman
Copy link
Member Author

I didn't think super hard about it, I think my default API position is just to define concrete methods rather than hide new functionality in trait implementations.

Thinking more now, I'd say that "the interned key of the default value" is not necessarily the same thing as the "default interned key", but that said: maybe my position on the trait is skewed, because imo integer types shouldn't have a default, and neither should an interner key. Judging by the API choices in both Python and Rust, though, I'm in the minority there (e.g. Python's int() and Rust's impls of Default for integers).

@kevinhartman
Copy link
Contributor

I didn't think super hard about it, I think my default API position is just to define concrete methods rather than hide new functionality in trait implementations.

I think in this case, I would actually rather we just have the get_default like you had it before. If you want to also implement Default, I'd be on board.

Thinking more now, I'd say that "the interned key of the default value" is not necessarily the same thing as the "default interned key"

Hmm. It's late in the night so I'm probably not picking up on the subtly, but wouldn't that break the premise of us using Default as the trait bound for this whole thing anyhow? I guess I can see what you mean, but I'd think it'd follow that the Interner should decide what its keys are, so in that case we should probably not let Interned structs be constructed on their own.

@jakelishman
Copy link
Member Author

I'm fine to remove it too, just let me know. If you want me to turn it into an actual Default impl that's fine too, since the explicit method will still exist (just be called default).

I'd say that Interned and Interner are so intrinsically linked that it makes little difference who decides what here - the two structs absolutely have to know how each other work in order not to violate each other's contracts. The "default" key being index 0 is fairly core to how the system works, and any risk of Interned and Interner getting out of sync on that numbering is the same as it was before between the Interner::new and Interner::get_default methods. I don't think there's a new risk by having the Interned method because I forced the creation routines to be consistent by having get_default call it - the risk of divergence with the Interner constructor remains the same (and personally I'm not worried about it).

@kevinhartman
Copy link
Contributor

I'm not worried either 🙂. But tl;dr, I think you should just remove Interned::of_default for now.

I don't think there's a new risk by having the Interned method because I forced the creation routines to be consistent by having get_default call it

What I meant was more that we're locking ourselves out of creating default Interned keys statefully (in the context of an Interner) by having a method to do it directly without an Interner. Whereas your get_default method on Interner takes a &self, so the default it produces could make use of some internal state. Does that matter? Probably not, but I don't see a reason to have both Interner::get_default and Interned::of_default (it doesn't feel more discoverable from an API perspective), and get_default seems safer and more in line with the idea that we're asking an Interner for a local reference to something within its jurisdiction.

@jakelishman
Copy link
Member Author

jakelishman commented Aug 29, 2024

Ok, it's gone again in b96698b.

(Fwiw, given none of this Rust stuff is in any way public, I'm not really worried about locking ourselves out of anything - this PR's parent just changed the entire Interner interface, for example. Keeping the constructor and the index inside the Interned type private from ourselves stops us from leaking the "the default index is 0" abstraction anywhere - and if people were making assumptions about the indices, we'd have rather larger problems anyway.)

Copy link
Contributor

@kevinhartman kevinhartman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, thanks for the changes!

@kevinhartman kevinhartman added this pull request to the merge queue Aug 30, 2024
Merged via the queue into Qiskit:main with commit 31fbcac Aug 30, 2024
15 checks passed
@jakelishman jakelishman deleted the interner-zero branch August 30, 2024 15:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Changelog: None Do not include in changelog Rust This PR or issue is related to Rust code in the repository
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants