Существует ли штраф за использование статических переменных в С++ 11

В С++ 11 это:

const std::vector<int>& f() {
    static const std::vector<int> x { 1, 2, 3 };
    return x;
}

является потокобезопасным. Однако есть ли дополнительное наказание за вызов этой функции после первого раза (т.е. Когда она инициализирована) из-за этой дополнительной поточно-безопасной гарантии? Мне интересно, будет ли функция медленнее, чем одна, используя глобальную переменную, потому что она должна получить мьютекс, чтобы проверить, будет ли он инициализироваться другим потоком каждый раз, когда он вызывается, или что-то в этом роде.

Ответ 1

"Лучшая интуиция, которая когда-либо была, - это" я должен ее измерить ". давайте узнаем:

#include <atomic>
#include <chrono>
#include <cstdint>
#include <iostream>
#include <numeric>
#include <vector>

namespace {
class timer {
    using hrc = std::chrono::high_resolution_clock;
    hrc::time_point start;

    static hrc::time_point now() {
      // Prevent memory operations from reordering across the
      // time measurement. This is likely overkill, needs more
      // research to determine the correct fencing.
      std::atomic_thread_fence(std::memory_order_seq_cst);
      auto t = hrc::now();
      std::atomic_thread_fence(std::memory_order_seq_cst);
      return t;
    }

public:
    timer() : start(now()) {}

    hrc::duration elapsed() const {
      return now() - start;
    }

    template <typename Duration>
    typename Duration::rep elapsed() const {
      return std::chrono::duration_cast<Duration>(elapsed()).count();
    }

    template <typename Rep, typename Period>
    Rep elapsed() const {
      return elapsed<std::chrono::duration<Rep,Period>>();
    }
};

const std::vector<int>& f() {
    static const auto x = std::vector<int>{ 1, 2, 3 };
    return x;
}

static const auto y = std::vector<int>{ 1, 2, 3 };
const std::vector<int>& g() {
    return y;
}

const unsigned long long n_iterations = 500000000;

template <typename F>
void test_one(const char* name, F f) {
  f(); // First call outside the timer.

  using value_type = typename std::decay<decltype(f()[0])>::type;
  std::cout << name << ": " << std::flush;

  auto t = timer{};
  auto sum = uint64_t{};
  for (auto i = n_iterations; i > 0; --i) {
    const auto& vec = f();
    sum += std::accumulate(begin(vec), end(vec), value_type{});
  }
  const auto elapsed = t.elapsed<std::chrono::milliseconds>();
  std::cout << elapsed << " ms (" << sum << ")\n";
}
} // anonymous namespace

int main() {
  test_one("local static", f);
  test_one("global static", g);
}

Запуск в Coliru, локальная версия выполняет 5е8 итераций в 4618 мс, глобальную версию - 4392 мс. Так что да, локальная версия медленнее примерно на 0,452 нс на итерацию. Хотя есть измеримая разница, она слишком мала, чтобы влиять на наблюдаемую производительность в большинстве ситуаций.


EDIT: Интересный контрапункт, переключение с clang++ на g++ изменяет порядок результатов. g++ - скомпилированные бинарные прогоны в 4418 мс (глобальные) против 4181 мс (локальные), поэтому локальные быстрее на 474 пикосекунды на итерацию. Тем не менее он подтверждает вывод о том, что разница между этими двумя методами мала.
EDIT 2: Изучив сгенерированную сборку, я решил преобразовать из указателей функций в функциональные объекты для лучшей встраивания. Сроки с косвенными вызовами с помощью указателей функций на самом деле не характерны для кода в OP. Поэтому я использовал эту программу:
#include <atomic>
#include <chrono>
#include <cstdint>
#include <iostream>
#include <numeric>
#include <vector>

namespace {
class timer {
    using hrc = std::chrono::high_resolution_clock;
    hrc::time_point start;

    static hrc::time_point now() {
      // Prevent memory operations from reordering across the
      // time measurement. This is likely overkill.
      std::atomic_thread_fence(std::memory_order_seq_cst);
      auto t = hrc::now();
      std::atomic_thread_fence(std::memory_order_seq_cst);
      return t;
    }

public:
    timer() : start(now()) {}

    hrc::duration elapsed() const {
      return now() - start;
    }

    template <typename Duration>
    typename Duration::rep elapsed() const {
      return std::chrono::duration_cast<Duration>(elapsed()).count();
    }

    template <typename Rep, typename Period>
    Rep elapsed() const {
      return elapsed<std::chrono::duration<Rep,Period>>();
    }
};

class f {
public:
    const std::vector<int>& operator()() {
        static const auto x = std::vector<int>{ 1, 2, 3 };
        return x;
    }
};

class g {
    static const std::vector<int> x;
public:
    const std::vector<int>& operator()() {
        return x;
    }
};

const std::vector<int> g::x{ 1, 2, 3 };

const unsigned long long n_iterations = 500000000;

template <typename F>
void test_one(const char* name, F f) {
  f(); // First call outside the timer.

  using value_type = typename std::decay<decltype(f()[0])>::type;
  std::cout << name << ": " << std::flush;

  auto t = timer{};
  auto sum = uint64_t{};
  for (auto i = n_iterations; i > 0; --i) {
    const auto& vec = f();
    sum += std::accumulate(begin(vec), end(vec), value_type{});
  }
  const auto elapsed = t.elapsed<std::chrono::milliseconds>();
  std::cout << elapsed << " ms (" << sum << ")\n";
}
} // anonymous namespace

int main() {
  test_one("local static", f());
  test_one("global static", g());
}

Неудивительно, что время выполнения было быстрее при g++ (3803ms local, 2323ms global) и clang (4183ms local, 3253ms глобальный). Результаты подтверждают нашу интуицию, что глобальная техника должна быть более быстрой, чем локальная, с дельтами 2.96 наносекунд (g++) и 1,86 наносекундами (clang) на итерацию.

Ответ 2

Да, будет стоить проверить, был ли объект инициализирован. Обычно это проверяет атомную логическую переменную, а не блокирует мьютекс.