在MSVC中,“Escape”和“Clobber”的等效术语是什么?

19
Chandler Carruth的CppCon 2015演讲中,他介绍了两个神奇的函数,可以在不产生额外性能损失的情况下击败优化器。
以下是这些函数的参考代码(使用GNU风格的内联汇编):
void escape(void* p)
{
    asm volatile("" : : "g"(p) : "memory");
}

void clobber()
{
    asm volatile("" : : : "memory");
}

它适用于支持GNU风格内联汇编的任何编译器(GCC、Clang、英特尔编译器,可能还有其他)。然而,他提到它在MSVC中不起作用。

检查Google Benchmark的实现,似乎他们使用reinterpret cast转换为volatile const char&并将其传递给在非gcc / clang编译器上隐藏在不同翻译单元中的函数。

template <class Tp>
inline BENCHMARK_ALWAYS_INLINE void DoNotOptimize(Tp const& value) {
    internal::UseCharPointer(&reinterpret_cast<char const volatile&>(value));
}

// some other translation unit
void UseCharPointer(char const volatile*) {}

然而,我有两个顾虑:

  1. 我可能会产生一个函数调用
  2. 有可能“聪明”的链接时优化器会认识到UseCharPointer很小,将其内联,然后丢弃我想要保留的所有代码,或者“聪明”的优化器可能被允许执行其他我不想要的重新排序。

在MSVC中是否有任何低级等效于GNU样式汇编函数?还是这是MSVC上最好的选择?


相关:在基准测试时防止编译器优化 询问原始的 GNU C 内联 asm 版本实际上是做什么的。 - Peter Cordes
2个回答

8

虽然我不知道MSVC是否有相应的汇编技巧,但Facebook在他们的Folly基准库中使用以下方法:

/**
 * Call doNotOptimizeAway(var) against variables that you use for
 * benchmarking but otherwise are useless. The compiler tends to do a
 * good job at eliminating unused variables, and this function fools
 * it into thinking var is in fact needed.
 */
#ifdef _MSC_VER

#pragma optimize("", off)

template <class T>
void doNotOptimizeAway(T&& datum) {
  datum = datum;
}

#pragma optimize("", on)

#elif defined(__clang__)

template <class T>
__attribute__((__optnone__)) void doNotOptimizeAway(T&& /* datum */) {}

#else

template <class T>
void doNotOptimizeAway(T&& datum) {
  asm volatile("" : "+r" (datum));
}

#endif

这里是GitHub上的代码链接。


2
我正在寻找一种方法在我自己的小型基准库中实现完全相同的操作。令人沮丧的是,MSVC针对x64的编译禁用了__asm技巧,而针对x86的编译则允许使用它!
经过几次尝试后,我重复了谷歌的解决方案,而不会产生额外的调用!美妙之处在于这个解决方案适用于MSVC(/Ox)和GCC(-O3)。
template <class T>
inline auto doNotOptimizeAway(T const& datum) {
    return reinterpret_cast<char const volatile&>(datum);
}

在调用的地方,我只是不使用返回的 volatile!最初的回答。
int main()
{
    int a{10};
    doNotOptimizeAway(a);
    return 0;
}

生成的汇编代码 (编译器探索器)

a$ = 8
main    PROC
        mov     DWORD PTR a$[rsp], 10
        movzx   eax, BYTE PTR a$[rsp]
        xor     eax, eax
        ret     0
main    ENDP

1
MSVC风格的__asm{}也不是特别有用;它没有类似于GNU C内联汇编的输入/输出约束,让您告诉编译器一个空的汇编块实际上读取或读写C变量,以强制编译器将该变量实现为寄存器。 - Peter Cordes
1
一个volatile load会强制编译器将其存在内存中,而不仅仅是像mov edx, 10这样。但至少编译器仍然知道该变量只被读取,所以return a;仍然可以在store/reload之间编译为mov eax, 10。但仍然有可能会在使用它的地方引入2个指令,除非该变量已经存在于内存中,那么你只需要进行加载。因此,这可能会在不太干扰编译器生成代码的情况下有用,但绝对不是免费的。 - Peter Cordes
1
对于窄类型(sizeof(T)<=sizeof(void*)),在大多数情况下,将数据赋值给 volatile T dummy = datum 可能更便宜:只有存储而没有重新加载。只有对于编译器已经溢出的数据,volatile 加载才更好。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接