Skip to content

Commit ef1a7d2

Browse files
committed
accounts-db: Sampled LRU eviction
Instead of rigid LRU eviction, perform sampled LRU eviction. Sampled LRU eviction takes K random keys and picks the one with the lowest update timestamp. This way, even though the evicted element is not always the oldest in the whole cache, removes the necessity of maintaining a queue and reduces the contention caused by locking the queue. The K parameter is configurable, but the best performing value so far was 16 and it's used as the default one. The already existing `concurrent_{read,scan}_write` benchmarks are not sufficient for benchmarking the eviction and evaluating what kind of eviction policy performs the best, because they don't fill up the cache, so eviction never happens. To address that, add a new benchmark group which measures time spent on concurrent writes and reads on a full cache (where each write causes eviction). To reduce the noise coming from other parts of accounts-db, use only the cache in that benchmark. The result of that benchmark with the old rigid LRU evction mechamism, for 128 read and write threads, is: ``` cache_eviction/cache_eviction/128 time: [20.737 s 20.936 s 21.138 s] ``` With sampled LRU eviction (K=16), it improves to: ``` cache_eviction/cache_eviction_k_16/128 time: [7.6609 s 7.7177 s 7.7847 s] ```
1 parent b302e82 commit ef1a7d2

File tree

3 files changed

+261
-195
lines changed

3 files changed

+261
-195
lines changed

accounts-db/benches/bench_read_only_accounts_cache.rs

+98-91
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const NUM_READERS_WRITERS: &[usize] = &[
2525
// 32 cores.
2626
// 32, 64, 128
2727
];
28+
const EVICT_SAMPLE_SIZES: &[usize] = &[1, 2, 4, 8, 10, 16, 32];
2829

