1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
   31
   32
   33
   34
   35
   36
   37
   38
   39
   40
   41
   42
   43
   44
   45
   46
   47
   48
   49
   50
   51
   52
   53
   54
   55
   56
   57
   58
   59
   60
   61
   62
   63
   64
   65
   66
   67
   68
   69
   70
   71
   72
   73
   74
   75
   76
   77
   78
   79
   80
   81
   82
   83
   84
   85
   86
   87
   88
   89
   90
   91
   92
   93
   94
   95
   96
   97
   98
   99
  100
  101
  102
  103
  104
  105
  106
  107
  108
  109
  110
  111
  112
  113
  114
  115
  116
  117
  118
  119
  120
  121
  122
  123
  124
  125
  126
  127
  128
  129
  130
  131
  132
  133
  134
  135
  136
  137
  138
  139
  140
  141
  142
  143
  144
  145
  146
  147
  148
  149
  150
  151
  152
  153
  154
  155
  156
  157
  158
  159
  160
  161
  162
  163
  164
  165
  166
  167
  168
  169
  170
  171
  172
  173
  174
  175
  176
  177
  178
  179
  180

base / threading / counter_perftest.cc [blame]

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <atomic>
#include <string>

#include "base/barrier_closure.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/synchronization/lock.h"
#include "base/synchronization/waitable_event.h"
#include "base/threading/simple_thread.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/perf/perf_result_reporter.h"

// This file contains tests to measure the cost of incrementing:
// - A non-atomic variable, no lock.
// - A non-atomic variable, with lock.
// - An atomic variable, no memory barriers.
// - An atomic variable, acquire-release barriers.
// The goal is to provide data to guide counter implementation choices.

namespace base {

namespace {

constexpr char kMetricPrefixCounter[] = "Counter.";
constexpr char kMetricOperationThroughput[] = "operation_throughput";
constexpr uint64_t kNumIterations = 100000000;

perf_test::PerfResultReporter SetUpReporter(const std::string& story_name) {
  perf_test::PerfResultReporter reporter(kMetricPrefixCounter, story_name);
  reporter.RegisterImportantMetric(kMetricOperationThroughput, "operations/ms");
  return reporter;
}

class Uint64_NoLock {
 public:
  Uint64_NoLock() = default;
  void Increment() { counter_ = counter_ + 1; }
  uint64_t value() const { return counter_; }

 private:
  // Volatile to prevent the compiler from over-optimizing the increment.
  volatile uint64_t counter_ = 0;
};

class Uint64_Lock {
 public:
  Uint64_Lock() = default;
  void Increment() {
    AutoLock auto_lock(lock_);
    ++counter_;
  }
  uint64_t value() const {
    AutoLock auto_lock(lock_);
    return counter_;
  }

 private:
  mutable Lock lock_;
  uint64_t counter_ GUARDED_BY(lock_) = 0;
};

class AtomicUint64_NoBarrier {
 public:
  AtomicUint64_NoBarrier() = default;
  void Increment() { counter_.fetch_add(1, std::memory_order_relaxed); }
  uint64_t value() const { return counter_; }

 private:
  std::atomic<uint64_t> counter_{0};
};

class AtomicUint64_Barrier {
 public:
  AtomicUint64_Barrier() = default;
  void Increment() { counter_.fetch_add(1, std::memory_order_acq_rel); }
  uint64_t value() const { return counter_; }

 private:
  std::atomic<uint64_t> counter_{0};
};

template <typename CounterType>
class IncrementThread : public SimpleThread {
 public:
  // Upon entering its main function, the thread waits for |start_event| to be
  // signaled. Then, it increments |counter| |kNumIterations| times.
  // Finally, it invokes |done_closure|.
  explicit IncrementThread(WaitableEvent* start_event,
                           CounterType* counter,
                           OnceClosure done_closure)
      : SimpleThread("IncrementThread"),
        start_event_(start_event),
        counter_(counter),
        done_closure_(std::move(done_closure)) {}

  // SimpleThread:
  void Run() override {
    start_event_->Wait();
    for (uint64_t i = 0; i < kNumIterations; ++i)
      counter_->Increment();
    std::move(done_closure_).Run();
  }

 private:
  const raw_ptr<WaitableEvent> start_event_;
  const raw_ptr<CounterType> counter_;
  OnceClosure done_closure_;
};

template <typename CounterType>
void RunIncrementPerfTest(const std::string& story_name, int num_threads) {
  WaitableEvent start_event;
  WaitableEvent end_event;
  CounterType counter;
  RepeatingClosure done_closure = BarrierClosure(
      num_threads, BindOnce(&WaitableEvent::Signal, Unretained(&end_event)));

  std::vector<std::unique_ptr<IncrementThread<CounterType>>> threads;
  for (int i = 0; i < num_threads; ++i) {
    threads.push_back(std::make_unique<IncrementThread<CounterType>>(
        &start_event, &counter, done_closure));
    threads.back()->Start();
  }

  TimeTicks start_time = TimeTicks::Now();
  start_event.Signal();
  end_event.Wait();
  TimeTicks end_time = TimeTicks::Now();

  EXPECT_EQ(num_threads * kNumIterations, counter.value());

  auto reporter = SetUpReporter(story_name);
  reporter.AddResult(
      kMetricOperationThroughput,
      kNumIterations / (end_time - start_time).InMillisecondsF());

  for (auto& thread : threads)
    thread->Join();
}

}  // namespace

TEST(CounterPerfTest, Uint64_NoLock_1Thread) {
  RunIncrementPerfTest<Uint64_NoLock>("Uint64_NoLock_1Thread", 1);
}

// No Uint64_NoLock_4Threads test because it would cause data races.

TEST(CounterPerfTest, Uint64_Lock_1Thread) {
  RunIncrementPerfTest<Uint64_Lock>("Uint64_Lock_1Thread", 1);
}

TEST(CounterPerfTest, Uint64_Lock_4Threads) {
  RunIncrementPerfTest<Uint64_Lock>("Uint64_Lock_4Threads", 4);
}

TEST(CounterPerfTest, AtomicUint64_NoBarrier_1Thread) {
  RunIncrementPerfTest<AtomicUint64_NoBarrier>("AtomicUint64_NoBarrier_1Thread",
                                               1);
}

TEST(CounterPerfTest, AtomicUint64_NoBarrier_4Threads) {
  RunIncrementPerfTest<AtomicUint64_NoBarrier>(
      "AtomicUint64_NoBarrier_4Threads", 4);
}

TEST(CounterPerfTest, AtomicUint64_Barrier_1Thread) {
  RunIncrementPerfTest<AtomicUint64_Barrier>("AtomicUint64_Barrier_1Thread", 1);
}

TEST(CounterPerfTest, AtomicUint64_Barrier_4Threads) {
  RunIncrementPerfTest<AtomicUint64_Barrier>("AtomicUint64_Barrier_4Threads",
                                             4);
}

}  // namespace base