MSVC 中微基准测试的优化屏障:告诉优化器您的内存

Optimization barrier for microbenchmarks in MSVC: tell the optimizer you clobber memory?

本文关键字:优化 内存 基准测试 MSVC      更新时间:2023-10-16

Chandler Carruth在他的CppCon2015演讲中介绍了两个函数,可用于对优化器进行一些细粒度的抑制。它们对于编写微基准测试很有用,优化器不会简单地陷入毫无意义。

void clobber() {
  asm volatile("" : : : "memory");
}
void escape(void* p) {
  asm volatile("" : : "g"(p) : "memory");
}    

它们使用内联程序集语句来更改优化程序的假设。

clobber 中的程序集语句声明其中的程序集代码可以在内存中的任何位置读取和写入。实际的汇编代码为空,但优化器不会查看它,因为它asm volatile。当我们告诉它代码可能会在内存中的任何地方读写时,它就会相信它。这有效地防止优化程序在调用clobber之前重新排序或丢弃内存写入,并强制在调用clobber †后读取内存。

escape 中的那个 ,另外使指针p对程序集块可见。同样,由于优化器不会查看实际的内联汇编代码,因此代码可以为空,并且优化器仍将假定块使用指针p所指向的地址。这有效地强制任何p指向内存中,而不是不在寄存器中,因为程序集块可能会从该地址执行读取。

(这很重要,因为 clobber 函数不会强制读取或写入编译器决定放入寄存器中的任何内容,因为 clobber 中的汇编语句没有声明任何特定内容必须对程序集可见。

所有这些都是在没有这些"障碍"直接生成任何额外代码的情况下发生的。 它们纯粹是编译时工件。

不过,它们使用GCC和Clang支持的语言扩展。有没有办法在使用 MSVC 时具有类似的行为?


† 要理解为什么优化器必须这样思考,想象一下,如果程序集块是一个循环,将 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;
    }
}

更新

问题:您如何确保isClobberingEnabled"以不可推断的方式"返回false?当然,将定义放在另一个翻译单元中是微不足道的,但是一旦启用LTCG,该策略就失败了。你有什么想法?

:我们可以利用数论中一个难以证明的性质,例如费马大定理:

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's 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;
}

我用以下内容代替了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.