Доступ к статической функциональной переменной медленнее, чем доступ к глобальной переменной?

Статические локальные переменные инициализируются при первом вызове функции:

Переменные, объявленные в области блока со спецификатором static, имеют статический период хранения, но инициализируются, когда первый элемент управления проходит через их объявление (если их инициализация не является zero- или константной инициализацией, которая может быть выполнена до того, как блок будет введен первым). При всех последующих вызовах декларация пропущена.

Кроме того, в С++ 11 есть еще больше проверок:

Если несколько потоков пытаются инициализировать одну и ту же статическую локальную переменную одновременно, инициализация происходит ровно один раз (аналогичное поведение можно получить для произвольных функций с помощью std :: call_once). Примечание. В обычных реализациях этой функции используются варианты дважды проверенного шаблона блокировки, что уменьшает накладные расходы во время выполнения для уже инициализированной локальной статистики для одного неатомного логического сравнения. (поскольку С++ 11)

В то же время глобальные переменные, похоже, инициализируются при запуске программы (хотя технически только выделение/освобождение упоминается в cppreference):

статическая продолжительность хранения. Хранилище для объекта выделяется, когда программа начинается и освобождается, когда программа заканчивается. Существует только один экземпляр объекта. Все объекты, объявленные в области пространства имен (включая глобальное пространство имен), имеют такую продолжительность хранения, а также те, которые объявлены со статическими или внешними.

Так, например, следующий пример:

struct A {
    // complex type...
};
const A& f()
{
    static A local{};
    return local;
}

A global{};
const A& g()
{
    return global;
}

Правильно ли я предполагаю, что f() должен проверить, была ли его переменная инициализирована каждый раз, когда она вызывается, и, следовательно, f() будет медленнее, чем g()?

Ответ 1

Конечно, вы концептуально верны, но современные архитектуры могут справиться с этим.

Современный компилятор и архитектура могли бы организовать конвейер таким образом, чтобы предполагаемая ветвь была инициализирована. Следовательно, накладные расходы на инициализацию будут иметь дополнительный свалку для трубопровода, что и все.

Если у вас есть какие-либо сомнения, проверьте сборку.

Ответ 2

Да, это почти наверняка немного медленнее. В большинстве случаев это, однако, не имеет значения, и стоимость будет излишней из-за "логики и стиля".

Технически, локально-статическая переменная-функция такая же, как глобальная переменная. Только то, что его имя известно не во всем мире (это хорошо), и его инициализация гарантируется не только в точно определенное время, но и только один раз, и потокобезопасность.

Это означает, что локально-статическая переменная-функция должна знать, произошла ли инициализация, и, следовательно, требуется, по крайней мере, один дополнительный доступ к памяти и один условный переход, который глобальный (в принципе) не нужен. Реализация может сделать что-то похожее для глобалов, но она не нуждается (и обычно этого не делает).

Скорее всего, во всех случаях скачок предсказан правильно, но два. Первые два вызова, скорее всего, будут предсказаны неправильно (обычно прыжки по умолчанию предполагаются принятыми, а не нет, ошибочное предположение при первом вызове, а последующие скачки предполагаются теми же, что и предыдущие, опять неправильными). После этого вы должны быть хорошими, чтобы приблизиться к 100% правильному прогнозу.
Но даже правильно предсказанный прыжок не является бесплатным (CPU все еще может запускать только определенное количество команд за каждый цикл, даже если они берут нулевое время для завершения), но это не так много. Если латентность памяти, которая может составлять пару сотен циклов в худшем случае, может быть спрятана, стоимость почти исчезает при конвейерной обработке. Кроме того, каждый доступ извлекает дополнительную кешлинку, которая в противном случае не нужна (предположительно, флаг инициализации не сохраняется в той же строке кэша, что и данные). Таким образом, у вас немного хуже производительность L1 (L2 должен быть достаточно большим, чтобы вы могли сказать "да, ну и что").

Также необходимо выполнить что-то однократно и поточно, что глобальный (в принципе) не должен делать, по крайней мере, не так, как вы видите. Реализация может сделать что-то другое, но большинство просто инициализируют глобальные переменные до того, как будет введена main, и не редко большинство из них выполняется с помощью memset или неявно, потому что переменная хранится в сегменте, который все равно обнуляется.
Ваша статическая переменная должна быть инициализирована при выполнении кода инициализации, и это должно происходить в потоковом режиме. В зависимости от того, насколько сложна ваша реализация, это может быть довольно дорогостоящим. Я решил отказаться от функции безопасности потока и всегда компилировать с помощью fno-threadsafe-statics (даже если это не соответствует стандарту) после обнаружения того, что GCC (который в остальном является компилятором OK allround) фактически блокирует мьютекс для каждого статического инициализация.

Ответ 3

Из https://en.cppreference.com/w/cpp/language/initialization

Отложенная динамическая инициализация
Определяется реализацией, происходит ли динамическая инициализация - перед первым оператором основной функции (для статики) или начальной функцией потока (для локаторов нитей) или после отсрочки.

Если инициализация не-встроенной переменной (поскольку С++ 17) откладывается после первого оператора функции main/thread, это происходит до первого использования odr любой переменной со статической/длительностью хранения потоков, определенной в той же единицы перевода, что и переменная, подлежащая инициализации.

Поэтому подобная проверка может потребоваться также для глобальных переменных.

поэтому f() не требуется "медленнее", чем g().

Ответ 4

g() не является потокобезопасным и восприимчив ко всем типам проблем с упорядочением. Безопасность будет стоить дорого. Существует несколько способов оплаты:

f(), Meyer Singleton, платит цену за каждый доступ. При частом доступе или доступе во время чувствительного к производительности раздела вашего кода, имеет смысл избежать f(). Предположительно, ваш процессор имеет конечное число схем, которые он может использовать для предсказания ветвления, и вы все равно вынуждены читать атомную переменную до ветки. Это высокая цена, чтобы постоянно платить за то, что инициализация произошла только один раз.

h(), описанная ниже, очень похожа на g() с дополнительной косвенностью, но предполагает, что h_init() ровно один раз в начале выполнения. Предпочтительно, вы должны определить подпрограмму, которая вызывается как строка main(); который вызывает каждую функцию, такую как h_init(), с абсолютным упорядочением. Надеемся, что эти объекты не нужно разрушать.

В качестве альтернативы, если вы используете GCC, вы можете аннотировать h_init() с __attribute__((constructor)). Однако я предпочитаю объяснение статической подпрограммы init.

A * h_global = nullptr;
void h_init() { h_global = new A { }; }
A const& h() { return *h_global; }

h2() точно так же, как h(), минус дополнительное направление:

alignas(alignof(A)) char h2_global [sizeof(A)] = { };
void h2_init() { new (std::begin(h2_global)) A { }; }
A const& h2() { return * reinterpret_cast <A const *> (std::cbegin(h2_global)); }