【问题标题】:Optimization barrier for microbenchmarks in MSVC: tell the optimizer you clobber memory?MSVC 中微基准的优化障碍:告诉优化器你破坏内存?
【发布时间】:2016-11-10 02:08:42
【问题描述】:

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

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。

【问题讨论】:

  • 看起来like _ReadWriteBarrier 可能是clobber 的答案。我不知道escape。也许_ReadWriteBarrier 加上将指针传递给一些外部定义的函数。
  • 哦,我忘了提到它们的另一个特性:它们不会生成任何代码。优化器完成后,它们的任何效果都会消失。在运行时之前没有任何问题。它们纯粹是编译时的。
  • 就像@user786653 所说,_ReadWriteBarrier(或者可能只是_ReadBarrier/_WriteBarrier,如果这就是所有需要的话)在MSVC 中的效果与clobber 相同。对于escape,我分析汇编输出的经验是,如果你只标记变量volatile,MSVC 会做正确的事情。当然,这会产生一些运行时开销,因为生成的代码会始终在内存中保持变量的更新。这不是一个完美的解决方案,但我没有找到更好的解决方案。
  • @R.MartinhoFernandes ...只要您使用此机制进行基准测试,但不依赖它来防止线程代码的读/写迁移,我quiesce
  • 你研究过 std::atomic_signal_fence 和 atomic_thread_fence 吗?

标签: c++ visual-c++ optimization benchmarking


【解决方案1】:

鉴于your approximation of 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,该策略就会失败。你想到了什么?

答案:我们可以利用数论中难以证明的属性,例如,Fermat's Last Theorem

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

【讨论】:

  • @Peter 请注意,isClobberingEnabled 只被调用一次(它在命名空间范围内使用)。但是,也许您的观点仍然适用于nextLocationToClobber
  • @R.MartinhoFernandes:刚刚注意到并删除了我的评论。重新发布正确的版本:callnextLocationToClobber 意味着编译器不能将包含它的函数视为叶函数。希望调用破坏寄存器的溢出将仅限于调用发生的分支,并且对未采取的方面没有太大影响,但它仍然是非零影响。至少它将编译为全球的测试和分支。因此,与 gcc 不同,生成的代码量非零。 :/ 不过,可预测的分支很便宜。
  • 这可能是你可以用 MSVC 做的最好的事情,但如果它没有任何可以提供帮助的内置/内在函数,那将是令人失望的。
  • 您如何确保isClobberingEnabled“以不可推论的方式”返回 false?当然,将定义放在另一个翻译单元中是微不足道的,但是一旦启用 LTCG,该策略就会失败。你想到了什么?
【解决方案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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-04-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多