Skip to content

Commit 3e02361

Browse files
refactor(storage-proofs): restructure expander parents cache
- uses a single allocation of a Vec instead of HashMaps - uses separate locks for forward and reverse - uses u32 instead of usize for nodes to reduce memory usage - avoids allocations when reading from cache
1 parent 4a2f529 commit 3e02361

File tree

1 file changed

+144
-107
lines changed

1 file changed

+144
-107
lines changed

storage-proofs/src/zigzag_graph.rs

Lines changed: 144 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use std::collections::HashMap;
21
use std::marker::PhantomData;
32
use std::sync::{Arc, RwLock};
43

@@ -13,17 +12,80 @@ use crate::settings;
1312
pub const EXP_DEGREE: usize = 8;
1413

1514
// Cache of node's parents.
16-
pub type ParentCache = HashMap<usize, Vec<usize>>;
15+
pub type ParentCache = Vec<Option<Vec<u32>>>;
1716

1817
// ZigZagGraph will hold two different (but related) `ParentCache`,
1918
// the first one for the `forward` direction and the second one
2019
// for the `reversed`.
21-
pub type TwoWayParentCache = [ParentCache; 2];
20+
#[derive(Debug, Clone)]
21+
pub struct ShareableParentCache {
22+
forward: Arc<RwLock<ParentCache>>,
23+
reverse: Arc<RwLock<ParentCache>>,
24+
// Keep the size of the cache outside the lock to be easily accessible.
25+
cache_entries: u32,
26+
}
27+
28+
impl ShareableParentCache {
29+
pub fn new(cache_entries: u32) -> Self {
30+
ShareableParentCache {
31+
forward: Arc::new(RwLock::new(vec![None; cache_entries as usize])),
32+
reverse: Arc::new(RwLock::new(vec![None; cache_entries as usize])),
33+
cache_entries,
34+
}
35+
}
36+
37+
pub fn contains_forward(&self, node: u32) -> bool {
38+
assert!(node < self.cache_entries);
39+
self.forward.read().unwrap()[node as usize].is_some()
40+
}
41+
42+
pub fn contains_reverse(&self, node: u32) -> bool {
43+
assert!(node < self.cache_entries);
44+
self.reverse.read().unwrap()[node as usize].is_some()
45+
}
46+
47+
pub fn read_forward<F, T>(&self, node: u32, mut cb: F) -> T
48+
where
49+
F: FnMut(Option<&Vec<u32>>) -> T,
50+
{
51+
assert!(node < self.cache_entries);
52+
let lock = self.forward.read().unwrap();
53+
cb(lock[node as usize].as_ref())
54+
}
55+
56+
pub fn read_reverse<F, T>(&self, node: u32, mut cb: F) -> T
57+
where
58+
F: FnMut(Option<&Vec<u32>>) -> T,
59+
{
60+
assert!(node < self.cache_entries);
61+
let lock = self.reverse.read().unwrap();
62+
cb(lock[node as usize].as_ref())
63+
}
64+
65+
pub fn write_forward(&self, node: u32, parents: Vec<u32>) {
66+
assert!(node < self.cache_entries);
67+
68+
let mut write_lock = self.forward.write().unwrap();
69+
70+
let old_value = std::mem::replace(&mut write_lock[node as usize], Some(parents));
71+
72+
debug_assert_eq!(old_value, None);
73+
// We shouldn't be rewriting entries (with most likely the same values),
74+
// this would be a clear indication of a bug.
75+
}
76+
77+
pub fn write_reverse(&self, node: u32, parents: Vec<u32>) {
78+
assert!(node < self.cache_entries);
79+
80+
let mut write_lock = self.reverse.write().unwrap();
2281

23-
// The cache is hold in an `Arc` to make it available across different
24-
// threads. It is accessed through a `RwLock` to distinguish between
25-
// read an write operations.
26-
pub type ShareableParentCache = Arc<RwLock<TwoWayParentCache>>;
82+
let old_value = std::mem::replace(&mut write_lock[node as usize], Some(parents));
83+
84+
debug_assert_eq!(old_value, None);
85+
// We shouldn't be rewriting entries (with most likely the same values),
86+
// this would be a clear indication of a bug.
87+
}
88+
}
2789

2890
#[derive(Debug, Clone)]
2991
pub struct ZigZagGraph<H, G>
@@ -44,9 +106,7 @@ where
44106
// all entries sequentially when encoding or any random entry once when proving
45107
// or verifying, but there's no locality to take advantage of so keep the logic
46108
// as simple as possible).
47-
parents_cache: ShareableParentCache,
48-
// Keep the size of the cache outside the lock to be easily accessible.
49-
cache_entries: usize,
109+
parents_cache: Option<ShareableParentCache>,
50110
_h: PhantomData<H>,
51111
}
52112

@@ -76,11 +136,12 @@ where
76136
assert_eq!(expansion_degree, EXP_DEGREE);
77137
}
78138

