Skip to content

Commit 33e1978

Browse files
committed
graph: Improve weight estimation of BTreeMap
This commit also makes the stress example easier to handle, and copies the new weight code from `cache_weight.rs` to make comparisons between the two easier. Right now, both implementations are the same. With the new estimation, BTreeMap's CacheWeight is precise for small maps and closer to the true allocation for larger maps with more than ~ 22 entries.
1 parent 40e7c7b commit 33e1978

File tree

3 files changed

+274
-53
lines changed

3 files changed

+274
-53
lines changed

graph/examples/stress.rs

Lines changed: 168 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ use std::collections::{BTreeMap, HashMap};
33
use std::iter::FromIterator;
44
use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
55

6-
use graph::components::store::EntityType;
7-
use graph::prelude::{q, DeploymentHash, EntityKey};
6+
use graph::prelude::{lazy_static, q};
87
use rand::{thread_rng, Rng};
98
use structopt::StructOpt;
109

@@ -18,6 +17,149 @@ struct Counter;
1817

1918
static ALLOCATED: AtomicUsize = AtomicUsize::new(0);
2019

20+
lazy_static! {
21+
// Set 'MAP_MEASURE' to something to use the `CacheWeight` defined here
22+
// in the `btree` module for `BTreeMap`. If this is not set, use the
23+
// estimate from `graph::util::cache_weight`
24+
static ref MAP_MEASURE: bool = std::env::var("MAP_MEASURE").ok().is_some();
25+
26+
// When running the `valuemap` test for BTreeMap, put maps into the
27+
// values of the generated maps
28+
static ref NESTED_MAP: bool = std::env::var("NESTED_MAP").ok().is_some();
29+
}
30+
// Yes, a global variable. It gets set at the beginning of `main`
31+
static mut PRINT_SAMPLES: bool = false;
32+
33+
/// Helpers to estimate the size of a `BTreeMap`. Everything in this module,
34+
/// except for `node_size()` is copied from `std::collections::btree`.
35+
///
36+
/// It is not possible to know how many nodes a BTree has, as
37+
/// `BTreeMap` does not expose its depth or any other detail about
38+
/// the true size of the BTree. We estimate that size, assuming the
39+
/// average case, i.e., a BTree where every node has the average
40+
/// between the minimum and maximum number of entries per node, i.e.,
41+
/// the average of (B-1) and (2*B-1) entries, which we call
42+
/// `NODE_FILL`. The number of leaf nodes in the tree is then the
43+
/// number of entries divided by `NODE_FILL`, and the number of
44+
/// interior nodes can be determined by dividing the number of nodes
45+
/// at the child level by `NODE_FILL`
46+
47+
/// The other difficulty is that the structs with which `BTreeMap`
48+
/// represents internal and leaf nodes are not public, so we can't
49+
/// get their size with `std::mem::size_of`; instead, we base our
50+
/// estimates of their size on the current `std` code, assuming that
51+
/// these structs will not change
52+
53+
mod btree {
54+
use std::mem;
55+
use std::{mem::MaybeUninit, ptr::NonNull};
56+
57+
const B: usize = 6;
58+
const CAPACITY: usize = 2 * B - 1;
59+
60+
/// Assume BTree nodes are this full (average of minimum and maximum fill)
61+
const NODE_FILL: usize = ((B - 1) + (2 * B - 1)) / 2;
62+
63+
type BoxedNode<K, V> = NonNull<LeafNode<K, V>>;
64+
65+
struct InternalNode<K, V> {
66+
_data: LeafNode<K, V>,
67+
68+
/// The pointers to the children of this node. `len + 1` of these are considered
69+
/// initialized and valid, except that near the end, while the tree is held
70+
/// through borrow type `Dying`, some of these pointers are dangling.
71+
_edges: [MaybeUninit<BoxedNode<K, V>>; 2 * B],
72+
}
73+
74+
struct LeafNode<K, V> {
75+
/// We want to be covariant in `K` and `V`.
76+
_parent: Option<NonNull<InternalNode<K, V>>>,
77+
78+
/// This node's index into the parent node's `edges` array.
79+
/// `*node.parent.edges[node.parent_idx]` should be the same thing as `node`.
80+
/// This is only guaranteed to be initialized when `parent` is non-null.
81+
_parent_idx: MaybeUninit<u16>,
82+
83+
/// The number of keys and values this node stores.
84+
_len: u16,
85+
86+
/// The arrays storing the actual data of the node. Only the first `len` elements of each
87+
/// array are initialized and valid.
88+
_keys: [MaybeUninit<K>; CAPACITY],
89+
_vals: [MaybeUninit<V>; CAPACITY],
90+
}
91+
92+
pub fn node_size<K, V>(map: &std::collections::BTreeMap<K, V>) -> usize {
93+
// Measure the size of internal and leaf nodes directly - that's why
94+
// we copied all this code from `std`
95+
let ln_sz = mem::size_of::<LeafNode<K, V>>();
96+
let in_sz = mem::size_of::<InternalNode<K, V>>();
97+
98+
// Estimate the number of internal and leaf nodes based on the only
99+
// thing we can measure about a BTreeMap, the number of entries in
100+
// it, and use our `NODE_FILL` assumption to estimate how the tree
101+
// is structured. We try to be very good for small maps, since
102+
// that's what we use most often in our code. This estimate is only
103+
// for the indirect weight of the `BTreeMap`
104+
let (leaves, int_nodes) = if map.is_empty() {
105+
// An empty tree has no indirect weight
106+
(0, 0)
107+
} else if map.len() <= CAPACITY {
108+
// We only have the root node
109+
(1, 0)
110+
} else {
111+
// Estimate based on our `NODE_FILL` assumption
112+
let leaves = map.len() / NODE_FILL + 1;
113+
let mut prev_level = leaves / NODE_FILL + 1;
114+
let mut int_nodes = prev_level;
115+
while prev_level > 1 {
116+
int_nodes += prev_level;
117+
prev_level = prev_level / NODE_FILL + 1;
118+
}
119+
(leaves, int_nodes)
120+
};
121+
122+
let sz = leaves * ln_sz + int_nodes * in_sz;
123+
124+
if unsafe { super::PRINT_SAMPLES } {
125+
println!(
126+
" btree: leaves={} internal={} sz={} ln_sz={} in_sz={} len={}",
127+
leaves,
128+
int_nodes,
129+
sz,
130+
ln_sz,
131+
in_sz,
132+
map.len()
133+
);
134+
}
135+
sz
136+
}
137+
}
138+
139+
struct MapMeasure<K, V>(BTreeMap<K, V>);
140+
141+
impl<K, V> Default for MapMeasure<K, V> {
142+
fn default() -> MapMeasure<K, V> {
143+
MapMeasure(BTreeMap::new())
144+
}
145+
}
146+
147+
impl<K: CacheWeight, V: CacheWeight> CacheWeight for MapMeasure<K, V> {
148+
fn indirect_weight(&self) -> usize {
149+
if *MAP_MEASURE {
150+
let kv_sz = self
151+
.0
152+
.iter()
153+
.map(|(key, value)| key.indirect_weight() + value.indirect_weight())
154+
.sum::<usize>();
155+
let node_sz = btree::node_size(&self.0);
156+
kv_sz + node_sz
157+
} else {
158+
self.0.indirect_weight()
159+
}
160+
}
161+
}
162+
21163
unsafe impl GlobalAlloc for Counter {
22164
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
23165
let ret = System.alloc(layout);
@@ -101,16 +243,18 @@ impl Template<HashMap<String, String>> for HashMap<String, String> {
101243
}
102244
}
103245

