-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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
String interning #5121
String interning #5121
Conversation
(rust_highfive has picked a reviewer for you, use r? to override) |
Mostly a drive-by comment, but I wonder if we should use strings for features at all... A natural idea is to enumerate all features in a package, and then use a number as a feature identifier. That way, a set of features could be stored as a bitset and fit into a single u64 most of the time.... |
@matklad So I see one small problem with that, If I require |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! The wins here are quite nice :)
Can you confirm that this doesn't regress the Servo use case much? In the sense of "big pre-resolved dependency graph still executes quickly".
Other than that I'm mildly worried about the memory leaks here. Cargo is often used as a library and I could imagine that if you're doing lots of resolutions of lots of crates the memory could get a little out of hand here over time. I wonder if instead, though, we could have a scheme like:
- There's an
Interner
structure which is used to get/set strings - Each
Config
stores anInterner
- There's a scoped thread local
Interner
set at the start of each Cargo thread, maybe byConfig
?
It'd be a bit trickier to get the interner to other threads in Cargo's backend but I think it's plausible in the long run even with an Interner
struct. For now I think this'd look like a scoped thread local during resolve (aka destroyed entirely after resolve is done).
Does that make sense though? Or do you think the static/leak strategy would still be prefererable?
src/cargo/core/interning.rs
Outdated
RwLock::new((Vec::new(), HashMap::new())); | ||
} | ||
|
||
#[derive(Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could the Ord
and PartialOrd
references be left out here? I think b/c they're comparing strings rather than contents it may end up being confusing down the road.
I don't really know what I think about having a The current implementation only uses If we make I did not understand your suggestion about So overall I don't know. |
Ok good to know on the resolve time! Sorry I think I wasn't clear before, but on second thought it's probably not worth it to have destruction here and it's probably best to just have this be a global. We can always work with it later and tweak it as necessary. If it's global though we can for sure never delete it, so want to go ahead and add a |
Just to confirm you are leaning toward using the global/leek strategy, and you are asking me to add code that uses unsafe to convert an Next time I get a chance I will play with some of the options, and put links hear for us to compare. |
Ah yeah I'm coming around to a global interner which leaks everything you add to it. I do like the idea of |
Note: that all the performance number should be rethought in lite of #5132 If we are cloning less often than improvements to performance of cloning will matter less. I assume that if two |
Holy cow, nice! |
In a test on rust-lang#4810 (comment) Before we got to 5000000 ticks in ~72 sec After we got to 5000000 ticks in ~65 sec
In a test on rust-lang#4810 (comment) Before we got to 5000000 ticks in ~65 sec After we got to 5000000 ticks in ~52 sec
A nice to you. You are the one that spotted and identified the problem! I didn't notice that the clones I was optimizing where not supposed to be there. I rebased and clean up this PR. Remarkably, it still pulls its weight! Next to try a version with |
I have worked on the scoped/ I have threaded it thru to some of the places that need it, |
Thanks for trying that out! I think it's clear the lazy_static approach will do fine for now. If you additionally don't see lock contention in the profile then I think it's for sure a fine approach. |
The However the fact that this can hand out Witch brings us to: 5000000 ticks in ~48 sec, 0r 104k ticks/sec |
Ok sounds good to me! And to be clear it's not that we want no unsafe code in Cargo we just want very little :). Cargo itself has a fair bit of transitive unsafe code |
I tried interning the features: If we want the speed of Eh2406@411355a and dont want the One way or the other, I am feeling ready to move onto the next avenue of attack. |
Ah yeah I'm fine incrementally improving the situation here rather than trying to do it all at once. Did you want to put some more changes in this PR and then I can r+? |
src/cargo/core/interning.rs
Outdated
use std::mem; | ||
use std::cmp::Ordering; | ||
|
||
pub fn leek(s: String) -> &'static str { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thing this may actually want to do is call s.into_boxed_str()
as that'll call shink_to_fit
and make sure we're not hanging on to any lingering bytes, but that can happen in a follow-up!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let boxed = s.into_boxed_str();
let ptr = boxed.as_ptr();
let len = boxed.len();
mem::forget(boxed);
Yes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
indeed!
Eventually this could even use Box::leak
:)
src/cargo/core/interning.rs
Outdated
cache.insert(s); | ||
InternedString { ptr: s.as_ptr(), len: s.len() } | ||
} | ||
pub fn to_inner(&self) -> &'static str { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this be listed as an implementation of Deref
to get all the fun methods on strings?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, thou they do not hash the same. I can use Deref
instead of to_inner
if you prefer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah I wouldn't worry too much about that
Looks great! I'd like to get the |
Er actually, there's also a bunch of test failures on Windows at least :( |
I removed the last optimization as I don't know how it was causing the test failures. And added your suggestions. I think let us merge this, and I'll rework the last commit as a separate PR. |
@bors: r+ |
📌 Commit 98480e8 has been approved by |
String interning This builds on the work from #5118. This interns the strings in the part of resolver that gets cloned a lot. In a test on #4810 (comment) Before we got to 1700000 ticks in ~(63 to 67) sec from #5118 After we got to 1700000 ticks in ~(42 to 45) sec The interning code itself would be much better with a `leak` function that converts a `String` to a `&'static str`. Something like: ```rust pub fn leek(s: String) -> &'static str { let ptr = s.as_ptr(); let len = s.len(); mem::forget(s); unsafe { let slice = slice::from_raw_parts(ptr, len); str::from_utf8(slice).unwrap() } } ``` but "there is no unsafe in Cargo", and I am not the best at unsafe. So I just `to_string` and lived with the extra copy. Is there a better way to hand out references? I assumed that `InternedString::new` world start appearing in profile result, and that we would want `PackageId`, and `Summary`, Et Al. to store the `InternedString`. That is why I put the interner in a shared folder. So far it is just used in the resolver. It may make sense for a lot more of the Strings to be interned, but with the extra copy... I have not explored it yet.
☀️ Test successful - status-appveyor, status-travis |
restructure `Activations` for better clone This builds on the work in #5121 When we last met we had: 5000000 ticks in ~48 sec, 0r 104k ticks/sec This small change brings us to: 5000000 ticks in ~21 sec, 0r 238k ticks/sec Edit: sorry for the large diff only the last commit is new. The rest are from #5121
This builds on the work from #5118. This interns the strings in the part of resolver that gets cloned a lot.
In a test on #4810 (comment)
Before we got to 1700000 ticks in ~(63 to 67) sec from #5118
After we got to 1700000 ticks in ~(42 to 45) sec
The interning code itself would be much better with a
leak
function that converts aString
to a&'static str
. Something like:but "there is no unsafe in Cargo", and I am not the best at unsafe. So I just
to_string
and lived with the extra copy. Is there a better way to hand out references?I assumed that
InternedString::new
world start appearing in profile result, and that we would wantPackageId
, andSummary
, Et Al. to store theInternedString
. That is why I put the interner in a shared folder. So far it is just used in the resolver. It may make sense for a lot more of the Strings to be interned, but with the extra copy... I have not explored it yet.