blob: 85ab4e9cb1b038879b05d103aa04a1eaf1ea03fa [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#include <unistd.h>
#include <gtest/gtest.h>
#include <pthread.h>
#include <mutex>
#include <cstdio>
#include "butil/atomicops.h"
#include "butil/time.h"
#include "butil/synchronization/lock.h"
#include "butil/synchronization/seqlock.h"
namespace {
// A multi-word payload. The seqlock's job is to make every reader observe a
// snapshot in which all words are equal; a torn read would see a mix of an old
// and a new value. Every field is a relaxed atomic, as Seqlock requires.
static const int kWords = 8;
struct Payload {
butil::atomic<uint64_t> w[kWords];
void relaxed_set(uint64_t v) {
for (auto& i : w) {
// Store word by word (not as one atomic group) so that, without the
// seqlock, a concurrent reader could observe a torn value.
i.store(v, butil::memory_order_relaxed);
}
}
// Returns the first word and whether all words are equal to it.
uint64_t relaxed_get(bool* consistent) const {
uint64_t v0 = w[0].load(butil::memory_order_relaxed);
*consistent = true;
for (int i = 1; i < kWords; ++i) {
if (w[i].load(butil::memory_order_relaxed) != v0) {
*consistent = false;
}
}
return v0;
}
};
TEST(SeqlockTest, SingleThreadedReadWrite) {
butil::Seqlock<> seqlock;
Payload payload;
payload.relaxed_set(0);
for (uint64_t v = 1; v <= 1000; ++v) {
seqlock.store([&] { payload.relaxed_set(v); });
uint64_t got = seqlock.load([&] {
bool consistent = false;
uint64_t r = payload.relaxed_get(&consistent);
EXPECT_TRUE(consistent);
return r;
});
ASSERT_EQ(v, got);
}
}
TEST(SeqlockTest, LoadReturnsValueByType) {
butil::Seqlock<> seqlock;
butil::atomic<int> payload(0);
seqlock.store([&] {
payload.store(42, butil::memory_order_relaxed);
});
// load returns whatever the callback returns, by value.
int v = seqlock.load([&] {
return payload.load(butil::memory_order_relaxed);
});
ASSERT_EQ(42, v);
// A different return type also works.
std::pair<int, int> pr = seqlock.load([&] {
int v = payload.load(butil::memory_order_relaxed);
return std::make_pair(v, v + 1);
});
ASSERT_EQ(42, pr.first);
ASSERT_EQ(43, pr.second);
}
TEST(SeqlockTest, MutexSpecializationSingleThreaded) {
butil::Seqlock<butil::Mutex> seqlock;
Payload payload;
payload.relaxed_set(0);
for (uint64_t v = 1; v <= 1000; ++v) {
seqlock.store([&] { payload.relaxed_set(v); });
bool consistent = false;
uint64_t got = seqlock.load([&] {
bool c = false;
uint64_t r = payload.relaxed_get(&c);
consistent = c;
return r;
});
ASSERT_TRUE(consistent);
ASSERT_EQ(v, got);
}
}
// Single-threaded performance comparison: Seqlock vs Mutex.
//
// There is no contention here, so this measures the bare cost of the
// synchronization itself:
// * Seqlock<> : one relaxed counter load, two counter stores and a
// release fence on the write path; two counter loads and
// an acquire fence on the read path (no atomic RMW in
// either path).
// * Seqlock<Mutex> : the same counter work plus a lock/unlock pair on the
// write path; the read path is identical to Seqlock<>.
// * Mutex : a lock/unlock pair (atomic RMW + potential syscall on
// contention) on BOTH the write and the read path.
//
// Timings are reported, not asserted: absolute numbers depend on the machine,
// the compiler and the current load, so turning them into thresholds would
// make the test flaky. The test only asserts that the measurements ran and
// produced the expected values.
static const int kPerfRounds = 1000000;
// Accumulates every value read so the compiler cannot optimize the read loops
// away as dead code.
static butil::atomic<uint64_t> g_perf_sink(0);
inline void perf_consume(uint64_t v) {
g_perf_sink.fetch_add(v, butil::memory_order_relaxed);
}
// A pure compiler barrier: it emits no instruction, but forbids the compiler
// from moving memory accesses across it. Without one between the rounds below,
// a write body made purely of relaxed stores is legally collapsible into its
// last iteration -- which is exactly what happens to Seqlock<> once its counter
// increments are plain stores rather than fetch_add, and it measures 0 ns/op.
// The mutex bodies resist that on their own; barriering all of them keeps the
// comparison between the three honest.
inline void perf_barrier() {
butil::atomic_signal_fence(butil::memory_order_seq_cst);
}
// Runs `body(round)` for `rounds` rounds and returns the elapsed nanoseconds.
// A short warmup runs first so that page faults, branch predictor and cache
// warmup are not charged to the measured loop.
template <typename Body>
int64_t TimeLoopNs(int rounds, Body&& body) {
int kWarmup = 1000;
for (int i = 1; i <= kWarmup; ++i) {
body(static_cast<uint64_t>(i));
perf_barrier();
}
int64_t start_ns = butil::cpuwide_time_ns();
for (int i = 1; i <= rounds; ++i) {
body(static_cast<uint64_t>(i));
perf_barrier();
}
return butil::cpuwide_time_ns() - start_ns;
}
inline double ns_per_op(int64_t elapsed_ns, int rounds) {
return static_cast<double>(elapsed_ns) / rounds;
}
TEST(SeqlockTest, SingleThreadedPerfVsMutex) {
// Seqlock<>: single-writer, no mutex.
butil::Seqlock<> seqlock;
Payload seqlock_payload;
seqlock_payload.relaxed_set(0);
int64_t seqlock_write_ns = TimeLoopNs(kPerfRounds, [&](uint64_t v) {
seqlock.store([&] { seqlock_payload.relaxed_set(v); });
// Make both the counter and payload observable after every write so
// the compiler cannot eliminate stores to these local test objects.
asm volatile("" : : "m"(seqlock), "m"(seqlock_payload) : "memory");
});
int64_t seqlock_read_ns = TimeLoopNs(kPerfRounds, [&](uint64_t) {
perf_consume(seqlock.load([&] {
bool consistent = false;
return seqlock_payload.relaxed_get(&consistent);
}));
});
// Seqlock<Mutex>: same read path, writes additionally take a mutex.
butil::Seqlock<butil::Mutex> mutex_seqlock;
Payload mutex_seqlock_payload;
mutex_seqlock_payload.relaxed_set(0);
int64_t mutex_seqlock_write_ns = TimeLoopNs(kPerfRounds, [&](uint64_t v) {
mutex_seqlock.store([&] { mutex_seqlock_payload.relaxed_set(v); });
asm volatile("" : : "m"(mutex_seqlock), "m"(mutex_seqlock_payload)
: "memory");
});
int64_t mutex_seqlock_read_ns = TimeLoopNs(kPerfRounds, [&](uint64_t) {
perf_consume(mutex_seqlock.load([&] {
bool consistent = false;
return mutex_seqlock_payload.relaxed_get(&consistent);
}));
});
// Plain Mutex baseline: both sides take the lock.
butil::Mutex mutex;
Payload mutex_payload;
mutex_payload.relaxed_set(0);
int64_t mutex_write_ns = TimeLoopNs(kPerfRounds, [&](uint64_t v) {
{
std::lock_guard<butil::Mutex> lk(mutex);
mutex_payload.relaxed_set(v);
}
asm volatile("" : : "m"(mutex), "m"(mutex_payload) : "memory");
});
int64_t mutex_read_ns = TimeLoopNs(kPerfRounds, [&](uint64_t) {
std::lock_guard<butil::Mutex> lk(mutex);
bool consistent = false;
perf_consume(mutex_payload.relaxed_get(&consistent));
});
printf("\n[seqlock perf] single thread, %d rounds, payload=%d words\n",
kPerfRounds, kWords);
printf("%-18s %14s %14s\n", "impl", "write(ns/op)", "read(ns/op)");
printf("%-18s %14.2f %14.2f\n", "Seqlock<>",
ns_per_op(seqlock_write_ns, kPerfRounds),
ns_per_op(seqlock_read_ns, kPerfRounds));
printf("%-18s %14.2f %14.2f\n", "Seqlock<Mutex>",
ns_per_op(mutex_seqlock_write_ns, kPerfRounds),
ns_per_op(mutex_seqlock_read_ns, kPerfRounds));
printf("%-18s %14.2f %14.2f\n", "Mutex",
ns_per_op(mutex_write_ns, kPerfRounds),
ns_per_op(mutex_read_ns, kPerfRounds));
printf("%-18s %13.2fx %13.2fx\n", "Mutex/Seqlock<>",
static_cast<double>(mutex_write_ns) / seqlock_write_ns,
static_cast<double>(mutex_read_ns) / seqlock_read_ns);
// The loops really ran and every implementation published the last value.
ASSERT_GT(seqlock_write_ns, 0);
ASSERT_GT(mutex_seqlock_write_ns, 0);
ASSERT_GT(mutex_write_ns, 0);
ASSERT_GT(seqlock_read_ns, 0);
ASSERT_GT(mutex_seqlock_read_ns, 0);
ASSERT_GT(mutex_read_ns, 0);
ASSERT_GT(g_perf_sink.load(butil::memory_order_relaxed), 0u);
bool consistent = false;
ASSERT_EQ(static_cast<uint64_t>(kPerfRounds),
seqlock_payload.relaxed_get(&consistent));
ASSERT_TRUE(consistent);
ASSERT_EQ(static_cast<uint64_t>(kPerfRounds),
mutex_seqlock_payload.relaxed_get(&consistent));
ASSERT_TRUE(consistent);
ASSERT_EQ(static_cast<uint64_t>(kPerfRounds),
mutex_payload.relaxed_get(&consistent));
ASSERT_TRUE(consistent);
}
// Concurrent consistency tests
struct SharedState {
butil::Seqlock<>* single_writer_seqlock = nullptr; // single-writer lock
butil::Seqlock<butil::Mutex>* multi_writer_seqlock = nullptr; // multi-writer lock
Payload payload;
butil::atomic<bool> stopped{false};
butil::atomic<uint64_t> version{0}; // source of the value written
butil::atomic<uint64_t> reads{0}; // total reads performed
butil::atomic<uint64_t> torn{0}; // inconsistent snapshots observed
};
void* SingleWriterThread(void* arg) {
SharedState* shared_state = static_cast<SharedState*>(arg);
while (!shared_state->stopped.load(butil::memory_order_relaxed)) {
uint64_t version = shared_state->version.fetch_add(1, butil::memory_order_relaxed) + 1;
shared_state->single_writer_seqlock->store([&] {
shared_state->payload.relaxed_set(version);
});
}
return nullptr;
}
void* MultiWriterThread(void* arg) {
SharedState* shared_state = static_cast<SharedState*>(arg);
while (!shared_state->stopped.load(butil::memory_order_relaxed)) {
uint64_t version = shared_state->version.fetch_add(1, butil::memory_order_relaxed) + 1;
shared_state->multi_writer_seqlock->store([&] {
shared_state->payload.relaxed_set(version);
});
}
return nullptr;
}
struct ReaderArg {
SharedState* shared_state;
bool multi_writer;
};
void* ReaderThread(void* arg) {
ReaderArg* reader_arg = static_cast<ReaderArg*>(arg);
SharedState* shared_state = reader_arg->shared_state;
uint64_t local_reads = 0;
uint64_t local_torn = 0;
while (!shared_state->stopped.load(butil::memory_order_relaxed)) {
bool consistent = false;
auto load_body = [&] {
bool c = false;
uint64_t r = shared_state->payload.relaxed_get(&c);
consistent = c;
return r;
};
if (reader_arg->multi_writer) {
shared_state->multi_writer_seqlock->load(load_body);
} else {
shared_state->single_writer_seqlock->load(load_body);
}
++local_reads;
if (!consistent) {
++local_torn;
}
}
shared_state->reads.fetch_add(local_reads, butil::memory_order_relaxed);
shared_state->torn.fetch_add(local_torn, butil::memory_order_relaxed);
return nullptr;
}
TEST(SeqlockTest, SingleWriterManyReaders) {
SharedState shared_state;
butil::Seqlock<> sl;
shared_state.single_writer_seqlock = &sl;
shared_state.payload.relaxed_set(0);
const int kReaders = 4;
pthread_t writer;
pthread_t readers[kReaders];
ReaderArg args[kReaders];
ASSERT_EQ(0, pthread_create(&writer, nullptr, SingleWriterThread, &shared_state));
for (int i = 0; i < kReaders; ++i) {
args[i].shared_state = &shared_state;
args[i].multi_writer = false;
ASSERT_EQ(0, pthread_create(&readers[i], nullptr, ReaderThread, &args[i]));
}
usleep(500 * 1000); // 0.5s of hammering
shared_state.stopped.store(true, butil::memory_order_relaxed);
pthread_join(writer, nullptr);
for (auto reader : readers) {
pthread_join(reader, nullptr);
}
ASSERT_GT(shared_state.reads.load(), 0u);
ASSERT_EQ(0u, shared_state.torn.load()) << "readers observed torn snapshots";
}
TEST(SeqlockTest, MultiWriterManyReaders) {
SharedState shared_state;
butil::Seqlock<butil::Mutex> seqlock;
shared_state.multi_writer_seqlock = &seqlock;
shared_state.payload.relaxed_set(0);
const int kWriters = 3;
const int kReaders = 4;
pthread_t writers[kWriters];
pthread_t readers[kReaders];
ReaderArg args[kReaders];
for (auto& writer : writers) {
ASSERT_EQ(0, pthread_create(&writer, nullptr, MultiWriterThread, &shared_state));
}
for (int i = 0; i < kReaders; ++i) {
args[i].shared_state = &shared_state;
args[i].multi_writer = true;
ASSERT_EQ(0, pthread_create(&readers[i], nullptr, ReaderThread, &args[i]));
}
usleep(500 * 1000);
shared_state.stopped.store(true, butil::memory_order_relaxed);
for (auto writer : writers) {
pthread_join(writer, nullptr);
}
for (auto reader : readers) {
pthread_join(reader, nullptr);
}
ASSERT_GT(shared_state.reads.load(), 0u);
ASSERT_EQ(0u, shared_state.torn.load()) << "readers observed torn snapshots";
}
} // namespace