GithubHelp home page GithubHelp logo

flurry's Introduction

Crates.io Documentation Codecov Dependency status

A port of Java's java.util.concurrent.ConcurrentHashMap to Rust.

The port is based on the public domain source file from JSR166 as of CVS revision 1.323, and is jointly licensed under MIT and Apache 2.0 to match the Rust API guidelines. The Java source files are included under the jsr166/ subdirectory for easy reference.

The port was developed as part of a series of live coding streams kicked off by this tweet.

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

flurry's People

Contributors

arthamys avatar chapeupreto avatar cuviper avatar danielsanchezq avatar dependabot[bot] avatar domenicquirl avatar garkgarcia avatar ian-p-cooke avatar ibraheemdev avatar jakkusakura avatar jhinch avatar jimvdl avatar jmchacon avatar jonathangb avatar jonhoo avatar joshka avatar mathiaspius avatar notstirred avatar others avatar ralfjung avatar rtkay123 avatar simenb avatar soruh avatar srijs avatar stupremee avatar thallada avatar tudyx avatar twe4ked avatar voidc avatar wasabi375 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

flurry's Issues

Racy test failure: treeifying a Moved entry

$ cargo test --test jdk concurrent_associate::test_concurrent_insert
thread '<unnamed>' panicked at 'internal error: entered unreachable code: treeifying a Moved entry', /home/jon/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/macros.rs:16:9
stack backtrace:
   0: backtrace::backtrace::libunwind::trace
             at /cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.46/src/backtrace/libunwind.rs:86
   1: backtrace::backtrace::trace_unsynchronized
             at /cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.46/src/backtrace/mod.rs:66
   2: std::sys_common::backtrace::_print_fmt
             at src/libstd/sys_common/backtrace.rs:78
   3: <std::sys_common::backtrace::_print::DisplayBacktrace as core::fmt::Display>::fmt
             at src/libstd/sys_common/backtrace.rs:59
   4: core::fmt::write
             at src/libcore/fmt/mod.rs:1069
   5: std::io::Write::write_fmt
             at src/libstd/io/mod.rs:1504
   6: std::sys_common::backtrace::_print
             at src/libstd/sys_common/backtrace.rs:62
   7: std::sys_common::backtrace::print
             at src/libstd/sys_common/backtrace.rs:49
   8: std::panicking::default_hook::{{closure}}
             at src/libstd/panicking.rs:198
   9: std::panicking::default_hook
             at src/libstd/panicking.rs:218
  10: std::panicking::rust_panic_with_hook
             at src/libstd/panicking.rs:511
  11: rust_begin_unwind
             at src/libstd/panicking.rs:419
  12: std::panicking::begin_panic_fmt
             at src/libstd/panicking.rs:373
  13: flurry::map::HashMap<K,V,S>::treeify_bin
             at /home/jon/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/macros.rs:16
  14: flurry::map::HashMap<K,V,S>::put
             at ./src/map.rs:1952
  15: flurry::map::HashMap<K,V,S>::insert
             at ./src/map.rs:1625
  16: jdk::concurrent_associate::insert
             at tests/jdk/concurrent_associate.rs:24
  17: core::ops::function::Fn::call
             at /home/jon/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libcore/ops/function.rs:72
  18: jdk::concurrent_associate::test_once::{{closure}}
             at tests/jdk/concurrent_associate.rs:52
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
test concurrent_associate::test_concurrent_insert ... FAILED

Implement Drain and IntoIterator

We should implement the Drain and IntoIterator traits on HashMap!

Part of what is tricky about Drain is the contract for what happens when the user "does something weird". For example, what happens if I write the following code:

std::mem::forget(map.drain());

Is map now empty or full? What happens if I read one element and then drop the Drain? Take a look at hashbrown::RawDrain for some inspiration. I'm genuinely not sure what the best way to express this is a concurrent map.

@soruh began an implementation in #33, but it has since run out of time to work on it. It may still be useful to draw inspiration from though, and has a fair amount of good discussion around the challenges involved.

Implement and run a "serious" concurrent hash table benchmarking suite

It is easy to write simple benchmarks for concurrent hash tables. But when these data-structures hit real world data is when we learn how they truly perform. There has been much research on concurrent hash tables, and how to benchmark them, so let's build a benchmark (or set of benchmarks rather) inspired by that work! We can then run that benchmark against the various concurrent map implementations out there in Rust world, and (hopefully) get some useful data out of them.

I'm hoping this thread can act as a staging ground for designing this benchmark, and that it can then be forked off into its own stand-alone project.

Work of note to get started (please let me know if you know of others):

/cc @xacrimon

Hashmap lacks of get_mut API

The current design of hashmap API doesn't support nested structures well.

For example:

pub struct ConnectionPool {
  slots: FlurryHashMap<String, ConnectionSlot>,
}

pub struct ConnectionSlot {
  endpoint: String,
  connections: Vec<Connection>,
}