79-
let cache_entries = if settings::SETTINGS.lock().unwrap().maximize_caching {
139+
let parents_cache = if settings::SETTINGS.lock().unwrap().maximize_caching {
80140
info!("using parents cache of unlimited size",);
81-
nodes
141+
assert!(nodes <= std::u32::MAX as usize);
142+
Some(ShareableParentCache::new(nodes as u32))
82143
} else {
83-
0
144+
None
84145
};
85146

86147
ZigZagGraph {
@@ -91,11 +152,7 @@ where
91152
expansion_degree,
92153
reversed: false,
93154
feistel_precomputed: feistel::precompute((expansion_degree * nodes) as feistel::Index),
94-
parents_cache: Arc::new(RwLock::new([
95-
HashMap::with_capacity(cache_entries),
96-
HashMap::with_capacity(cache_entries),
97-
])),
98-
cache_entries,
155+
parents_cache,
99156
_h: PhantomData,
100157
}
101158
}
@@ -131,7 +188,9 @@ pub trait ZigZag: ::std::fmt::Debug + Clone + PartialEq + Eq {
131188
fn base_graph(&self) -> Self::BaseGraph;
132189
fn expansion_degree(&self) -> usize;
133190
fn reversed(&self) -> bool;
134-
fn expanded_parents(&self, node: usize) -> Vec<usize>;
191+
fn expanded_parents<F, T>(&self, node: usize, cb: F) -> T
192+
where
193+
F: FnMut(&Vec<u32>) -> T;
135194
fn real_index(&self, i: usize) -> usize;
136195
fn new_zigzag(
137196
nodes: usize,
@@ -163,21 +222,21 @@ impl<Z: ZigZag> Graph<Z::BaseHasher> for Z {
163222
}
164223

165224
// expanded_parents takes raw_node
166-
let expanded_parents = self.expanded_parents(raw_node);
167-
168-
for (ii, value) in expanded_parents.iter().enumerate() {
169-
parents[ii + self.base_graph().degree()] = *value
170-
}
225+
self.expanded_parents(raw_node, |expanded_parents| {
226+
for (ii, value) in expanded_parents.iter().enumerate() {
227+
parents[ii + self.base_graph().degree()] = *value as usize
228+
}
171229

172-
// Pad so all nodes have correct degree.
173-
let current_length = self.base_graph().degree() + expanded_parents.len();
174-
for ii in 0..(self.degree() - current_length) {
175-
if self.reversed() {
176-
parents[ii + current_length] = self.size() - 1
177-
} else {
178-
parents[ii + current_length] = 0
230+
// Pad so all nodes have correct degree.
231+
let current_length = self.base_graph().degree() + expanded_parents.len();
232+
for ii in 0..(self.degree() - current_length) {
233+
if self.reversed() {
234+
parents[ii + current_length] = self.size() - 1
235+
} else {
236+
parents[ii + current_length] = 0
237+
}
179238
}
180-
}
239+
});
181240
assert!(parents.len() == self.degree());
182241
parents.sort();
183242

@@ -266,53 +325,20 @@ where
266325
// back this function in the `reversed` direction).
267326
}
268327

269-
// The first cache in `parents_cache` corresponds to the forward direction,
270-
// the second one to the reversed.
271-
fn get_cache_index(&self) -> usize {
272-
if self.forward() {
273-
0
274-
} else {
275-
1
276-
}
277-
}
278-
279328
// Read the `node` entry in the parents cache (which may not exist) for
280329
// the current direction set in the graph and return a copy of it (or
281330
// `None` to signal a cache miss).
282-
fn read_parents_cache(&self, node: usize) -> Option<Vec<usize>> {
283-
// If the index exceeds the cache size don't bother checking.
284-
if node >= self.cache_entries {
285-
return None;
286-
}
287-
288-
let read_lock = self.parents_cache.read().unwrap();
289-
290-
let parents_cache = &(*read_lock)[self.get_cache_index()];
291-
292-
if let Some(parents) = parents_cache.get(&node) {
293-
Some(parents.clone())
331+
fn contains_parents_cache(&self, node: usize) -> bool {
332+
if let Some(ref cache) = self.parents_cache {
333+
if self.forward() {
334+
cache.contains_forward(node as u32)
335+
} else {
336+
cache.contains_reverse(node as u32)
337+
}
294338
} else {
295-
None
339+
false
296340
}
297341
}
298-
299-
// Save the `parents` of the `node` in its entry of the cache.
300-
fn write_parents_cache(&self, node: usize, parents: Vec<usize>) {
301-
// Don't allow writing more entries than the already allocated space.
302-
if node >= self.cache_entries {
303-
return;
304-
}
305-
306-
let mut write_lock = self.parents_cache.write().unwrap();
307-
308-
let parents_cache = &mut (*write_lock)[self.get_cache_index()];
309-
310-
let old_value = parents_cache.insert(node, parents);
311-
312-
debug_assert_eq!(old_value, None);
313-
// We shouldn't be rewriting entries (with most likely the same values),
314-
// this would be a clear indication of a bug.
315-
}
316342
}
317343

318344
impl<'a, H, G> ZigZag for ZigZagGraph<H, G>
@@ -365,31 +391,48 @@ where
365391
// we would only need to compute the parents in one direction and with
366392
// that fill both caches.
367393
#[inline]
368-
fn expanded_parents(&self, node: usize) -> Vec<usize> {
369-
if let Some(parents) = self.read_parents_cache(node) {
370-
return parents;
371-
}
372-
373-
let parents: Vec<usize> = (0..self.expansion_degree)
374-
.filter_map(|i| {
375-
let other = self.correspondent(node, i);
376-
if self.reversed {
377-
if other > node {
378-
Some(other)
394+
fn expanded_parents<F, T>(&self, node: usize, mut cb: F) -> T
395+
where
396+
F: FnMut(&Vec<u32>) -> T,
397+
{
398+
if !self.contains_parents_cache(node) {
399+
let parents: Vec<u32> = (0..self.expansion_degree)
400+
.filter_map(|i| {
401+
let other = self.correspondent(node, i);
402+
if self.reversed {
403+
if other > node {
404+
Some(other as u32)
405+
} else {
406+
None
407+
}
408+
} else if other < node {
409+
Some(other as u32)
379410
} else {
380411
None
381412
}
382-
} else if other < node {
383-
Some(other)
413+
})
414+
.collect();
415+
416+
if let Some(ref cache) = self.parents_cache {
417+
if self.forward() {
418+
cache.write_forward(node as u32, parents);
384419
} else {
385-
None
420+
cache.write_reverse(node as u32, parents);
386421
}
387-
})
388-
.collect();
389-
390-
self.write_parents_cache(node, parents.clone());
422+
} else {
423+
return cb(&parents);
424+
}
425+
}
391426

392-
parents
427+
if let Some(ref cache) = self.parents_cache {
428+
if self.forward() {
429+
cache.read_forward(node as u32, |parents| cb(parents.unwrap()))
430+
} else {
431+
cache.read_reverse(node as u32, |parents| cb(parents.unwrap()))
432+
}
433+
} else {
434+
unreachable!()
435+
}
393436
}
394437

395438
#[inline]
@@ -515,40 +558,34 @@ mod tests {
515558
// Check to make sure all (expanded) node-parent relationships also exist in reverse,
516559
// in the original graph's Hashmap.
517560
for p in parents {
518-
assert!(gcache[&p].contains(&i));
561+
assert!(gcache[&(*p as usize)].contains(&(i as u32)));
519562
}
520563
}
521564

522565
// And then do the same check to make sure all (expanded) node-parent relationships from the original
523566
// are present in the zigzag, just reversed.
524567
for i in 0..g.size() {
525-
let parents = g.expanded_parents(i);
526-
for p in parents {
527-
assert!(gzcache[&p].contains(&i));
528-
}
568+
g.expanded_parents(i, |parents| {
569+
for p in parents.iter() {
570+
assert!(gzcache[&(*p as usize)].contains(&(i as u32)));
571+
}
572+
});
529573
}
530574
// Having checked both ways, we know the graph and its zigzag counterpart have 'expanded' components
531575
// which are each other's inverses. It's important that this be true.
532576
}
533577

534578
fn get_all_expanded_parents<H: 'static + Hasher>(
535579
zigzag_graph: &ZigZagBucketGraph<H>,
536-
) -> HashMap<usize, Vec<usize>> {
537-
let mut parents_map: HashMap<usize, Vec<usize>> = HashMap::new();
580+
) -> HashMap<usize, Vec<u32>> {
581+
let mut parents_map: HashMap<usize, Vec<u32>> = HashMap::new();
538582
for i in 0..zigzag_graph.size() {
539-
parents_map.insert(i, zigzag_graph.expanded_parents(i));
583+
parents_map.insert(i, zigzag_graph.expanded_parents(i, |p| p.clone()));
540584
}
541585

542-
assert_eq!(get_cache_size(&zigzag_graph), zigzag_graph.cache_entries);
543-
544586
parents_map
545587
}
546588

547-
fn get_cache_size<H: 'static + Hasher>(zigzag_graph: &ZigZagBucketGraph<H>) -> usize {
548-
let parents_cache_lock = zigzag_graph.parents_cache.read().unwrap();
549-
(*parents_cache_lock)[zigzag_graph.get_cache_index()].len()
550-
}
551-
552589
// Test that 3 (or more) rounds of the Feistel cipher can be used
553590
// as a pseudorandom permutation, that is, each input will be mapped
554591
// to a unique output (and though not test here, since the cipher

0 commit comments

Comments
 (0)