Почему при инициализации локальных статических объектов используются скрытые защитные флаги?
Локальные статические объекты в C++ инициализируются один раз, в первый раз, когда они необходимы (что актуально, если инициализация имеет побочный эффект):
void once() {
static bool b = [] {
std::cout << "hello" << std::endl; return true;
} ();
}
once
напечатает "привет" при первом вызове, но не при повторном вызове.
Я поместил несколько вариантов этого шаблона в Compiler Explorer и заметил, что все реализации с громкими именами (GCC, Clang, ICC, VS) по сути делают одно и то же: скрытая переменная guard variable for once()::b
создается и проверяется, нужно ли инициализировать первичную переменную "на этот раз"; если он это делает, он инициализируется, а затем устанавливается защита, и в следующий раз он не выпрыгнет на код инициализации. например (минимизируется путем замены лямбда с помощью вызова extern bool init_b();
):
once():
movzx eax, BYTE PTR guard variable for once()::b[rip]
test al, al
je .L16
ret
.L16:
push rbx
mov edi, OFFSET FLAT:guard variable for once()::b
call __cxa_guard_acquire
test eax, eax
jne .L17
pop rbx
ret
.L17:
call init_b()
pop rbx
mov edi, OFFSET FLAT:guard variable for once()::b
jmp __cxa_guard_release
mov rbx, rax
mov edi, OFFSET FLAT:guard variable for once()::b
call __cxa_guard_abort
mov rdi, rbx
call _Unwind_Resume
... из GCC 6.3 с -O3.
Это не является необоснованным, и я знаю, что на практике условные переходы в любом случае близки к свободным, когда условие является постоянным. Тем не менее, мое интуитивное чувство все равно было бы реализовать это, без условного перехода к коду инициализации, который в качестве своего последнего действия перезаписывает исходный переход с nop
инструкции. Не обязательно вариант на каждой платформе, но семейство x86 кажется довольно либеральным в отношении того, что вы можете читать или писать и где.
Что такого плохого в этой, по-видимому, простой идее, что никакой основной компилятор не использует ее? (Или мне просто нужно стараться с моими примерами?)
2 ответа
Такая "оптимизация" небезопасна в многопоточной среде и может быть небезопасной даже в одной.
Написание "nops" может занять несколько инструкций.
Размер инструкции jmp может быть недоступен до тех пор, пока не будет оптимизирован окончательный код (требуется ли смещение 8, 16 или 32 бита?)
Кэширование инструкций в ЦП не учитывает изменения в байтах кода, если только одна из поднаборов инструкций не выполнена, чтобы вызвать сброс кешей.
И все, что предполагает, что код может быть записан через сегмент данных.
В большинстве современных операционных систем изменение кода, загруженного с программой, вызывает проблемы. Это может привести как к проблемам с производительностью (неизмененный код может обмениваться страницами между многими экземплярами DLL в некоторых системах), так и к проблемам безопасности (не позволяющим использовать технологии защиты исполняемого пространства).