2930
/// Benchmarks the read-only cache eviction mechanism. It does so by performing
3031
/// multithreaded reads and writes on a full cache. Each write triggers
@@ -37,106 +38,112 @@ fn bench_cache_eviction(c: &mut Criterion) {
3738

3839
let mut group = c.benchmark_group("cache_eviction");
3940

40-
for num_readers_writers in NUM_READERS_WRITERS {
41-
let cache = Arc::new(ReadOnlyAccountsCache::new(
42-
AccountsDb::DEFAULT_MAX_READ_ONLY_CACHE_DATA_SIZE_LO,
43-
AccountsDb::DEFAULT_MAX_READ_ONLY_CACHE_DATA_SIZE_HI,
44-
AccountsDb::READ_ONLY_CACHE_MS_TO_SKIP_LRU_UPDATE,
45-
));
41+
for evict_sample_size in EVICT_SAMPLE_SIZES {
42+
for num_readers_writers in NUM_READERS_WRITERS {
43+
let cache = Arc::new(ReadOnlyAccountsCache::new(
44+
AccountsDb::DEFAULT_MAX_READ_ONLY_CACHE_DATA_SIZE_LO,
45+
AccountsDb::DEFAULT_MAX_READ_ONLY_CACHE_DATA_SIZE_HI,
46+
*evict_sample_size,
47+
AccountsDb::READ_ONLY_CACHE_MS_TO_SKIP_LRU_UPDATE,
48+
));
4649

47-
// Prepare accounts for the cache fillup.
48-
let pubkeys: Vec<_> = std::iter::repeat_with(solana_sdk::pubkey::new_rand)
49-
.take(NUM_ACCOUNTS_INIT)
50-
.collect();
51-
let accounts_data = std::iter::repeat(
52-
Account {
53-
lamports: 1,
54-
// 1 MiB
55-
data: vec![1; 1024 * 1024],
56-
..Default::default()
57-
}
58-
.to_account_shared_data(),
59-
)
60-
.take(NUM_ACCOUNTS_INIT);
61-
let storable_accounts = pubkeys.iter().zip(accounts_data);
62-
63-
// Fill up the cache.
64-
let slot = 0;
65-
for (pubkey, account) in storable_accounts {
66-
cache.store(*pubkey, slot, account);
67-
}
50+
// Prepare accounts for the cache fillup.
51+
let pubkeys: Vec<_> = std::iter::repeat_with(solana_sdk::pubkey::new_rand)
52+
.take(NUM_ACCOUNTS_INIT)
53+
.collect();
54+
let accounts_data = std::iter::repeat(
55+
Account {
56+
lamports: 1,
57+
// 1 MiB
58+
data: vec![1; 1024 * 1024],
59+
..Default::default()
60+
}
61+
.to_account_shared_data(),
62+
)
63+
.take(NUM_ACCOUNTS_INIT);
64+
let storable_accounts = pubkeys.iter().zip(accounts_data);
6865

69-
// Prepare accounts for the new writes.
70-
let new_storable_accounts = std::iter::repeat_with(solana_sdk::pubkey::new_rand)
71-
.map(|pubkey| {
72-
(
73-
pubkey,
74-
Account {
75-
lamports: 1,
76-
// 1 MiB
77-
data: vec![1; 1024 * 1024],
78-
..Default::default()
79-
}
80-
.to_account_shared_data(),
81-
)
82-
})
83-
.take(NUM_NEW_ACCOUNTS_PER_THREAD);
66+
// Fill up the cache.
67+
let slot = 0;
68+
for (pubkey, account) in storable_accounts {
69+
cache.store(*pubkey, slot, account);
70+
}
8471

85-
// Spawn the reader threads in the background.
86-
let stop_reader = Arc::new(AtomicBool::new(false));
87-
let reader_handles = (0..*num_readers_writers).map(|i| {
88-
let cache = cache.clone();
89-
let pubkeys = pubkeys.clone();
90-
let stop_reader = stop_reader.clone();
91-
Builder::new()
92-
.name(format!("reader{i:02}"))
93-
.spawn({
94-
move || {
95-
// Continuously read random accounts.
96-
let mut rng = SmallRng::seed_from_u64(i as u64);
97-
while !stop_reader.load(Ordering::Relaxed) {
98-
let pubkey = pubkeys.choose(&mut rng).unwrap();
99-
test::black_box(cache.load(*pubkey, slot));
72+
// Prepare accounts for the new writes.
73+
let new_storable_accounts = std::iter::repeat_with(solana_sdk::pubkey::new_rand)
74+
.map(|pubkey| {
75+
(
76+
pubkey,
77+
Account {
78+
lamports: 1,
79+
// 1 MiB
80+
data: vec![1; 1024 * 1024],
81+
..Default::default()
10082
}
101-
}
83+
.to_account_shared_data(),
84+
)
10285
})
103-
.unwrap()
104-
});
86+
.take(NUM_NEW_ACCOUNTS_PER_THREAD);
10587

106-
// Benchmark reads and writes on a full cache, trigerring eviction on each
107-
// write.
108-
let slot = 1;
109-
group.sample_size(10);
110-
group.bench_function(
111-
BenchmarkId::new("cache_eviction", num_readers_writers),
112-
|b| {
113-
b.iter(|| {
114-
// Perform the writes.
115-
let writer_handles = (0..*num_readers_writers).map(|i| {
116-
let cache = cache.clone();
117-
let new_storable_accounts = new_storable_accounts.clone();
118-
Builder::new()
119-
.name(format!("writer{i:02}"))
120-
.spawn({
121-
move || {
122-
for (pubkey, account) in new_storable_accounts {
123-
cache.store(pubkey, slot, account);
88+
// Spawn the reader threads in the background.
89+
let stop_reader = Arc::new(AtomicBool::new(false));
90+
let reader_handles = (0..*num_readers_writers).map(|i| {
91+
let cache = cache.clone();
92+
let pubkeys = pubkeys.clone();
93+
let stop_reader = stop_reader.clone();
94+
Builder::new()
95+
.name(format!("reader{i:02}"))
96+
.spawn({
97+
move || {
98+
// Continuously read random accounts.
99+
let mut rng = SmallRng::seed_from_u64(i as u64);
100+
while !stop_reader.load(Ordering::Relaxed) {
101+
let pubkey = pubkeys.choose(&mut rng).unwrap();
102+
test::black_box(cache.load(*pubkey, slot));
103+
}
104+
}
105+
})
106+
.unwrap()
107+
});
108+
109+
// Benchmark reads and writes on a full cache, trigerring eviction on each
110+
// write.
111+
let slot = 1;
112+
group.sample_size(10);
113+
group.bench_function(
114+
BenchmarkId::new(
115+
format!("cache_eviction_k_{evict_sample_size}"),
116+
num_readers_writers,
117+
),
118+
|b| {
119+
b.iter(|| {
120+
// Perform the writes.
121+
let writer_handles = (0..*num_readers_writers).map(|i| {
122+
let cache = cache.clone();
123+
let new_storable_accounts = new_storable_accounts.clone();
124+
Builder::new()
125+
.name(format!("writer{i:02}"))
126+
.spawn({
127+
move || {
128+
for (pubkey, account) in new_storable_accounts {
129+
cache.store(pubkey, slot, account);
130+
}
124131
}
125-
}
126-
})
127-
.unwrap()
128-
});
132+
})
133+
.unwrap()
134+
});
129135

130-
for writer_handle in writer_handles {
131-
writer_handle.join().unwrap();
132-
}
133-
})
134-
},
135-
);
136+
for writer_handle in writer_handles {
137+
writer_handle.join().unwrap();
138+
}
139+
})
140+
},
141+
);
136142

137-
stop_reader.store(true, Ordering::Relaxed);
138-
for reader_handle in reader_handles {
139-
reader_handle.join().unwrap();
143+
stop_reader.store(true, Ordering::Relaxed);
144+
for reader_handle in reader_handles {
145+
reader_handle.join().unwrap();
146+
}
140147
}
141148
}
142149
}

accounts-db/src/accounts_db.rs

+9
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,7 @@ pub const ACCOUNTS_DB_CONFIG_FOR_TESTING: AccountsDbConfig = AccountsDbConfig {
499499
shrink_paths: None,
500500
shrink_ratio: DEFAULT_ACCOUNTS_SHRINK_THRESHOLD_OPTION,
501501
read_cache_limit_bytes: None,
502+
read_cache_evict_sample_size: None,
502503
write_cache_limit_bytes: None,
503504
ancient_append_vec_offset: None,
504505
ancient_storage_ideal_size: None,
@@ -525,6 +526,7 @@ pub const ACCOUNTS_DB_CONFIG_FOR_BENCHMARKS: AccountsDbConfig = AccountsDbConfig
525526
shrink_paths: None,
526527
shrink_ratio: DEFAULT_ACCOUNTS_SHRINK_THRESHOLD_OPTION,
527528
read_cache_limit_bytes: None,
529+
read_cache_evict_sample_size: None,
528530
write_cache_limit_bytes: None,
529531
ancient_append_vec_offset: None,
530532
ancient_storage_ideal_size: None,
@@ -649,6 +651,7 @@ pub struct AccountsDbConfig {
649651
/// The low and high watermark sizes for the read cache, in bytes.
650652
/// If None, defaults will be used.
651653
pub read_cache_limit_bytes: Option<(usize, usize)>,
654+
pub read_cache_evict_sample_size: Option<usize>,
652655
pub write_cache_limit_bytes: Option<u64>,
653656
/// if None, ancient append vecs are set to ANCIENT_APPEND_VEC_DEFAULT_OFFSET
654657
/// Some(offset) means include slots up to (max_slot - (slots_per_epoch - 'offset'))
@@ -1896,6 +1899,8 @@ impl AccountsDb {
18961899
pub const DEFAULT_MAX_READ_ONLY_CACHE_DATA_SIZE_LO: usize = 400 * 1024 * 1024;
18971900
pub const DEFAULT_MAX_READ_ONLY_CACHE_DATA_SIZE_HI: usize = 410 * 1024 * 1024;
18981901

1902+
const DEFAULT_READ_ONLY_CACHE_EVICT_SAMPLE_SIZE: usize = 16;
1903+
18991904
pub fn default_for_tests() -> Self {
19001905
Self::new_single_for_tests()
19011906
}
@@ -1977,6 +1982,9 @@ impl AccountsDb {
19771982
Self::DEFAULT_MAX_READ_ONLY_CACHE_DATA_SIZE_LO,
19781983
Self::DEFAULT_MAX_READ_ONLY_CACHE_DATA_SIZE_HI,
19791984
));
1985+
let read_cache_evict_sample_size = accounts_db_config
1986+
.read_cache_evict_sample_size
1987+
.unwrap_or(Self::DEFAULT_READ_ONLY_CACHE_EVICT_SAMPLE_SIZE);
19801988

19811989
// Increase the stack for foreground threads
19821990
// rayon needs a lot of stack
@@ -2032,6 +2040,7 @@ impl AccountsDb {
20322040
read_only_accounts_cache: ReadOnlyAccountsCache::new(
20332041
read_cache_size.0,
20342042
read_cache_size.1,
2043+
read_cache_evict_sample_size,
20352044
Self::READ_ONLY_CACHE_MS_TO_SKIP_LRU_UPDATE,
20362045
),
20372046
write_cache_limit_bytes: accounts_db_config.write_cache_limit_bytes,

0 commit comments

Comments
 (0)