Skip to content

Commit b3fdb9b

Browse files
pdillingerfacebook-github-bot
authored andcommitted
Use safer atomic APIs for some memtable code (facebook#13844)
Summary: Two instances of change that are not just cosmetic: * InlineSkipList<>::Node::CASNext() was implicitly using memory_order_seq_cst to access `next_` while it's intended to be accessed with acquire/release. This is probably not a correctness issue for compare_exchange_strong but potentially a previously missed optimization. * Similar for `max_height_` in Insert which is otherwise accessed with relaxed memory order. * One non-relaxed access to `is_range_del_table_empty_` in a function only used in assertions. Access to this atomic is otherwise relaxed (and should be - comment added) Didn't do all of memtable.h because some of them are more complicated changes and I should probably add FetchMin and FetchMax functions to simplify and take advantage of C++27 functions where available (intended follow-up). Pull Request resolved: facebook#13844 Test Plan: existing tests Reviewed By: xingbowang Differential Revision: D79742552 Pulled By: pdillinger fbshipit-source-id: d97ce72ba9af6c105694b7d40622db9e994720cd
1 parent 5c7162d commit b3fdb9b

7 files changed

Lines changed: 92 additions & 101 deletions

File tree

db/memtable.cc

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ size_t MemTable::ApproximateMemoryUsage() {
179179
}
180180
total_usage += usage;
181181
}
182-
approximate_memory_usage_.store(total_usage, std::memory_order_relaxed);
182+
approximate_memory_usage_.StoreRelaxed(total_usage);
183183
// otherwise, return the actual usage
184184
return total_usage;
185185
}
@@ -193,12 +193,12 @@ bool MemTable::ShouldFlushNow() {
193193
// This is set if memtable_max_range_deletions is > 0,
194194
// and that many range deletions are done
195195
if (memtable_max_range_deletions_ > 0 &&
196-
num_range_deletes_.load(std::memory_order_relaxed) >=
196+
num_range_deletes_.LoadRelaxed() >=
197197
static_cast<uint64_t>(memtable_max_range_deletions_)) {
198198
return true;
199199
}
200200

201-
size_t write_buffer_size = write_buffer_size_.load(std::memory_order_relaxed);
201+
size_t write_buffer_size = write_buffer_size_.LoadRelaxed();
202202
// In a lot of times, we cannot allocate arena blocks that exactly matches the
203203
// buffer size. Thus we have to decide if we should over-allocate or
204204
// under-allocate.
@@ -214,7 +214,7 @@ bool MemTable::ShouldFlushNow() {
214214
auto allocated_memory =
215215
table_->ApproximateMemoryUsage() + arena_.MemoryAllocatedBytes();
216216

217-
approximate_memory_usage_.store(allocated_memory, std::memory_order_relaxed);
217+
approximate_memory_usage_.StoreRelaxed(allocated_memory);
218218

219219
// if we can still allocate one more block without exceeding the
220220
// over-allocation ratio, then we should not flush.
@@ -756,7 +756,7 @@ FragmentedRangeTombstoneIterator* MemTable::NewRangeTombstoneIterator(
756756
const ReadOptions& read_options, SequenceNumber read_seq,
757757
bool immutable_memtable) {
758758
if (read_options.ignore_range_deletions ||
759-
is_range_del_table_empty_.load(std::memory_order_relaxed)) {
759+
is_range_del_table_empty_.LoadRelaxed()) {
760760
return nullptr;
761761
}
762762
return NewRangeTombstoneIteratorInternal(read_options, read_seq,
@@ -767,7 +767,7 @@ FragmentedRangeTombstoneIterator*
767767
MemTable::NewTimestampStrippingRangeTombstoneIterator(
768768
const ReadOptions& read_options, SequenceNumber read_seq, size_t ts_sz) {
769769
if (read_options.ignore_range_deletions ||
770-
is_range_del_table_empty_.load(std::memory_order_relaxed)) {
770+
is_range_del_table_empty_.LoadRelaxed()) {
771771
return nullptr;
772772
}
773773
if (!timestamp_stripping_fragmented_range_tombstone_list_) {
@@ -831,7 +831,7 @@ void MemTable::ConstructFragmentedRangeTombstones() {
831831
// There should be no concurrent Construction.
832832
// We could also check fragmented_range_tombstone_list_ to avoid repeate
833833
// constructions. We just construct them here again to be safe.
834-
if (!is_range_del_table_empty_.load(std::memory_order_relaxed)) {
834+
if (!is_range_del_table_empty_.LoadRelaxed()) {
835835
// TODO: plumb Env::IOActivity, Env::IOPriority
836836
auto* unfragmented_iter = new MemTableIterator(
837837
MemTableIterator::kRangeDelEntries, *this, ReadOptions());
@@ -854,7 +854,7 @@ ReadOnlyMemTable::MemTableStats MemTable::ApproximateStats(
854854
if (entry_count == 0) {
855855
return {0, 0};
856856
}
857-
uint64_t n = num_entries_.load(std::memory_order_relaxed);
857+
uint64_t n = num_entries_.LoadRelaxed();
858858
if (n == 0) {
859859
return {0, 0};
860860
}
@@ -864,7 +864,7 @@ ReadOnlyMemTable::MemTableStats MemTable::ApproximateStats(
864864
// the inaccuracy.
865865
entry_count = n;
866866
}
867-
uint64_t data_size = data_size_.load(std::memory_order_relaxed);
867+
uint64_t data_size = data_size_.LoadRelaxed();
868868
return {entry_count * (data_size / n), entry_count};
869869
}
870870

@@ -994,17 +994,14 @@ Status MemTable::Add(SequenceNumber s, ValueType type,
994994

995995
// this is a bit ugly, but is the way to avoid locked instructions
996996
// when incrementing an atomic
997-
num_entries_.store(num_entries_.load(std::memory_order_relaxed) + 1,
998-
std::memory_order_relaxed);
999-
data_size_.store(data_size_.load(std::memory_order_relaxed) + encoded_len,
1000-
std::memory_order_relaxed);
997+
num_entries_.StoreRelaxed(num_entries_.LoadRelaxed() + 1);
998+
data_size_.StoreRelaxed(data_size_.LoadRelaxed() + encoded_len);
1001999
if (type == kTypeDeletion || type == kTypeSingleDeletion ||
10021000
type == kTypeDeletionWithTimestamp) {
1003-
num_deletes_.store(num_deletes_.load(std::memory_order_relaxed) + 1,
1004-
std::memory_order_relaxed);
1001+
num_deletes_.StoreRelaxed(num_deletes_.LoadRelaxed() + 1);
10051002
} else if (type == kTypeRangeDeletion) {
1006-
uint64_t val = num_range_deletes_.load(std::memory_order_relaxed) + 1;
1007-
num_range_deletes_.store(val, std::memory_order_relaxed);
1003+
uint64_t val = num_range_deletes_.LoadRelaxed() + 1;
1004+
num_range_deletes_.StoreRelaxed(val);
10081005
}
10091006

10101007
if (bloom_filter_ && prefix_extractor_ &&
@@ -1105,7 +1102,7 @@ Status MemTable::Add(SequenceNumber s, ValueType type,
11051102
if (allow_concurrent) {
11061103
range_del_mutex_.unlock();
11071104
}
1108-
is_range_del_table_empty_.store(false, std::memory_order_relaxed);
1105+
is_range_del_table_empty_.StoreRelaxed(false);
11091106
}
11101107
UpdateOldestKeyTime();
11111108

@@ -1524,7 +1521,7 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
15241521
// range tombstones. This is the simplest way to ensure range tombstones are
15251522
// handled. TODO: allow Bloom checks where max_covering_tombstone_seq==0
15261523
bool no_range_del = read_options.ignore_range_deletions ||
1527-
is_range_del_table_empty_.load(std::memory_order_relaxed);
1524+
is_range_del_table_empty_.LoadRelaxed();
15281525
MultiGetRange temp_range(*range, range->begin(), range->end());
15291526
if (bloom_filter_ && no_range_del) {
15301527
bool whole_key =

db/memtable.h

Lines changed: 27 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
// found in the LICENSE file. See the AUTHORS file for names of contributors.
99

1010
#pragma once
11-
#include <atomic>
1211
#include <deque>
1312
#include <functional>
1413
#include <memory>
@@ -568,7 +567,7 @@ class MemTable final : public ReadOnlyMemTable {
568567
// As a cheap version of `ApproximateMemoryUsage()`, this function doesn't
569568
// require external synchronization. The value may be less accurate though
570569
size_t ApproximateMemoryUsageFast() const {
571-
return approximate_memory_usage_.load(std::memory_order_relaxed);
570+
return approximate_memory_usage_.LoadRelaxed();
572571
}
573572

574573
size_t MemoryAllocatedBytes() const override {
@@ -689,16 +688,13 @@ class MemTable final : public ReadOnlyMemTable {
689688
// Used in concurrent memtable inserts.
690689
void BatchPostProcess(const MemTablePostProcessInfo& update_counters) {
691690
table_->BatchPostProcess();
692-
num_entries_.fetch_add(update_counters.num_entries,
693-
std::memory_order_relaxed);
694-
data_size_.fetch_add(update_counters.data_size, std::memory_order_relaxed);
691+
num_entries_.FetchAddRelaxed(update_counters.num_entries);
692+
data_size_.FetchAddRelaxed(update_counters.data_size);
695693
if (update_counters.num_deletes != 0) {
696-
num_deletes_.fetch_add(update_counters.num_deletes,
697-
std::memory_order_relaxed);
694+
num_deletes_.FetchAddRelaxed(update_counters.num_deletes);
698695
}
699696
if (update_counters.num_range_deletes > 0) {
700-
num_range_deletes_.fetch_add(update_counters.num_range_deletes,
701-
std::memory_order_relaxed);
697+
num_range_deletes_.FetchAddRelaxed(update_counters.num_range_deletes);
702698
// noop for skip-list memtable
703699
// Besides correctness test in stress test, memtable flush record count
704700
// check will catch this if it were not noop.
@@ -707,35 +703,26 @@ class MemTable final : public ReadOnlyMemTable {
707703
UpdateFlushState();
708704
}
709705

710-
uint64_t NumEntries() const override {
711-
return num_entries_.load(std::memory_order_relaxed);
712-
}
706+
uint64_t NumEntries() const override { return num_entries_.LoadRelaxed(); }
713707

714-
uint64_t NumDeletion() const override {
715-
return num_deletes_.load(std::memory_order_relaxed);
716-
}
708+
uint64_t NumDeletion() const override { return num_deletes_.LoadRelaxed(); }
717709

718710
uint64_t NumRangeDeletion() const override {
719-
return num_range_deletes_.load(std::memory_order_relaxed);
711+
return num_range_deletes_.LoadRelaxed();
720712
}
721713

722-
uint64_t GetDataSize() const override {
723-
return data_size_.load(std::memory_order_relaxed);
724-
}
714+
uint64_t GetDataSize() const override { return data_size_.LoadRelaxed(); }
725715

726-
size_t write_buffer_size() const {
727-
return write_buffer_size_.load(std::memory_order_relaxed);
728-
}
716+
size_t write_buffer_size() const { return write_buffer_size_.LoadRelaxed(); }
729717

730718
// Dynamically change the memtable's capacity. If set below the current usage,
731719
// the next key added will trigger a flush. Can only increase size when
732720
// memtable prefix bloom is disabled, since we can't easily allocate more
733-
// space.
721+
// space. Non-atomic update ok because this is only called with DB mutex held.
734722
void UpdateWriteBufferSize(size_t new_write_buffer_size) {
735723
if (bloom_filter_ == nullptr ||
736-
new_write_buffer_size < write_buffer_size_) {
737-
write_buffer_size_.store(new_write_buffer_size,
738-
std::memory_order_relaxed);
724+
new_write_buffer_size < write_buffer_size_.LoadRelaxed()) {
725+
write_buffer_size_.StoreRelaxed(new_write_buffer_size);
739726
}
740727
}
741728

@@ -827,7 +814,7 @@ class MemTable final : public ReadOnlyMemTable {
827814

828815
bool IsFragmentedRangeTombstonesConstructed() const override {
829816
return fragmented_range_tombstone_list_.get() != nullptr ||
830-
is_range_del_table_empty_;
817+
is_range_del_table_empty_.LoadRelaxed();
831818
}
832819

833820
// Gets the newest user defined timestamps in the memtable. This should only
@@ -853,16 +840,22 @@ class MemTable final : public ReadOnlyMemTable {
853840
ConcurrentArena arena_;
854841
std::unique_ptr<MemTableRep> table_;
855842
std::unique_ptr<MemTableRep> range_del_table_;
856-
std::atomic_bool is_range_del_table_empty_;
843+
// This is OK to be relaxed access because consistency between table_ and
844+
// range_del_table_ is provided by explicit multi-versioning with sequence
845+
// numbers. It's ok for stale memory to say the range_del_table_ is empty when
846+
// it's actually not because if it was relevant to our read (based on sequence
847+
// number), the relaxed memory read would get a sufficiently updated value
848+
// because of the ordering provided by LastPublishedSequence().
849+
RelaxedAtomic<bool> is_range_del_table_empty_;
857850

858851
// Total data size of all data inserted
859-
std::atomic<uint64_t> data_size_;
860-
std::atomic<uint64_t> num_entries_;
861-
std::atomic<uint64_t> num_deletes_;
862-
std::atomic<uint64_t> num_range_deletes_;
852+
RelaxedAtomic<uint64_t> data_size_;
853+
RelaxedAtomic<uint64_t> num_entries_;
854+
RelaxedAtomic<uint64_t> num_deletes_;
855+
RelaxedAtomic<uint64_t> num_range_deletes_;
863856

864857
// Dynamically changeable memtable option
865-
std::atomic<size_t> write_buffer_size_;
858+
RelaxedAtomic<size_t> write_buffer_size_;
866859

867860
// The sequence number of the kv that was inserted first
868861
std::atomic<SequenceNumber> first_seqno_;
@@ -898,7 +891,7 @@ class MemTable final : public ReadOnlyMemTable {
898891

899892
// keep track of memory usage in table_, arena_, and range_del_table_.
900893
// Gets refreshed inside `ApproximateMemoryUsage()` or `ShouldFlushNow`
901-
std::atomic<uint64_t> approximate_memory_usage_;
894+
RelaxedAtomic<uint64_t> approximate_memory_usage_;
902895

903896
// max range deletions in a memtable, before automatic flushing, 0 for
904897
// unlimited.

memtable/inlineskiplist.h

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,14 @@
4444
#include <assert.h>
4545
#include <stdlib.h>
4646

47-
#include <algorithm>
48-
#include <atomic>
4947
#include <type_traits>
5048

5149
#include "memory/allocator.h"
5250
#include "port/likely.h"
5351
#include "port/port.h"
5452
#include "rocksdb/slice.h"
5553
#include "test_util/sync_point.h"
56-
#include "util/coding.h"
54+
#include "util/atomic.h"
5755
#include "util/random.h"
5856

5957
namespace ROCKSDB_NAMESPACE {
@@ -215,18 +213,17 @@ class InlineSkipList {
215213
Comparator const compare_;
216214
Node* const head_;
217215

218-
// Modified only by Insert(). Read racily by readers, but stale
219-
// values are ok.
220-
std::atomic<int> max_height_; // Height of the entire list
216+
// Maximum height of any node in the list (or in the process of being added).
217+
// Modified only by Insert(). Relaxed reads are always OK because starting
218+
// from higher levels only helps efficiency, not correctness.
219+
RelaxedAtomic<int> max_height_;
221220

222221
// seq_splice_ is a Splice used for insertions in the non-concurrent
223222
// case. It caches the prev and next found during the most recent
224223
// non-concurrent insertion.
225224
Splice* seq_splice_;
226225

227-
inline int GetMaxHeight() const {
228-
return max_height_.load(std::memory_order_relaxed);
229-
}
226+
inline int GetMaxHeight() const { return max_height_.LoadRelaxed(); }
230227

231228
int RandomHeight();
232229

@@ -311,7 +308,7 @@ struct InlineSkipList<Comparator>::Node {
311308
// Stores the height of the node in the memory location normally used for
312309
// next_[0]. This is used for passing data from AllocateKey to Insert.
313310
void StashHeight(const int height) {
314-
assert(sizeof(int) <= sizeof(next_[0]));
311+
static_assert(sizeof(int) <= sizeof(next_[0]));
315312
memcpy(static_cast<void*>(&next_[0]), &height, sizeof(int));
316313
}
317314

@@ -332,30 +329,30 @@ struct InlineSkipList<Comparator>::Node {
332329
assert(n >= 0);
333330
// Use an 'acquire load' so that we observe a fully initialized
334331
// version of the returned Node.
335-
return ((&next_[0] - n)->load(std::memory_order_acquire));
332+
return ((&next_[0] - n)->Load());
336333
}
337334

338335
void SetNext(int n, Node* x) {
339336
assert(n >= 0);
340337
// Use a 'release store' so that anybody who reads through this
341338
// pointer observes a fully initialized version of the inserted node.
342-
(&next_[0] - n)->store(x, std::memory_order_release);
339+
(&next_[0] - n)->Store(x);
343340
}
344341

345342
bool CASNext(int n, Node* expected, Node* x) {
346343
assert(n >= 0);
347-
return (&next_[0] - n)->compare_exchange_strong(expected, x);
344+
return (&next_[0] - n)->CasStrong(expected, x);
348345
}
349346

350347
// No-barrier variants that can be safely used in a few locations.
351348
Node* NoBarrier_Next(int n) {
352349
assert(n >= 0);
353-
return (&next_[0] - n)->load(std::memory_order_relaxed);
350+
return (&next_[0] - n)->LoadRelaxed();
354351
}
355352

356353
void NoBarrier_SetNext(int n, Node* x) {
357354
assert(n >= 0);
358-
(&next_[0] - n)->store(x, std::memory_order_relaxed);
355+
(&next_[0] - n)->StoreRelaxed(x);
359356
}
360357

361358
// Insert node after prev on specific level.
@@ -369,7 +366,7 @@ struct InlineSkipList<Comparator>::Node {
369366
private:
370367
// next_[0] is the lowest level link (level 0). Higher levels are
371368
// stored _earlier_, so level 1 is at next_[-1].
372-
std::atomic<Node*> next_[1];
369+
AcqRelAtomic<Node*> next_[1];
373370
};
374371

375372
template <class Comparator>
@@ -789,7 +786,7 @@ char* InlineSkipList<Comparator>::AllocateKey(size_t key_size) {
789786
template <class Comparator>
790787
typename InlineSkipList<Comparator>::Node*
791788
InlineSkipList<Comparator>::AllocateNode(size_t key_size, int height) {
792-
auto prefix = sizeof(std::atomic<Node*>) * (height - 1);
789+
auto prefix = sizeof(AcqRelAtomic<Node*>) * (height - 1);
793790

794791
// prefix is space for the height - 1 pointers that we store before
795792
// the Node instance (next_[-(height - 1) .. -1]). Node starts at
@@ -923,9 +920,9 @@ bool InlineSkipList<Comparator>::Insert(const char* key, Splice* splice,
923920
int height = x->UnstashHeight();
924921
assert(height >= 1 && height <= kMaxHeight_);
925922

926-
int max_height = max_height_.load(std::memory_order_relaxed);
923+
int max_height = max_height_.LoadRelaxed();
927924
while (height > max_height) {
928-
if (max_height_.compare_exchange_weak(max_height, height)) {
925+
if (max_height_.CasWeakRelaxed(max_height, height)) {
929926
// successfully updated it
930927
max_height = height;
931928
break;

0 commit comments

Comments
 (0)