pub fn remove<S: AsRef>(&self, endpoint: S, connection: &Connection)
  let guard = self.slots.guard();
  self.slots.get(endpoint.as_ref(), &guard).map(|slot| {
    slot.remove(connection); // cannot borrow `*slot` as mutable, as it is behind a `&` reference
  });
}

map::tree_bins::concurrent_tree_bin: attempt to subtract with overflow

Hit two of these at the same time. This is after #85.

test map::tree_bins::concurrent_tree_bin ...
thread '<unnamed>' panicked at 'attempt to subtract with overflow', src/map.rs:1163:17
stack backtrace:
...
  13: core::panicking::panic
             at src/libcore/panicking.rs:54
  14: flurry::map::HashMap<K,V,S>::add_count
             at src/map.rs:1163
  15: flurry::map::HashMap<K,V,S>::replace_node
             at src/map.rs:2626
  16: flurry::map::HashMap<K,V,S>::remove
             at src/map.rs:2366
  17: flurry::map::tree_bins::concurrent_tree_bin::{{closure}}
             at src/map.rs:3429
thread '<unnamed>' panicked at 'attempt to add with overflow', src/map.rs:1159:17
...
  13: core::panicking::panic
             at src/libcore/panicking.rs:54
  14: flurry::map::HashMap<K,V,S>::add_count
             at src/map.rs:1159
  15: flurry::map::HashMap<K,V,S>::put
             at src/map.rs:1970
  16: flurry::map::HashMap<K,V,S>::insert
             at src/map.rs:1625
  17: flurry::map::tree_bins::concurrent_tree_bin::{{closure}}
             at src/map.rs:3419

Optimize garbage collection

