Оптимизационный барьер для микрообъектов в 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
.