104-
type ValueMap = BTreeMap<String, q::Value>;
246+
type ValueMap = MapMeasure<String, q::Value>;
105247

106248
/// Template for testing roughly a GraphQL response, i.e., a `BTreeMap<String, Value>`
107249
impl Template<ValueMap> for ValueMap {
108250
type Item = ValueMap;
109251

110252
fn create(size: usize) -> Self {
111253
let mut map = BTreeMap::new();
254+
let modulus = if *NESTED_MAP { 9 } else { 8 };
255+
112256
for i in 0..size {
113-
let value = match i % 9 {
257+
let value = match i % modulus {
114258
0 => q::Value::Boolean(i % 11 > 5),
115259
1 => q::Value::Int((i as i32).into()),
116260
2 => q::Value::Null,
@@ -129,23 +273,24 @@ impl Template<ValueMap> for ValueMap {
129273
}
130274
q::Value::Object(map)
131275
}
132-
_ => q::Value::String(format!("other{}", i)),
276+
_ => unreachable!(),
133277
};
134278
map.insert(format!("val{}", i), value);
135279
}
136-
map
280+
MapMeasure(map)
137281
}
138282

139283
fn sample(&self, size: usize) -> Box<Self::Item> {
140-
Box::new(BTreeMap::from_iter(
141-
self.iter()
284+
Box::new(MapMeasure(BTreeMap::from_iter(
285+
self.0
286+
.iter()
142287
.take(size)
143288
.map(|(k, v)| (k.to_owned(), v.to_owned())),
144-
))
289+
)))
145290
}
146291
}
147292

