Доступ к статической переменной функции медленнее, чем доступ к глобальной переменной? - PullRequest
0 голосов
/ 06 сентября 2018

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

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

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

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

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

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

Итак, приведем следующий пример:

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

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

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

Ответы [ 4 ]

0 голосов
/ 12 сентября 2018

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

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

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

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

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)); }
0 голосов
/ 06 сентября 2018

С https://en.cppreference.com/w/cpp/language/initialization

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

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

Так что аналогичная проверка может должна быть выполнена и для глобальных переменных.

поэтому f() не нужно «медленнее» , чем g().

0 голосов
/ 06 сентября 2018

Да, это почти наверняка немного медленнее. Однако в большинстве случаев это не будет иметь значения, и стоимость будет перевешиваться преимуществом «логики и стиля».

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

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

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

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

0 голосов
/ 06 сентября 2018

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

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

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

...