Оптимизационный барьер для микрообъектов в MSVC: скажите оптимизатору, что вы clobber-память?

Чандлер Каррут представил две функции в своем

Они используют встроенные инструкции сборки для изменения допущений оптимизатора.

В инструкции сборки в clobber указано, что код сборки в нем может читать и записывать в любом месте в памяти. Фактический код сборки пуст, но оптимизатор не будет смотреть на него, потому что он asm volatile. Он считает, что когда мы говорим, что код может читать и писать повсюду в памяти. Это эффективно предотвращает переупорядочение или отбрасывание памяти оптимизатором перед вызовом clobber и заставляет память считывать после вызова clobber & dagger;.

Один в escape, дополнительно делает указатель p видимым для блока сборки. Опять же, поскольку оптимизатор не будет рассматривать фактический встроенный ассемблерный код, код может быть пустым, а оптимизатор все равно предположит, что блок использует адрес, указанный указателем p. Это эффективно заставляет любые p точки быть в памяти, а не в регистре, потому что блок сборки может выполнять чтение с этого адреса.

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

Все это происходит без какого-либо дополнительного кода, создаваемого непосредственно этими "барьерами". Это чисто артефакты времени компиляции.

Тем не менее, они используют языковые расширения, поддерживаемые в GCC и Clang. Есть ли способ иметь подобное поведение при использовании MSVC?

& крестик; Чтобы понять, почему оптимизатор должен думать таким образом, представьте, был ли блок сборки циклом, добавляющим 1 к каждому байту в памяти.

Ответы

Ответ 1

Учитывая ваше приближение escape(), вы также должны быть в порядке со следующей аппроксимацией clobber() (обратите внимание, что это проектная идея, откладывающая некоторые из решение для реализации функции nextLocationToClobber()):

// always returns false, but in an undeducible way
bool isClobberingEnabled();

// The challenge is to implement this function in a way,
// that will make even the smartest optimizer believe that
// it can deliver a valid pointer pointing anywhere in the heap,
// stack or the static memory.
volatile char* nextLocationToClobber();

const bool clobberingIsEnabled = isClobberingEnabled();
volatile char* clobberingPtr;

inline void clobber() {
    if ( clobberingIsEnabled ) {
        // This will never be executed, but the compiler
        // cannot know about it.
        clobberingPtr = nextLocationToClobber();
        *clobberingPtr = *clobberingPtr;
    }
}

UPDATE

Вопрос. Как бы вы гарантировали, что isClobberingEnabled возвращает false "неуправляемым образом"? Конечно, было бы тривиально поместить определение в другую единицу перевода, но как только вы включите LTCG, эта стратегия будет побеждена. Что вы имели в виду?

Ответ. Мы можем воспользоваться труднодостижимым свойством из теории чисел, например Fermat Last Теорема:

bool undeducible_false() {
    // It took mathematicians more than 3 centuries to prove Fermat's
    // last theorem in its most general form. Hardly that knowledge
    // has been put into compilers (or the compiler will try hard
    // enough to check all one million possible combinations below).

    // Caveat: avoid integer overflow (Fermat theorem
    //         doesn't hold for modulo arithmetic)
    std::uint32_t a = std::clock() % 100 + 1;
    std::uint32_t b = std::rand() % 100 + 1;
    std::uint32_t c = reinterpret_cast<std::uintptr_t>(&a) % 100 + 1;

    return a*a*a + b*b*b == c*c*c;
}

Ответ 2

Я использовал вместо escape следующее.

#ifdef _MSC_VER
#pragma optimize("", off)
template <typename T>
inline void escape(T* p) {
    *reinterpret_cast<char volatile*>(p) =
        *reinterpret_cast<char const volatile*>(p); // thanks, @milleniumbug
}
#pragma optimize("", on)
#endif

Это не идеально, но это достаточно близко, я думаю.

К сожалению, у меня нет способа подражать clobber.