148-
type UsizeMap = BTreeMap<usize, usize>;
293+
type UsizeMap = MapMeasure<usize, usize>;
149294

150295
/// Template for testing roughly a GraphQL response, i.e., a `BTreeMap<String, Value>`
151296
impl Template<UsizeMap> for UsizeMap {
@@ -156,15 +301,16 @@ impl Template<UsizeMap> for UsizeMap {
156301
for i in 0..size {
157302
map.insert(i * 2, i * 3);
158303
}
159-
map
304+
MapMeasure(map)
160305
}
161306

162307
fn sample(&self, size: usize) -> Box<Self::Item> {
163-
Box::new(BTreeMap::from_iter(
164-
self.iter()
308+
Box::new(MapMeasure(BTreeMap::from_iter(
309+
self.0
310+
.iter()
165311
.take(size)
166312
.map(|(k, v)| (k.to_owned(), v.to_owned())),
167-
))
313+
)))
168314
}
169315
}
170316

@@ -221,23 +367,28 @@ fn stress<T: Template<T, Item = T>>(opt: &Opt) {
221367

222368
println!("type: {}", cacheable.name());
223369
println!(
224-
"obj: {} iterations: {} cache_size: {}",
370+
"obj: {} iterations: {} cache_size: {}\n",
225371
cacheable.template.weight(),
226372
opt.niter,
227373
opt.cache_size
228374
);
229-
println!("heap_factor is heap_size / cache_size");
230375

231376
let mut rng = thread_rng();
232377
let base_mem = ALLOCATED.load(SeqCst);
233378
let print_mod = opt.niter / opt.print_count + 1;
234379
let mut should_print = true;
380+
let mut print_header = true;
235381
for key in 0..opt.niter {
236382
should_print = should_print || key % print_mod == 0;
237383
let before_mem = ALLOCATED.load(SeqCst);
238384
if let Some((evicted, _, new_weight)) = cacheable.cache.evict(opt.cache_size) {
239385
let after_mem = ALLOCATED.load(SeqCst);
240386
if should_print {
387+
if print_header {
388+
println!("heap_factor is heap_size / cache_size");
389+
print_header = false;
390+
}
391+
241392
let heap_factor = (after_mem - base_mem) as f64 / opt.cache_size as f64;
242393
println!(
243394
"evicted: {:6} dropped: {:6} new_weight: {:8} heap_factor: {:0.2} ",
@@ -279,6 +430,7 @@ fn stress<T: Template<T, Item = T>>(opt: &Opt) {
279430
/// the target `cache_size`
280431
pub fn main() {
281432
let opt = Opt::from_args();
433+
unsafe { PRINT_SAMPLES = opt.samples }
282434

283435
// Use different Cacheables to see how the cache manages memory with
284436
// different types of cache entries. Uncomment one of the 'let mut cacheable'

0 commit comments

Comments
 (0)