Почему при инициализации локальных статических объектов используются скрытые защитные флаги?

Локальные статические объекты в 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 в некоторых системах), так и к проблемам безопасности (не позволяющим использовать технологии защиты исполняемого пространства).

Другие вопросы по тегам