Ответ 1
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, гарантированная сильно синхронизированными CPU, оказывается достаточной для создания этого шаблона получения/выпуска, поэтому он может выполнять чтение raw этого первого байта, когда он делает его двойную блокировку, а не считывание. Это возможно только потому, что GCC не использует атомарный API С++ для двойной блокировки - он использует платформенный подход.
GCC не может оптимизировать атом в общем случае. На архитектурах, которые сконструированы так, чтобы быть менее синхронизированными (например, рассчитанными на 1024 ядра), GCC не может полагаться на архетект, чтобы сделать LL/SS для него. Таким образом, GCC вынужден фактически испускать атом. Однако на обычных платформах, таких как x86 и x64, это может быть быстрее.
call_once
может иметь эффективность статики GCC, поскольку она аналогичным образом ограничивает количество операций, которые можно выполнить с помощью once_flag
, до доли функций, которые могут быть применены к атому. Компромисс заключается в том, что статика гораздо удобнее использовать, когда они применимы, но call_once
работает во многих случаях, когда статики недостаточны (например, once_flag
, принадлежащих динамически сгенерированному объекту).
Существует небольшая разница в производительности между static и call_once
на этих более высоких платформах. Многие из этих платформ, не предлагая LL/SS, будут, по крайней мере, предлагать чтение без слежения целого числа. Эти платформы могут использовать это и указатель на конкретный поток, чтобы подсчитать количество потоков для предотвращения атомистики. Этого достаточно для статического или call_once
, но зависит от того, что счетчик не перевернулся. Если у вас нет безупречного 64-битного целого числа, call_once
должен беспокоиться о опрокидывании. Реализация может или не стоит беспокоиться об этом. Если он игнорирует эту проблему, она может быть такой же быстрой, как статика. Если он обращает внимание на этот вопрос, он должен быть таким же медленным, как атомистика. Static знает во время компиляции, сколько статических переменных/блоков существует, поэтому он может доказать, что во время компиляции нет опрокидывания (или, по крайней мере, уверенно!)