# Mutex lock benchmark

I'm trying to measure how many CPU cycles a mutex lock takes. Can you find any problems in my mutex lock benchmark?

#include <iostream>
#include <mutex>
#include <chrono>
#include <string>

std::mutex twiceMutex;
typedef std::int64_t i64;

i64 twice(i64 x)
{
return 2 * x;
}

i64 twiceM(i64 x)
{
std::lock_guard<std::mutex> lock(twiceMutex);
return 2 * x;
}

const i64 I = 100'000'000; // number of iterations

struct Benchmark
{
i64 (*fn)(i64);

i64 operator()()const
{
namespace chrono = std::chrono;
i64 res = 0;
for (i64 i = 0; i < I; i++)
{
res += fn(i);
}

auto elapsed = chrono::duration_cast<std::chrono::nanoseconds>(endTime - startTime).count();
return elapsed;
}
};

int main()
{
const double T = Benchmark{ twice }(); // total nanoseconds without mutex
const double TM = Benchmark{ twiceM }(); // total nanoseconds with mutex

const double K = 3.7; //GHz of my CPU
const double t = T / I; // nanoseconds per iteration, without mutex
const double tm = TM / I; // nanoseconds per iteration, with mutex
const double m = tm - t; // difference nanoseconds per iteration, this should be the cost of locking a mutex in nanoseconds

// print the results in cycles
std::cout << t * K << std::endl;
std::cout << tm * K << std::endl;
std::cout << m * K << std::endl;

{
std::string _;
std::getline(std::cin, _);
}
}


I'm intentionally not taking into account thread contention; just interested in the best case cost.

The results are:

9.25905
80.1517
70.8926


So ~71 CPU cycles for locking a mutex

• Have you disabled CPU throttling? Otherwise the time to ramp up to full CPU speed can effect the results. Commented Jun 21, 2019 at 21:43
• @1201ProgramAlarm nope, I don't even know how to do that. The test runs for a 3~4 seconds though Commented Jun 21, 2019 at 21:48
• Micro-review: the fixed-with integer types are defined in <cstdint>, so that should be included. Commented Jun 24, 2019 at 11:18

Obviously this code tests only the overhead of an uncontested lock - we'll need a different test if we want to measure the cost of try_lock() too.

std::int64_t requires an include of <cstdint>. Is there a good reason not to use std::int64_fast_t, to be more portable?

Conversely, we include <thread> but use none of its definitions.

I is a terrible name for a variable - it's not descriptive, it's visually similar to l and 1, and it's all-caps like a macro.

I don't see why we need the Benchmark struct - why not a simple function, taking fn as argument?

I'd be wary about computing res and throwing it away - an optimiser may be able to eliminate most of the calculation.

The return value is obscure - better to return a duration, which carries information about the units used.

This is probably incorrect:

    const double K = 3.7; //GHz of my CPU


CPUs that run in the GHz range often run at varying speeds according to load - it's worth confirming that

It would be better to subtract the two times first, so we then only need to divide the result once by I. Or perhaps it would be clearer if the benchmark() function did the division, so that its result is always appropriately scaled, and I doesn't need to be global?

There's no need to flush std::cout at the end of each line. Printing raw numbers without explanation isn't very user-friendly.

The std::getline() call is pointless and irritating. Just return when done.

# Improved code

#include <chrono>
#include <cstdint>
#include <iostream>
#include <mutex>
#include <string>

typedef std::int64_t i64;

i64 twice(i64 x)
{
return 2 * x;
}

i64 twiceM(i64 x)
{
static std::mutex m;
auto _ = std::lock_guard<std::mutex>{m};
return 2 * x;
}

auto benchmark(i64 (*function)(i64),
std::uint_fast64_t iteration_count = 100'000'000)
{

i64 res = 0;
for (auto i = iteration_count;  i-->0;  ) {
res += function(i);
}
volatile auto _ = res;
return (steady_clock::now() - startTime) / iteration_count;
}

int main()
{
// prime caches and allow cpufreq to ramp up if enabled
auto _ = benchmark(twice, 100'000);

auto time_no_mutex = benchmark(twice); // without mutex
auto time_with_mutex = benchmark(twiceM); // with mutex
auto time_diff = time_with_mutex - time_no_mutex;

std::cout << "Lock overhead is "
<< time_diff
<< " per call\n";
}