| // 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 |