diff --git a/src/libcollections/btree/map.rs b/src/libcollections/btree/map.rs index ab1605954337e611cbbee13a571b936edcd61c20..dbbff61b8dd50c139154eb800a05892f7a6843d3 100644 --- a/src/libcollections/btree/map.rs +++ b/src/libcollections/btree/map.rs @@ -29,6 +29,47 @@ /// A map based on a B-Tree. +/// +/// B-Trees represent a fundamental compromise between cache-efficiency and actually minimizing +/// the amount of work performed in a search. In theory, a binary search tree (BST) is the optimal +/// choice for a sorted map, as a perfectly balanced BST performs the theoretical minimum amount of +/// comparisons necessary to find an element (log2n). However, in practice the way this +/// is done is *very* inefficient for modern computer architectures. In particular, every element +/// is stored in its own individually heap-allocated node. This means that every single insertion +/// triggers a heap-allocation, and every single comparison should be a cache-miss. Since these +/// are both notably expensive things to do in practice, we are forced to at very least reconsider +/// the BST strategy. +/// +/// A B-Tree instead makes each node contain B-1 to 2B-1 elements in a contiguous array. By doing +/// this, we reduce the number of allocations by a factor of B, and improve cache effeciency in +/// searches. However, this does mean that searches will have to do *more* comparisons on average. +/// The precise number of comparisons depends on the node search strategy used. For optimal cache +/// effeciency, one could search the nodes linearly. For optimal comparisons, one could search +/// the node using binary search. As a compromise, one could also perform a linear search +/// that initially only checks every ith element for some choice of i. +/// +/// Currently, our implementation simply performs naive linear search. This provides excellent +/// performance on *small* nodes of elements which are cheap to compare. However in the future we +/// would like to further explore choosing the optimal search strategy based on the choice of B, +/// and possibly other factors. Using linear search, searching for a random element is expected +/// to take O(B logBn) comparisons, which is generally worse than a BST. In practice, +/// however, performance is excellent. `BTreeMap` is able to readily outperform `TreeMap` under +/// many workloads, and is competetive where it doesn't. BTreeMap also generally *scales* better +/// than TreeMap, making it more appropriate for large datasets. +/// +/// However, `TreeMap` may still be more appropriate to use in many contexts. If elements are very +/// large or expensive to compare, `TreeMap` may be more appropriate. It won't allocate any +/// more space than is needed, and will perform the minimal number of comparisons necessary. +/// `TreeMap` also provides much better performance stability guarantees. Generally, very few +/// changes need to be made to update a BST, and two updates are expected to take about the same +/// amount of time on roughly equal sized BSTs. However a B-Tree's performance is much more +/// amortized. If a node is overfull, it must be split into two nodes. If a node is underfull, it +/// may be merged with another. Both of these operations are relatively expensive to perform, and +/// it's possible to force one to occur at every single level of the tree in a single insertion or +/// deletion. In fact, a malicious or otherwise unlucky sequence of insertions and deletions can +/// force this degenerate behaviour to occur on every operation. While the total amount of work +/// done on each operation isn't *catastrophic*, and *is* still bounded by O(B logBn), +/// it is certainly much slower when it does. #[deriving(Clone)] pub struct BTreeMap { root: Node, @@ -93,6 +134,8 @@ pub fn new() -> BTreeMap { } /// Makes a new empty BTreeMap with the given B. + /// + /// B cannot be less than 2. pub fn with_b(b: uint) -> BTreeMap { assert!(b > 1, "B must be greater than 1"); BTreeMap { @@ -1145,9 +1188,12 @@ fn test_entry(){ #[cfg(test)] mod bench { - use test::Bencher; + use std::prelude::*; + use std::rand::{weak_rng, Rng}; + use test::{Bencher, black_box}; use super::BTreeMap; + use MutableMap; use deque::bench::{insert_rand_n, insert_seq_n, find_rand_n, find_seq_n}; #[bench] @@ -1200,4 +1246,34 @@ pub fn find_seq_10_000(b: &mut Bencher) { let mut m : BTreeMap = BTreeMap::new(); find_seq_n(10_000, &mut m, b); } + + fn bench_iter(b: &mut Bencher, size: uint) { + let mut map = BTreeMap::::new(); + let mut rng = weak_rng(); + + for _ in range(0, size) { + map.swap(rng.gen(), rng.gen()); + } + + b.iter(|| { + for entry in map.iter() { + black_box(entry); + } + }); + } + + #[bench] + pub fn iter_20(b: &mut Bencher) { + bench_iter(b, 20); + } + + #[bench] + pub fn iter_1000(b: &mut Bencher) { + bench_iter(b, 1000); + } + + #[bench] + pub fn iter_100000(b: &mut Bencher) { + bench_iter(b, 100000); + } } diff --git a/src/libcollections/btree/set.rs b/src/libcollections/btree/set.rs index b21af89742c947080bf13bb013fdec33a1593470..8958f0ef5bee749eee1b7f391fffda60918c5e7c 100644 --- a/src/libcollections/btree/set.rs +++ b/src/libcollections/btree/set.rs @@ -23,6 +23,9 @@ use {Mutable, Set, MutableSet, MutableMap, Map}; /// A set based on a B-Tree. +/// +/// See BTreeMap's documentation for a detailed discussion of this collection's performance +/// benefits and drawbacks. #[deriving(Clone, Hash, PartialEq, Eq, Ord, PartialOrd)] pub struct BTreeSet{ map: BTreeMap, @@ -65,6 +68,8 @@ pub fn new() -> BTreeSet { } /// Makes a new BTreeSet with the given B. + /// + /// B cannot be less than 2. pub fn with_b(b: uint) -> BTreeSet { BTreeSet { map: BTreeMap::with_b(b) } } diff --git a/src/libcollections/treemap.rs b/src/libcollections/treemap.rs index 73e2d8466546ffcaedda6b8b3039e323bdf905e7..a86e7386eddd8c07b55369721e030d07c5055f4a 100644 --- a/src/libcollections/treemap.rs +++ b/src/libcollections/treemap.rs @@ -2232,9 +2232,12 @@ fn test_index_nonexistent() { #[cfg(test)] mod bench { - use test::Bencher; + use std::prelude::*; + use std::rand::{weak_rng, Rng}; + use test::{Bencher, black_box}; use super::TreeMap; + use MutableMap; use deque::bench::{insert_rand_n, insert_seq_n, find_rand_n, find_seq_n}; // Find seq @@ -2288,6 +2291,36 @@ pub fn find_seq_10_000(b: &mut Bencher) { let mut m : TreeMap = TreeMap::new(); find_seq_n(10_000, &mut m, b); } + + fn bench_iter(b: &mut Bencher, size: uint) { + let mut map = TreeMap::::new(); + let mut rng = weak_rng(); + + for _ in range(0, size) { + map.swap(rng.gen(), rng.gen()); + } + + b.iter(|| { + for entry in map.iter() { + black_box(entry); + } + }); + } + + #[bench] + pub fn iter_20(b: &mut Bencher) { + bench_iter(b, 20); + } + + #[bench] + pub fn iter_1000(b: &mut Bencher) { + bench_iter(b, 1000); + } + + #[bench] + pub fn iter_100000(b: &mut Bencher) { + bench_iter(b, 100000); + } } #[cfg(test)] diff --git a/src/libcollections/trie.rs b/src/libcollections/trie.rs index a022a1bc89016981cbb140aa2e5eabd4eda4903f..2e388a92a59f43cbea786ac94119019698e26e67 100644 --- a/src/libcollections/trie.rs +++ b/src/libcollections/trie.rs @@ -949,8 +949,8 @@ unsafe fn new() -> $name<'a, T> { // rules, and are just manipulating raw pointers like there's no // such thing as invalid pointers and memory unsafety. The // reason is performance, without doing this we can get the - // bench_iter_large microbenchmark down to about 30000 ns/iter - // (using .unsafe_get to index self.stack directly, 38000 + // (now replaced) bench_iter_large microbenchmark down to about + // 30000 ns/iter (using .unsafe_get to index self.stack directly, 38000 // ns/iter with [] checked indexing), but this smashes that down // to 13500 ns/iter. // @@ -1459,31 +1459,39 @@ fn test_index_nonexistent() { mod bench_map { use std::prelude::*; use std::rand::{weak_rng, Rng}; - use test::Bencher; + use test::{Bencher, black_box}; use MutableMap; use super::TrieMap; - #[bench] - fn bench_iter_small(b: &mut Bencher) { - let mut m = TrieMap::::new(); + fn bench_iter(b: &mut Bencher, size: uint) { + let mut map = TrieMap::::new(); let mut rng = weak_rng(); - for _ in range(0u, 20) { - m.insert(rng.gen(), rng.gen()); + + for _ in range(0, size) { + map.swap(rng.gen(), rng.gen()); } - b.iter(|| for _ in m.iter() {}) + b.iter(|| { + for entry in map.iter() { + black_box(entry); + } + }); } #[bench] - fn bench_iter_large(b: &mut Bencher) { - let mut m = TrieMap::::new(); - let mut rng = weak_rng(); - for _ in range(0u, 1000) { - m.insert(rng.gen(), rng.gen()); - } + pub fn iter_20(b: &mut Bencher) { + bench_iter(b, 20); + } - b.iter(|| for _ in m.iter() {}) + #[bench] + pub fn iter_1000(b: &mut Bencher) { + bench_iter(b, 1000); + } + + #[bench] + pub fn iter_100000(b: &mut Bencher) { + bench_iter(b, 100000); } #[bench]