Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.
Sign upChanged HashMap's internal layout. Cleanup. #21973
Conversation
rust-highfive
assigned
Gankro
Feb 5, 2015
pczarn
force-pushed the
pczarn:hash_map-mem-layout
branch
from
dc6bc2d
to
64e8b91
Feb 5, 2015
cgaebel
reviewed
Feb 5, 2015
| // middle of a cache line, this strategy pulls in one cache line of hashes on | ||
| // most lookups (64-byte cache line with 8-byte hash). I think this choice is | ||
| // pretty good, but α could go up to 0.95, or down to 0.84 to trade off some | ||
| // space. | ||
| // | ||
| // > Wait, what? Where did you get 1-α^k from? |
This comment has been minimized.
This comment has been minimized.
cgaebel
reviewed
Feb 5, 2015
| @@ -126,23 +150,10 @@ fn test_resize_policy() { | |||
| // α^3, etc. Therefore, the odds of colliding k times is α^k. The odds of NOT | |||
| // colliding after k tries is 1-α^k. | |||
| // | |||
| // The paper from 1986 cited below mentions an implementation which keeps track | |||
This comment has been minimized.
This comment has been minimized.
cgaebel
reviewed
Feb 5, 2015
| @@ -220,11 +231,12 @@ fn test_resize_policy() { | |||
| /// | |||
| /// Relevant papers/articles: | |||
| /// | |||
| /// 1. Pedro Celis. ["Robin Hood Hashing"](https://cs.uwaterloo.ca/research/tr/1986/CS-86-14.pdf) | |||
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
pczarn
Feb 5, 2015
Author
Contributor
To avoid confusion. It doesn't seem relevant to this particular implementation
This comment has been minimized.
This comment has been minimized.
cgaebel
Feb 5, 2015
Contributor
I think it's relevant. It's the seminal paper on robin hood hashing, no?
pczarn
force-pushed the
pczarn:hash_map-mem-layout
branch
from
64e8b91
to
407572c
Feb 5, 2015
cgaebel
reviewed
Feb 5, 2015
| let size = table.size(); | ||
| let mut probe = Bucket::new(table, hash); | ||
| let mut probe = if let Some(probe) = Bucket::new(table, hash) { |
This comment has been minimized.
This comment has been minimized.
cgaebel
reviewed
Feb 5, 2015
| if is_match(full.read().0) { | ||
| return FoundExisting(full); | ||
| if is_match(bucket.read().1) { | ||
| return InternalEntry::Occupied(OccupiedEntryState { |
This comment has been minimized.
This comment has been minimized.
cgaebel
Feb 5, 2015
Contributor
I don't know what the changes in this function have to do with in-place hashmap growth.
cgaebel
reviewed
Feb 5, 2015
|
|
||
| loop { | ||
| let (old_hash, old_key, old_val) = bucket.replace(hash, k, v); | ||
| let (old_hash, old_key, old_val) = { |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
pczarn
Feb 5, 2015
Author
Contributor
I tried to get rid of non-essential methods in the table module. Besides, replace was used only from here.
This comment has been minimized.
This comment has been minimized.
|
Can this PR be split up into "In-place growth for HashMap" and "Cleanup"? |
This comment has been minimized.
This comment has been minimized.
|
Just leaving a note: I am concerned about how this change will negatively affect memory consumption for certain choices of K and V. That said, I think speed is more important than memory consumption, to some limit. |
This comment has been minimized.
This comment has been minimized.
|
It will also affect the performance of the "keys" and "values" iterators. |
pczarn
force-pushed the
pczarn:hash_map-mem-layout
branch
from
49bfe41
to
f495fb8
Feb 7, 2015
This comment has been minimized.
This comment has been minimized.
|
Don't the posted benchmark results indicate that insertion ( (This outcome makes some sense to me, at least for a heavily loaded table, since the unused interleaved values for non-matching keys are going to occupy portions of the cache line when we are doing a probe sequence over a series of keys.)
I don't know how to evaluate whether the gain is worth the cost here. I just want to make sure that everyone is on board for this shift (or find out if my interpretation of the results is wrong). |
This comment has been minimized.
This comment has been minimized.
|
Just a note that I believe @pczarn is currently reworking this PR. |
pczarn
force-pushed the
pczarn:hash_map-mem-layout
branch
from
f495fb8
to
6218462
Feb 16, 2015
This comment has been minimized.
This comment has been minimized.
|
I did the reworking. Some small details still need attention.
Sounds bad. Can you find a real world example of this? To keep the performance the same, I could make hashmap use two allocations, Here's a relevant post on data layout: http://www.reedbeta.com/blog/2015/01/12/data-oriented-hash-table/ This is how benchmark results have changed:
@pnkfelix: Lookup has become slower after the first commit because of refactoring. Keep in mind that the benchmark does 1000 lookups per iteration and the difference between 41.7ns and 42.4ns per lookup is small. The improvement from in-place growth is suprisingly low. I'll have to check why. |
This comment has been minimized.
This comment has been minimized.
|
@pczarn The performance of key/value iterators is just because with the current design they walk over a compact array, and with your proposed design they eat twice as much cache as they do this. If doing a small per-key or per-value operation, this essentially halves memory bandwidth. |
This comment has been minimized.
This comment has been minimized.
|
Well strictly speaking it's already walking over the hashes checking for |
This comment has been minimized.
This comment has been minimized.
|
Oh shoot, I let this slip through the cracks. Needs a rebase (hopefully the last one, since we should be done with crazy API churn). |
pczarn
force-pushed the
pczarn:hash_map-mem-layout
branch
from
6218462
to
316b300
Feb 21, 2015
This comment has been minimized.
This comment has been minimized.
|
Updated. I'm going to test and make corrections when new snapshots land. So iteration over small keys/values is already 2x-4x more cache-intensive than in an array. With larger values, like in To avoid the issue, keys/values can be stored in an array such as |
pczarn
force-pushed the
pczarn:hash_map-mem-layout
branch
from
627837f
to
edcddb1
Feb 26, 2015
pczarn
referenced this pull request
Feb 26, 2015
Closed
llvm::StructType::getElementType: Assertion `N < NumContainedTys && "Element number out of range!"' failed. #21721
pczarn
force-pushed the
pczarn:hash_map-mem-layout
branch
from
edcddb1
to
b7317cd
Feb 27, 2015
This comment has been minimized.
This comment has been minimized.
|
Done, tested. |
This comment has been minimized.
This comment has been minimized.
|
Great! Will review tonight. |
This comment has been minimized.
This comment has been minimized.
|
Ack, a few of my issues are addressed by later commits. Doing this commit-by-commit isn't the right strategy here. Shifting gears. |
Gankro
reviewed
Feb 27, 2015
| // inform rustc that in fact instances of K and V are reachable from here. | ||
| marker: marker::PhantomData<(K,V)>, | ||
| // NB. The table will probably need manual impls of Send and Sync if this | ||
| // field ever changes. |
This comment has been minimized.
This comment has been minimized.
Gankro
Feb 27, 2015
Contributor
I don't think that needs saying? That's true for pretty much all Uniques.
Gankro
reviewed
Feb 27, 2015
| @@ -69,50 +67,42 @@ const EMPTY_BUCKET: u64 = 0u64; | |||
| pub struct RawTable<K, V> { | |||
| capacity: usize, | |||
| size: usize, | |||
| hashes: Unique<u64>, | |||
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
|
pczarn
force-pushed the
pczarn:hash_map-mem-layout
branch
from
3f67ec1
to
776d23e
Apr 29, 2015
This comment has been minimized.
This comment has been minimized.
|
@Gankro (this was rebased and needs review) |
This comment has been minimized.
This comment has been minimized.
|
@Manishearth Yes I tried to get someone else to review over two months ago. :/ Today's that last day of my pseudo-vacation so I'm free to tackle this again this week. |
This comment has been minimized.
This comment has been minimized.
|
Alright I've started just reading the full source at the last commit's hash just because the changes have been so comprehensive that it borders on a rewrite. Was Deref vs Borrow ever fully addressed? Borrow is generally just a trait for Deref-like+Hash/Eq/Ord equivalence. |
Gankro
reviewed
May 12, 2015
| pub fn into_bucket(self) -> Bucket<K, V, M> { | ||
| /// Duplicates the current position. This can be useful for operations | ||
| /// on two or more buckets. | ||
| pub fn stash(self) -> Bucket<K, V, Bucket<K, V, M, S>, S> { |
This comment has been minimized.
This comment has been minimized.
Gankro
reviewed
May 15, 2015
| } | ||
| /// Pointer to one-past-the-last key-value pair. | ||
| pub fn as_mut_ptr(&self) -> *mut (K, V) { | ||
| unsafe { self.middle.get() as *const _ as *mut _ } |
This comment has been minimized.
This comment has been minimized.
Gankro
reviewed
May 15, 2015
| fn checked_size_generic<K, V>(capacity: usize) -> usize { | ||
| let size = size_generic::<K, V>(capacity); | ||
| let elem_size = size_of::<(K, V)>() + size_of::<SafeHash>(); | ||
| assert!(size >= capacity.checked_mul(elem_size).expect("capacity overflow"), |
This comment has been minimized.
This comment has been minimized.
Gankro
May 15, 2015
Contributor
I'm confused why this assert instead of just constructing size using checked ops?
Gankro
reviewed
May 15, 2015
|
|
||
| #[inline] | ||
| fn align<K, V>() -> usize { | ||
| cmp::max(mem::min_align_of::<(K, V)>(), mem::min_align_of::<u64>()) |
This comment has been minimized.
This comment has been minimized.
Gankro
reviewed
May 15, 2015
| } | ||
|
|
||
| /// A newtyped RawBucket. Not copyable. | ||
| pub struct RawFullBucket<K, V, M>(RawBucket<K, V>, PhantomData<M>); |
This comment has been minimized.
This comment has been minimized.
Gankro
reviewed
May 18, 2015
| let mut m = HashMap::new(); | ||
|
|
||
| for i in range_inclusive(1, 1000) { | ||
| m.insert(i, (String::new(), String::new())); |
This comment has been minimized.
This comment has been minimized.
Gankro
reviewed
May 18, 2015
| impl<K, V, M> InternalEntry<K, V, M> { | ||
| fn into_option(self) -> Option<FullBucket<K, V, M>> { | ||
| match self { | ||
| InternalEntry::Occupied(bucket) => Some(bucket.elem), |
This comment has been minimized.
This comment has been minimized.
Gankro
reviewed
May 18, 2015
| } | ||
|
|
||
| // If the hash doesn't match, it can't be this one.. | ||
| if hash == full.hash() { | ||
| if hash == *bucket.read().0 { |
This comment has been minimized.
This comment has been minimized.
Gankro
reviewed
May 18, 2015
| TableRef(_) => None | ||
| } | ||
| // Performs insertion with relaxed requirements. | ||
| // The caller should ensure that invariants of Robin Hood linear probing hold. |
This comment has been minimized.
This comment has been minimized.
Gankro
reviewed
May 18, 2015
| let mut buckets = Bucket::new(table, *hash as usize).unwrap(); | ||
| let ib = buckets.index(); | ||
|
|
||
| while buckets.index() != ib + cap { |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Gankro
reviewed
May 18, 2015
| @@ -596,6 +590,8 @@ impl<K, V, S> HashMap<K, V, S> | |||
| } | |||
|
|
|||
| /// Returns the number of elements the map can hold without reallocating. | |||
| /// This value may be lower than the real number of elements the map will | |||
| /// hold before reallocating. | |||
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
So, to the best of my knowledge this code seems to be correct, but I have strong reservations about where we're going with respect to the proliferation of type-complexity. I used to be able to grok this code pretty ok: We have HashMap, RawTable, and some Buckets. Now everything's super generic over some "M" type and there's Partial* types all over. It's not clear that these abstractions are pulling their weight: are they preventing real bugs or enabling simpler or more maintainable code? |
This comment has been minimized.
This comment has been minimized.
|
I share that sentiment. |
alexcrichton
added
the
T-libs
label
May 26, 2015
This comment has been minimized.
This comment has been minimized.
|
It's been a couple weeks with no response on any of my comments, and I'm not a huge fan of the general design changes. As such I'm closing this for now. We can continue discussion on the PR and maybe re-open if we get somewhere. |
pczarn commentedFeb 5, 2015
Changes HashMap's memory layout from
[hhhh...KKKK...VVVV...]to[KVKVKVKV...hhhh...]. This makes in-place growth easy to implement (and efficient).The removal of
find_with_or_insert_withhas made more cleanup possible.20 benchmark runs, averaged:
thanks to @Gankro for the Entry interface, and to @thestinger for improving jemalloc's in-place realloc!
cc @cgaebel
r? @Gankro