In the discussion about performance improvements (#50), a point that arose repeatedly is the impact of the crossbeam_epoch-based garbage collection we use to track map entries (nodes and values).

There are several angles from which we might look at reducing this impact. This issue is meant to be for discussion and changes around how and when our code interacts with garbage collection. Ideas floated in #72 (review) and previous comments may serve as a starting point. They include:

  • moving direct interactions with crossbeam_epoch, e.g. in the form of defer_destroy, out of critical sections in favour of a simpler collection mechanism for things to be deleted. The collected entities could then be handed of to garbage collection after dropping the lock for the critical section.
    It is yet unclear whether this would yield a significant improvement over calling defer_destroy directly from within the critical section (as happens currently), as crossbeam essentially also stores a closure for the drop and takes some measures to optimize the storage of these closures (for example, a deferred closure only requires a heap allocation if it exceeds a certain size).
  • optimizing defer_destroy and its usage itself. See again #72 (review)

Use of deprecated `compare_and_swap` and `spin_loop_hint`

Please replace them

warning: use of deprecated function `std::sync::atomic::spin_loop_hint`: use hint::spin_loop instead
 --> src/node.rs:2:26
  |
2 | use core::sync::atomic::{spin_loop_hint, AtomicBool, AtomicI64, Ordering};
  |                          ^^^^^^^^^^^^^^
  |
  = note: `#[warn(deprecated)]` on by default

warning: use of deprecated function `std::sync::atomic::spin_loop_hint`: use hint::spin_loop instead
   --> src/node.rs:402:13
    |
402 |             spin_loop_hint();
    |             ^^^^^^^^^^^^^^

warning: use of deprecated associated function `std::sync::atomic::AtomicIsize::compare_and_swap`: Use `compare_exchange` or `compare_exchange_weak` instead
   --> src/map.rs:485:30
    |
485 |             if self.size_ctl.compare_and_swap(sc, -1, Ordering::SeqCst) == sc {
    |                              ^^^^^^^^^^^^^^^^

warning: use of deprecated associated function `std::sync::atomic::AtomicIsize::compare_and_swap`: Use `compare_exchange` or `compare_exchange_weak` instead
   --> src/map.rs:602:22
    |
602 |                     .compare_and_swap(size_ctl, -1, Ordering::SeqCst)
    |                      ^^^^^^^^^^^^^^^^

warning: use of deprecated associated function `std::sync::atomic::AtomicIsize::compare_and_swap`: Use `compare_exchange` or `compare_exchange_weak` instead
   --> src/map.rs:661:22
    |
661 |                     .compare_and_swap(size_ctl, rs + 2, Ordering::SeqCst)
    |                      ^^^^^^^^^^^^^^^^

warning: use of deprecated associated function `std::sync::atomic::AtomicIsize::compare_and_swap`: Use `compare_exchange` or `compare_exchange_weak` instead
   --> src/map.rs:730:22
    |
730 |                     .compare_and_swap(next_index, next_bound, Ordering::SeqCst)
    |                      ^^^^^^^^^^^^^^^^

warning: use of deprecated associated function `std::sync::atomic::AtomicIsize::compare_and_swap`: Use `compare_exchange` or `compare_exchange_weak` instead
   --> src/map.rs:780:34
    |
780 |                 if self.size_ctl.compare_and_swap(sc, sc - 1, Ordering::SeqCst) == sc {
    |                                  ^^^^^^^^^^^^^^^^

warning: use of deprecated associated function `std::sync::atomic::AtomicIsize::compare_and_swap`: Use `compare_exchange` or `compare_exchange_weak` instead
    --> src/map.rs:1148:30
     |
1148 |             if self.size_ctl.compare_and_swap(sc, sc + 1, Ordering::SeqCst) == sc {
     |                              ^^^^^^^^^^^^^^^^

warning: use of deprecated associated function `std::sync::atomic::AtomicIsize::compare_and_swap`: Use `compare_exchange` or `compare_exchange_weak` instead
    --> src/map.rs:1213:34
     |
1213 |                 if self.size_ctl.compare_and_swap(sc, sc + 1, Ordering::SeqCst) == sc {
     |                                  ^^^^^^^^^^^^^^^^

warning: use of deprecated associated function `std::sync::atomic::AtomicIsize::compare_and_swap`: Use `compare_exchange` or `compare_exchange_weak` instead
    --> src/map.rs:1216:37
     |
1216 |             } else if self.size_ctl.compare_and_swap(sc, rs + 2, Ordering::SeqCst) == sc {
     |                                     ^^^^^^^^^^^^^^^^

warning: use of deprecated associated function `std::sync::atomic::AtomicI64::compare_and_swap`: Use `compare_exchange` or `compare_exchange_weak` instead
   --> src/node.rs:334:14
    |
334 |             .compare_and_swap(0, WRITER, Ordering::SeqCst)
    |              ^^^^^^^^^^^^^^^^

warning: use of deprecated associated function `std::sync::atomic::AtomicI64::compare_and_swap`: Use `compare_exchange` or `compare_exchange_weak` instead
   --> src/node.rs:357:22
    |
357 |                     .compare_and_swap(state, WRITER, Ordering::SeqCst)
    |                      ^^^^^^^^^^^^^^^^

warning: use of deprecated associated function `std::sync::atomic::AtomicI64::compare_and_swap`: Use `compare_exchange` or `compare_exchange_weak` instead
   --> src/node.rs:391:22
    |
391 |                     .compare_and_swap(state, state | WAITER, Ordering::SeqCst)
    |                      ^^^^^^^^^^^^^^^^

warning: use of deprecated associated function `std::sync::atomic::AtomicI64::compare_and_swap`: Use `compare_exchange` or `compare_exchange_weak` instead
   --> src/node.rs:459:18
    |
459 |                 .compare_and_swap(s, s + READER, Ordering::SeqCst)
    |                  ^^^^^^^^^^^^^^^^

warning: 14 warnings emitted

Unsoundness in `HashMap::clear`

HashMap::clear does not have K\V: Send + Sync bounds, even though it calls defer_destroy on nodes and values, which might call the destructors on another thread. Unless I am missing something, this is unsound and could cause undefined behavior.

Tedious map constructors

Since HashMap's various constructors are now generic over S where S: Default, you now often have to explicitly write something like:

let map: HashMap<_, _> = HashMap::new();

This is strictly more generic (it'll work with any S), but it's also kind of annoying. One option is to basically undo 17d138f and have those methods just be implemented for the DefaultHashBuilder instead. That would make the above (and much other code) not have to repeat the type. Thoughts?

Run cargo audit in CI?

Maybe we could run cargo audit on the CI for security audits. Just run it on my machine:

$ cargo audit
    Fetching advisory database from `https://github.com/RustSec/advisory-db.git`
      Loaded 73 security advisories (from ~/.cargo/advisory-db)
    Updating crates.io index
    Scanning Cargo.lock for vulnerabilities (79 crate dependencies)
error: Vulnerable crates found!

ID:       RUSTSEC-2020-0006
Crate:    bumpalo
Version:  3.2.0
Date:     2020-03-24
URL:      https://rustsec.org/advisories/RUSTSEC-2020-0006
Title:    Flaw in `realloc` allows reading unknown memory
Solution:  upgrade to >= 3.2.1
Dependency tree:
bumpalo 3.2.0
└── wasm-bindgen-backend 0.2.59
    └── wasm-bindgen-macro-support 0.2.59
        └── wasm-bindgen-macro 0.2.59
            └── wasm-bindgen 0.2.59
                ├── web-sys 0.3.36
                │   └── plotters 0.2.12
                │       └── criterion 0.3.1
                │           └── flurry 0.2.1
                ├── plotters 0.2.12
                └── js-sys 0.3.36
                    ├── web-sys 0.3.36
                    └── plotters 0.2.12

warning: 1 warning found

Crate:    bumpalo
Version:  3.2.0
Warning:  package has been yanked!

error: 1 vulnerability found!
warning: 1 warning found!

Miri CI invocation needs updating

cargo miri test changed to be more compatible with cargo test, so the old style (used by this repo) of running cargo miri test -- -Zmiri-ignore-leaks is deprecated. The new way is MIRIFLAGS="-Zmiri-ignore-leaks" cargo miri test.

I'm not familiar with Azure CI or else I'd have prepared a PR. I hope this is not too much of an effort. :)

Port and add more tests

There are many Java tests that we do not yet have in the Rust version. You can find most of them in this file from JSR166 and in this folder from JDK. Let's port all of them and get that test coverage up! I would particularly love to get some of the concurrent tests ported over!

There are also a good chunk of tests over in hashbrown that would be good to also run against flurry.

Allow use of pre-hashed keys

As an optimisation, I want to be able to memoize a key-hash. My current solution is double-hashing with the first hash being key -> u64, then the second hash using a no-op hasher from u64 -> u64. However, this requires storing the hash twice in the hashmap.

I'm ok with such methods being unsafe if necessary, but the hashbrown equivalents are completely safe https://docs.rs/hashbrown/latest/hashbrown/hash_map/struct.RawEntryBuilder.html#method.from_key_hashed_nocheck

Profile and improve map performance

We just merged #43, which gives us benchmarks! Currently we have benches against dashmap and hashbrown.

First runs of the benchmarks seem to indicate that retrieving values from the map is quite performant, but inserting into the map is rather slow compared to both other maps. Preliminary results:

  • against hashbrown, insert and insert_erase run in the order of one ms, compared to 10-20 us in hashbrown. get holds up reasonably well though, still ~20 us to ~5-6, but given the added concurrency this seems much better than insert. Both of these use one guard across all operations of one benchmark run and relatively consistent across input distributions.
  • against dashmap, dashmap inserts in something between 1 ms with 1 thread and .8 ms for multiple threads, however what's probably more important is throughput. This for some reason is not in the report produced by criterion, but was on the order of 100 MElements/s. We compare with about 25-12 ms depending on thread count, which is so slow it takes several minutes to run any benchmark and puts throughput at something like 5, maybe 10 MElements/s. This is using 1 guard per map operation, which admittedly is not what you would do in a scenario such as the benchmark.
  • against dashmap, but with only one guard per thread (the different setup), we insert in 14-11 ms, which is better, but still considerably slower than dashmap. Note that this does not compare directly due to the different setup of the threads.
  • get against dashmap seems really good! dashmap gets 700-500 us, while we get 1.5 ms on one thread, 800us on 2 threads and go down to below 300 us on 8 threads, even with pin() for every call to get. With only one guard, this improves to 800 to a bit more than 200 us, depending on thread count.

It would be interesting to find out if these results are reproducible and, if so, what causes inserts to be so slow. @jonhoo suggests graphing the results of perf record as a profiling method in the discussion in #43, maybe this can be used as a starting point for the analysis.

Finish the safety argument for `BinEntry::Moved`

The safety argument for why dereferencing the pointer in BinEntry::Moved has not been completed. It currently stands with a FIXME in case three here

flurry/src/lib.rs

Lines 474 to 492 in d3dae04

// safety: table is a valid pointer.
//
// we are in one of three cases:
//
// 1. if table is the one we read before the loop, then we read it while holding the
// guard, so it won't be dropped until after we drop that guard b/c the drop logic
// only queues a drop for the next epoch after removing the table.
//
// 2. if table is read by init_table, then either we did a load, and the argument is
// as for point 1. or, we allocated a table, in which case the earliest it can be
// deallocated is in the next epoch. we are holding up the epoch by holding the
// guard, so this deref is safe.
//
// 3. if table is set by a Moved node (below) through help_transfer, it will _either_
// keep using `table` (which is fine by 1. and 2.), or use the `next_table` raw
// pointer from inside the Moved. how do we know that that is safe?
//
// we must demonstrate that if a Moved(t) is _read_, then t must still be valid.
// FIXME

and case two here

flurry/src/lib.rs

Lines 1060 to 1072 in 6d56c58

// safety: table is a valid pointer.
//
// we are in one of two cases:
//
// 1. if table is the one we read before the loop, then we read it while holding the
// guard, so it won't be dropped until after we drop that guard b/c the drop logic
// only queues a drop for the next epoch after removing the table.
//
// 2. if table is set by a Moved node (below) through help_transfer, it will use the
// `next_table` raw pointer from inside the Moved. how do we know that that is safe?
//
// we must demonstrate that if a Moved(t) is _read_, then t must still be valid.
// FIXME cf. put

Expose non-replacing `insert`

By calling put with no_replacement = true, we can provide a version of insert that does not update the value if the key is already present. I can imagine that being a relatively useful method to provide. I'm not sure what to call it though? std::collections::HashMap does not have an equivalent, so we're on our own here.

Match up documentation with that from std::collections::HashMap

flurry::HashMap provides many of the same methods as std::collections::HashMap. We probably want to copy over both text and examples from the documentation from std over into flurry, since the Rust devs have already gone through a lot of care to make them good. We will probably also try to incorporate some of the documentation on the type std::collections::HashMap itself onto flurry::HashMap.

As a part of this process, we should also make sure to call out ways in which the methods on flurry::HashMap differs from those on the std map. Some things to consider:

  • If the method takes a Guard, briefly explain how to get a Guard (epoch::pin; also shown in the example), and refer to the crate-level docs.
  • If the standard library method takes a &mut self, and we take a &self, call this out, and explain that the method can be called from multiple threads concurrently.
  • For pretty much every method that reads from (like get()), aggregates over (like len()), or iterates over (like .iter()) the map, add some text along these lines:

    This method, like most methods in flurry, can be executed concurrently with modifications to the underlying map. If such concurrent modification occurs, this method may return unexpected results. See the "Consistency" section in the crate-level documentation for details.

When writing this documentation, you can use intra-rustdoc links liberally.
A quick introduction to the syntax:

/// This becomes a link to the [`HashMap`] type imported in the current file.
/// This becomes a link to the [same type](HashMap) but with different text.
/// The above is handy for referring to a [`method()`](HashMap::method).
/// You can also refer to a type elsewhere, like [`std::collections::HashMap`].
/// For links to [crate-level documentation](../index.html), use a relative URL.

It is unsound to accept arbitrary user-constructed Guards

As @Amanieu points out in #44 (comment), the current API which lets users supply their own Guard references to our methods is unsound. Specifically, a user can do the following:

use crossbeam_epoch::*;
let map = HashMap::default();
map.insert(42, String::from("hello"), &epoch::pin());

let evil = crossbeam_epoch::Collector::new();
let evil = evil.register();
let oops = map.get(&42, &evil.guard());

map.remove(&42, &epoch::pin());
// at this point, the default collector is allowed to free `"hello"`
// since no-one has the global epoch pinned as far as it is aware.
// `oops` is tied to the lifetime of a Guard that is not a part of
// the same epoch group, and so can now be dangling.
// but we can still access it!
assert_eq!(oops, "hello");

Here is a sketch of a possible solution, largely inspired by @Amanieu's proposal + #45.

First, we add a Collector field to HashMap, and add a constructor that takes a Collector (this can be no_std). The default() and new() constructors use epoch::default_collector().clone() as their Collector. Then, we add a check to every method that takes a &Guard (which is essentially all of them at the moment) that checks that the given Guard's collector matches self.collector (see SkipList::check_guard). This should take care of the safety issues.

We then add a wrapper type that is tied to a single Guard and exposes all the methods (and some more convenience ones like Index) without a Guard argument. This is #45. It should also have two constructors. One that takes a user-supplied Guard (and checks it), and one that does not (and uses epoch::pin). There should be a method to extract the Guard again from the HashMapRef (since it needs to take ownership of it to also have a variant that uses epoch::pin).

Fails to build on arm

As mentioned in the docs for std::sync::atomic, AtomicI64 isn't portable because not all systems have 64-bit atomics. At least 32-bit ARM, PowerPC, and MIPS do not, and I'd like to both use Flurry and support those architectures (my supported architectures are those of Debian where Rust is available).

Is it possible that the AtomicI64 could be an AtomicIsize here to better accommodate 32-bit platforms?

Replacing map elements incorrectly decrements the map's element count

I'm close to done with the initial work on #13. While looking at replaceNode I noticed a check where the Java code does this:
https://github.com/jonhoo/flurry/blob/master/jsr166/src/ConcurrentHashMap.java#L1141-L1148
That is (ignoring validated, which we don't have or need), it modifies the number of items in the map if there was an old value which got replaced and it got replaced by null, i.e. the node got deleted.
Our current code here only checks the first of these but not the second.

It is currently possible to reach the call to add_count even with a new_value of Some. These modified tests from map.rs show that while the old value does get replaced and an entry remains present in the map for the given key, the map's length gets incorrectly updated to 0:

#[test]
fn replace_existing() {
    let map = HashMap::<usize, usize>::new();
    {
        let guard = epoch::pin();
        map.insert(42, 42, &guard);
        assert_eq!(map.len(), 1); // Ok
        let old = map.replace_node(&42, Some(10), None, &guard);
        assert_eq!(old, Some((&42, &42)));
        assert_eq!(*map.get(&42, &guard).unwrap(), 10);
        assert_eq!(map.len(), 1); // Panics with left = 0
    }
}

#[test]
fn replace_existing_observed_value_matching() {
    let map = HashMap::<usize, usize>::new();
    {
        let guard = epoch::pin();
        map.insert(42, 42, &guard);
        assert_eq!(map.len(), 1); // Ok
        let observed_value = Shared::from(map.get(&42, &guard).unwrap() as *const _);
        let old = map.replace_node(&42, Some(10), Some(observed_value), &guard);
        assert_eq!(old, Some((&42, &42)));
        assert_eq!(*map.get(&42, &guard).unwrap(), 10);
        assert_eq!(map.len(), 1); // Panics with left = 0
    }
}

One reason this was not yet noticed could be that I think we don't currently expose any functionality that replaces with non-null values. In the Java code, this only happens through exposed replace methods.

Implement `HashSet::replace`

Std's HashSet exposes the HashSet::replace method, which is useful for situations where == potentially returns true even if it's arguments are structurally different. This should be a very trivial to fix.

Also, it should be easy to implement HashSet::is_subset and HashSet::is_superset equivalents.

no_std + alloc

This crate should be possible to write using only core and alloc, and thus be usable in no_std environments.

The hashbrown implementation should be hugely helpful in figuring out how to do this. In particular, see the magical incantations in src/lib.rs and the use of alloc and core in src/raw/mod.rs. We may be able to simplify some of those since hashbrown has to pull some tricks so that it can be pulled into std. Compare for example with the no_std support in crossbeam-epoch/src/lib.rs.

There are a couple of things that we have to figure out how to operate in a no_std environment:

  • crossbeam-epoch will need default-features = false and features = ["alloc"]. This will remove epoch::pin (but not epoch::Guard I think), and we'll need to figure out whether the API is still usable (and maybe include some examples).
  • RandomState is only available with std, so we will want to add a std feature (which is on by default) and only expose it when the feature is enabled. We'll also have to figure out how that affects the default value for S.
  • Neither num_cpus nor Once (which we use for NCPU) are available on no_std. We'll need to figure out how to determine the stride for transfers without it.
  • We currently use std::thread::yield_now if we detect an initialization race. It's not entirely clear what we do in this case in a no_std environment. I think it is fine for us to use spin_loop_hint if the std isn't set for the following reason: for there to be a race, there must be another thread running concurrently with us. That thread cannot be blocked on us, since we are not in any mutually-exclusive section. So our goal is just to not waste cycles and give it some time to complete. It is not a requirement that we fully yield.

Racy test failure: segfault in map::tree_bins::concurrent_tree_bin

This is this issue which I've factored out into its own issue. Basically, the map::tree_bins::concurrent_tree_bin test occasionally segfaults for me without a backtrace. I can reproduce on current nightly on Linux by running this command for a while:

$ while cargo test --lib map::tree_bins::concurrent_tree_bin -- --test-threads=1 --nocapture; do :; done

Using gdb, I managed to capture a stack trace:

Thread 21 "flurry-27205002" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7ffff5632700 (LWP 349291)]
std::thread::Thread::unpark () at src/libstd/thread/mod.rs:1191
1191    src/libstd/thread/mod.rs: No such file or directory.
(gdb) bt
#0  std::thread::Thread::unpark () at src/libstd/thread/mod.rs:1191
#1  0x00005555555eadb2 in flurry::node::TreeBin<K,V>::find (bin=..., hash=0, key=0x7ffff56317f8, guard=0x7ffff56317b8) at src/node.rs:472
#2  0x00005555555e3594 in flurry::raw::Table<K,V>::find (self=0x5555557839d0, bin=0x555555784f70, hash=0, key=0x7ffff56317f8, guard=0x7ffff56317b8) at src/raw/mod.rs:174
#3  0x00005555555bc56c in flurry::map::HashMap<K,V,S>::get_node (self=0x555555780b40, key=0x7ffff56317f8, guard=0x7ffff56317b8) at src/map.rs:1314
#4  0x00005555555bcd1e in flurry::map::HashMap<K,V,S>::get (self=0x555555780b40, key=0x7ffff56317f8, guard=0x7ffff56317b8) at src/map.rs:1387
#5  0x000055555559540e in flurry::map::tree_bins::concurrent_tree_bin::{{closure}} () at src/map.rs:3406
#6  0x00005555555abe91 in std::sys_common::backtrace::__rust_begin_short_backtrace (f=...) at /home/jon/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/sys_common/backtrace.rs:130
#7  0x000055555558f4a1 in std::thread::Builder::spawn_unchecked::{{closure}}::{{closure}} () at /home/jon/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/thread/mod.rs:475
#8  0x00005555555d6db1 in <std::panic::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (self=..., _args=()) at /home/jon/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panic.rs:318
#9  0x00005555555a353a in std::panicking::try::do_call (data=0x7ffff5631998 "0\vxUUU\000") at /home/jon/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panicking.rs:331
#10 0x00005555555a370d in __rust_try ()
#11 0x00005555555a3393 in std::panicking::try (f=...) at /home/jon/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panicking.rs:274
#12 0x00005555555d6e31 in std::panic::catch_unwind (f=...) at /home/jon/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panic.rs:394
#13 0x000055555558ec19 in std::thread::Builder::spawn_unchecked::{{closure}} () at /home/jon/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/thread/mod.rs:474
#14 0x00005555555875ae in core::ops::function::FnOnce::call_once{{vtable-shim}} () at /home/jon/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libcore/ops/function.rs:232
#15 0x00005555556cf61f in <alloc::boxed::Box<F> as core::ops::function::FnOnce<A>>::call_once () at /rustc/94d346360da50f159e0dc777dc9bc3c5b6b51a00/src/liballoc/boxed.rs:1008
#16 0x00005555556e25b3 in <alloc::boxed::Box<F> as core::ops::function::FnOnce<A>>::call_once () at /rustc/94d346360da50f159e0dc777dc9bc3c5b6b51a00/src/liballoc/boxed.rs:1008
#17 std::sys::unix::thread::Thread::new::thread_start () at src/libstd/sys/unix/thread.rs:87
#18 0x00007ffff7f7746f in start_thread () from /usr/lib/libpthread.so.0
#19 0x00007ffff7e8d3d3 in clone () from /usr/lib/libc.so.6

HashMap enters unreachable code in try_insert

Code to reproduce:

use flurry::HashMap;
use rand::{thread_rng, Rng};
use flurry::epoch::pin;

fn main() {
    let mut rng = thread_rng();
    let map = HashMap::new();
    let g = pin();
    for _ in 0..10000 {
        let el = rng.gen_range(0, 1000);
        let _ = map.try_insert(el, el, &g);
    }
}

Output:

thread 'main' panicked at 'internal error: entered unreachable code: no_replacement cannot result in PutResult::Replaced', <::std::macros::panic macros>:5:6
stack backtrace:
   0: backtrace::backtrace::libunwind::trace
             at /cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.40/src/backtrace/libunwind.rs:88
   1: backtrace::backtrace::trace_unsynchronized
             at /cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.40/src/backtrace/mod.rs:66
   2: std::sys_common::backtrace::_print_fmt
             at src/libstd/sys_common/backtrace.rs:77
   3: <std::sys_common::backtrace::_print::DisplayBacktrace as core::fmt::Display>::fmt
             at src/libstd/sys_common/backtrace.rs:59
   4: core::fmt::write
             at src/libcore/fmt/mod.rs:1052
   5: std::io::Write::write_fmt
             at src/libstd/io/mod.rs:1426
   6: std::sys_common::backtrace::_print
             at src/libstd/sys_common/backtrace.rs:62
   7: std::sys_common::backtrace::print
             at src/libstd/sys_common/backtrace.rs:49
   8: std::panicking::default_hook::{{closure}}
             at src/libstd/panicking.rs:204
   9: std::panicking::default_hook
             at src/libstd/panicking.rs:224
  10: std::panicking::rust_panic_with_hook
             at src/libstd/panicking.rs:472
  11: rust_begin_unwind
             at src/libstd/panicking.rs:380
  12: std::panicking::begin_panic_fmt
             at src/libstd/panicking.rs:334
  13: flurry::map::HashMap<K,V,S>::try_insert
             at ./<::std::macros::panic macros>:5
  14: flurry_test::main
             at src/main.rs:11
  15: std::rt::lang_start::{{closure}}
             at /rustc/b8cedc00407a4c56a3bda1ed605c6fc166655447/src/libstd/rt.rs:67
  16: std::rt::lang_start_internal::{{closure}}
             at src/libstd/rt.rs:52
  17: std::panicking::try::do_call
             at src/libstd/panicking.rs:305
  18: __rust_maybe_catch_panic
             at src/libpanic_unwind/lib.rs:86
  19: std::panicking::try
             at src/libstd/panicking.rs:281
  20: std::panic::catch_unwind
             at src/libstd/panic.rs:394
  21: std::rt::lang_start_internal
             at src/libstd/rt.rs:51
  22: std::rt::lang_start
             at /rustc/b8cedc00407a4c56a3bda1ed605c6fc166655447/src/libstd/rt.rs:67
  23: main
  24: __libc_start_main
  25: _start
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

Process finished with exit code 101

Hide Guard where possible

We should provide methods that wrap get, insert, remove, and friends so that the user does not need to know about Guard. I'm not sure what the best way to do so is, but it's worth thinking about.

Implement "standard" map/collection methods

This issue tracks the implementation status of various "standard" methods on collections in general and maps in particular that should be added to flurry. Checked boxes have already been "claimed".

  • drain (hard) and IntoIterator (easy) for both owned and & (see #33)

If you want to pick up any of these, leave a comment here and/or open a draft PR!

["RFC"] convenient, race-free mutability

Abstract

As I see it we have no built in support for mutating data in a race-free way, this "RFC" will look into how we could implement this.

The Problem

Say a user wants to mutate some data in the list. Currently they need to use some kind of lock around their data (HashMap<K, Mutex<V>> || HashMap<K, RwLock<V>>). The downside to this it that it incurs overhead, even if no concurrent accesses are made.
Now a "naive" solution would be this:

let mut item =  map.get(...).clone();

// modify `item` ...

map.insert(..., item);

However this basically the definition of a race condition, since while we modify item another thread could overwrite the data in the map thus making our clone of it stale.

The "easy" solution:

The easiest way to implement this would be to expose a compare_and_set or compare_and_swap function. This would alow users to make sure, the data they got, mutated and that they are now planning to store is still what it was, when they first read it.

pro

  • relatively simple

con

  • still impractical to use
  • users need to write loops for every modification, so that they can repeat it, if the store "failed".
  • non 0 overhead for additional atomic cas operations.

Proposed solution

I propose we add a variant to BinEntry:
Mutating((std::sync::Condvar, Node<K, V>))
Which is basically the same as BinEntry::Node with an additional Condvar, which is notified once the write has finished.

This would allow us to have a get_mut function which returns a WriteGuard(we'll have to use guards anyways, see #16).
This function finds the BinEntry for the key (if it exists), clones it's contents and replaces it with a BinEntry::Mutating(containing the original Node). All immutable reads can (/will have to be able to) just read the node data, but get_mut can wait on the Condvar for the ongoing read to be done and only clone the node data again once it has finished.

The WriteGuard would have to:

  • store the cloned Node / V, mutably (deref to it) and allow mutating it.
  • write the modified value to the map on drop, which does the following:
  • Make the BinEntry::Mutating a BinEntry::Node again.
  • Call notify_all on the Condvar

pro

  • No additional atomics in the fast path and a single Condvar when writing concurrently to the same entry.
  • Less CPU zycles, since we can block the thread while waiting for a write instead of repeatedly modifying the copy we get, without being able to write it.
  • Fasted than an additional lock, since we use the same atomic we already need to load.
  • No additional overhead if no writes are made.

con

  • we probably need to rewrite a lot of functions that expect only BinEntry::Node and BinEntry::Moved (or only BinEntry::Node as a >1st item of the LL)

Memory usage in Flurry Hashmap

Hi I am trying to use flurry hashmap in one of my projects and want to understand memory usage and garbage collection.

I am trying a simple actix web appplication with flurry hashmap by inserting same key/value again and again. Few observation are

  1. On first add memory usage ~15GB while it is 6.7GB with std hashmap behind the lock
  2. Calling add -> clear -> add repeatedly on same key/values eventually leads to OOM error and application is killed.

Output of program

curl --location --request GET '127.0.0.1:8080/add'
count 50331648! from thread ThreadId(18) - stats Length: 50331648, Capacity: 50331648, Memory: 15.7 GiB, Virtual: 17.3 GiB

curl --location --request GET '127.0.0.1:8080/clear'
count 0! from thread ThreadId(19) - stats Length: 0, Capacity: 0, Memory: 15.7 GiB, Virtual: 17.3 GiB

curl --location --request GET '127.0.0.1:8080/add'
count 50331648! from thread ThreadId(20) - stats Length: 50331648, Capacity: 50331648, Memory: 29.1 GiB, Virtual: 30.6 GiB

curl --location --request GET '127.0.0.1:8080/clear'
count 0! from thread ThreadId(21) - stats Length: 0, Capacity: 0, Memory: 29.1 GiB, Virtual: 30.6 GiB

curl --location --request GET '127.0.0.1:8080/add'
curl: (52) Empty reply from server

main.rs

use actix_web::web::Data;
use actix_web::{get, web, App, HttpServer};
use std::sync::Mutex;

use bytesize::ByteSize;
use sysinfo::{get_current_pid, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let data = web::Data::new(AppState {
        data: flurry::HashMap::<String, String>::new(),
        sys: Mutex::new(System::new_with_specifics(
            RefreshKind::new().with_processes(ProcessRefreshKind::new()),
        )),
    });

    HttpServer::new(move || {
        App::new()
            .app_data(data.clone())
            .service(add)
            .service(clear)
            .service(stats)
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

struct AppState {
    data: flurry::HashMap<String, String>,
    sys: Mutex<System>,
}

#[get("/stats")]
async fn stats(data: web::Data<AppState>) -> String {
    stats_2(data)
}

fn stats_2(data: Data<AppState>) -> String {
    let pid = get_current_pid().unwrap();
    let mut sys = data.sys.lock().unwrap();
    sys.refresh_process(pid);

    let proc = sys.process(pid).unwrap();
    let map = &data.data;
    let string = format!(
        "Length: {}, Capacity: {}, Memory: {}, Virtual: {}\n",
        map.len(),
        map.len(),
        ByteSize::b(proc.memory()).to_string_as(true),
        ByteSize::b(proc.virtual_memory()).to_string_as(true)
    );

    string
}

#[get("/add")]
async fn add(data: web::Data<AppState>) -> String {
    let size;
    {
        let max_entries = 100663296 as u64;
        let m = &data.data;
        for i in 0..max_entries / 2 {
            m.pin().insert(format!("str-{i}"), format!("str-{i}-{i}"));
        }

        size = m.len();
    }
    let stats1 = stats_2(data);
    format!(
        "count {size}! from thread {:?} - stats {stats1}\n",
        std::thread::current().id()
    )
}

#[get("/clear")]
async fn clear(data: web::Data<AppState>) -> String {
    let size;
    {
        let m = &data.data;
        m.pin().clear();
        // unsafe { malloc_trim(0) };
        size = m.len();
    }

    let stats1 = stats_2(data);
    format!(
        "count {size}! from thread {:?} - stats {stats1}\n",
        std::thread::current().id()
    )
}

Cargo.toml

[package]
name = "skiptest"
version = "0.1.0"
edition = "2021"


[dependencies]
actix-web = "4"

flurry = "0.4.0"

sysinfo = "0.28.4"
bytesize = "1.2.0"

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.