Для поточной безопасной ленивой инициализации следует предпочитать статическую переменную внутри функции, std :: call_once или явную двойную проверку блокировки? Есть ли значимые различия?
Все три можно увидеть в этом вопросе.
Дважды проверил Lock Singleton в C ++ 11
В Google появляются две версии двойной проверки блокировки в C ++ 11.
Энтони Уильямс шоу оба дважды проверяли блокировку с явным упорядочением памяти и std :: call_once. Он не упоминает статические, но эта статья могла быть написана до того, как стали доступны компиляторы C ++ 11.
Джефф Прешинг, в обширном записать, описывает несколько вариантов двойной проверки блокировки. Он упоминает об использовании статической переменной в качестве опции и даже показывает, что компиляторы будут генерировать код для двойной проверки блокировки для инициализации статической переменной. Мне не ясно, если он заключит, что один путь лучше, чем другой.
Я понимаю, что обе статьи должны быть педагогическими, и что нет никаких причин для этого. Компилятор сделает это за вас, если вы используете статическую переменную или std :: call_once.
GCC использует специфические для платформы приемы, чтобы полностью избежать ускоренных операций, используя тот факт, что он может выполнять анализ static
лучше, чем call_once или двойная проверка.
Поскольку двойная проверка использует атомарность в качестве метода предотвращения гонок, она должна каждый раз платить цену приобретения. Это не высокая цена, но это цена.
Он должен заплатить за это, потому что атомы должны оставаться атомарными во всех случаях, даже в сложных операциях, таких как сравнение-обмен. Это делает его очень трудно оптимизировать. Вообще говоря, компилятор должен оставить его в так, на всякий случай Вы используете переменную не только для двойной блокировки. У него нет простого способа доказать, что вы никогда не используете одну из более сложных операций на вашем атомарном сервере.
С другой стороны, static
является узкоспециализированным и частью языка. С самого начала он был спроектирован так, чтобы его можно было легко инициализировать. Соответственно, компилятор может использовать ярлыки, которые не были доступны для более общей версии. Компилятор на самом деле испускает следующий код для статики:
простая функция:
void foo() {
static X x;
}
переписан внутри GCC, чтобы:
void foo() {
static X x;
static guard x_is_initialized;
if ( __cxa_guard_acquire(x_is_initialized) ) {
X::X();
x_is_initialized = true;
__cxa_guard_release(x_is_initialized);
}
}
Который очень похож на замок с двойной проверкой. Тем не менее, компилятор немного обманывает здесь. Он знает, что пользователь никогда не может написать использовать cxa_guard
непосредственно. Он знает, что он используется только в особых случаях, когда компилятор решает его использовать. Таким образом, с этой дополнительной информацией, это может сэкономить некоторое время. Спецификации защиты CXA, как они распределены, все разделяют общее правило: __cxa_guard_acquire
никогда не будет изменять первый байт охранника, и __cxa_guard__release
установит его ненулевым.
Это означает, что каждый охранник должен быть монотонным, и он точно определяет, какие операции будут делать это. Соответственно, он может использовать преимущества существующих защитных чехлов на платформе хоста. На x86, например, защита LL / SS, гарантированная сильно синхронизированными процессорами, оказывается достаточной для выполнения этой схемы получения / освобождения, поэтому она может сырье читать этот первый байт, когда он выполняет двойную блокировку, а не чтение-чтение. Это возможно только потому, что GCC не использует атомарный API C ++ для двойной блокировки — он использует платформо-ориентированный подход.
GCC не может оптимизировать атомарный в общем случае. На архитектурах, которые спроектированы так, чтобы быть менее синхронизированными (например, разработанные для ядер 1024+), GCC не полагается на архитектуру для выполнения LL / SS для нее. Таким образом, GCC вынужден фактически излучать атом. Однако на распространенных платформах, таких как x86 и x64, это может быть быстрее.
call_once
может иметь эффективность статики GCC, потому что он также ограничивает количество операций, которые могут быть выполнены once_flag
на долю функций, которые могут быть применены к атомному. Компромисс состоит в том, что статики гораздо удобнее использовать, когда они применимы, но call_once
работает во многих случаях, когда статика недостаточна (например, once_flag
принадлежит динамически генерируемому объекту).
Существует небольшая разница в производительности между статическим и call_once
на этих более высоких платформах. Многие из этих платформ, хотя и не предлагают LL / SS, по крайней мере будут предлагать чтение целого числа без разрывов. Эти платформы могут использовать это и указатель потока, чтобы сделать подсчет эпох для каждого потока, чтобы избежать атомарности. Этого достаточно для статического или call_once
, но зависит от того, счетчик не переворачивается. Если у вас нет 64-разрядного целого числа без разрывов, call_once
должен беспокоиться о переворачивании Реализация может или не может беспокоиться об этом. Если он игнорирует эту проблему, это может быть так же быстро, как статика. Если он обращает внимание на эту проблему, он должен быть таким же медленным, как атомы. Static знает во время компиляции, сколько существует статических переменных / блоков, поэтому он может доказать, что во время компиляции не существует опрокидывания (или, по крайней мере, быть чертовски уверенным!)