什么`static_cast <volatile void =“”>`对于优化器意味着什么?

时间:2017-11-12 20:34:02

标签: c++ benchmarking void volatile microbenchmark

当人们试图在各种库中执行严格的基准测试时,我有时会看到这样的代码:

auto std_start = std::chrono::steady_clock::now();
for (int i = 0; i < 10000; ++i)
  for (int j = 0; j < 10000; ++j)
    volatile const auto __attribute__((unused)) c = std_set.count(i + j);
auto std_stop = std::chrono::steady_clock::now();

这里使用volatile来阻止优化器注意到被测代码的结果被丢弃,然后丢弃整个计算。

如果测试中的代码没有返回值,请说它是void do_something(int),那么有时我会看到这样的代码:

auto std_start = std::chrono::steady_clock::now();
for (int i = 0; i < 10000; ++i)
  for (int j = 0; j < 10000; ++j)
    static_cast<volatile void> (do_something(i + j));
auto std_stop = std::chrono::steady_clock::now();

这是volatile的正确用法吗?什么是volatile void?从编译器和标准的角度来看它意味着什么?

[dcl.type.cv]的标准(N4296)中,它说:

  

7 [注意:volatile是对实现的暗示,以避免涉及对象的激进优化   因为对象的值可能会被实现无法检测的方式更改。此外,   对于某些实现,volatile可能表示需要访问特殊硬件指令   物体。有关详细语义,请参见1.9。通常,volatile的语义是有意的   在C ++中与在C中相同。 - 尾注]

在1.9节中,它指定了很多关于执行模型的指导,但就volatile而言,它涉及&#34;访问volatile对象&#34;。我不清楚执行已经转换为volatile void的语句是什么意思,假设我正确理解了代码,以及确切地说如果产生了任何优化障碍。< / p>

1 个答案:

答案 0 :(得分:1)

static_cast<volatile void> (foo())不能作为一种方法,要求编译器在任何gcc / clang / MSVC / ICC中实际计算foo(),并启用优化。

#include <bitset>

void foo() {
    for (int i = 0; i < 10000; ++i)
      for (int j = 0; j < 10000; ++j) {
        std::bitset<64> std_set(i + j);
        //volatile const auto c = std_set.count();     // real work happens
        static_cast<volatile void> (std_set.count());  // optimizes away
      }
}

仅使用所有4个主要x86编译器编译ret 。 (MSVC针对std::bitset::count()或其他内容的独立定义发出asm,但向下滚动以查看其对foo()的简单定义。

(此信息的来源+ asm输出以及Matt Godbolt's compiler explorer上的下一个示例)

也许有些编译器会static_cast<volatile void>()执行某些操作,在这种情况下,它可能是一种轻量级的方法来编写一个不会花费指令将结果存储到内存中的重复循环,计算它。 (这有时可能是你想要的微基准测试。)

使用tmp += foo()(或tmp |=)累积结果并从main()返回结果或使用printf打印结果也很有用,而不是存储到{{ 1}}变量。或者各种特定于编译器的事情,例如使用空的内联volatile语句来破坏编译器的优化能力,而无需实际添加任何指令。

请参阅Chandler Carruth's CppCon2015 talk on using perf to investigate compiler optimizations,其中显示optimizer-escape function for GNU C。但是他的asm函数被编写为要求值在内存中(将asm escape()传递给它,并使用void* clobber)。我们不需要它,我们只需要编译器将值保存在寄存器或内存中,甚至是直接常量。 (它不太可能完全展开我们的循环,因为它不知道asm语句是零指令。)

此代码在gcc 上编译为只是 popcnt而没有任何额外的商店。

"memory"

// just force the value to be in memory, register, or even immediate
// instead of empty inline asm, use the operand in a comment so we can see what the compiler chose.  Absolutely no effect on optimization.
static void escape_integer(int a) {
  asm volatile("# value = %0" : : "g"(a));
}

// simplified with just one inner loop
void test1() {
    for (int i = 0; i < 10000; ++i) {
        std::bitset<64> std_set(i);
        int count = std_set.count();
        escape_integer(count);
    }
}

Clang选择将值放在内存中以满足#gcc8.0 20171110 nightly -O3 -march=nehalem (for popcnt instruction): test1(): # value = 0 # it peels the first iteration with an immediate 0 for the inline asm. mov eax, 1 .L4: popcnt rdx, rax # value = edx # the inline-asm comment has the %0 filled in to show where gcc put the value add rax, 1 cmp rax, 10000 jne .L4 ret 约束,这非常愚蠢。但是当你给它一个包含内存作为选项的内联asm约束时,clang确实倾向于这样做。所以它并不比钱德勒的"g"功能更好。

escape

ICC18与# clang5.0 -O3 -march=nehalem test1(): xor eax, eax #DEBUG_VALUE: i <- 0 .LBB1_1: # =>This Inner Loop Header: Depth=1 popcnt rcx, rax mov dword ptr [rsp - 4], ecx # value = -4(%rsp) # inline asm gets a value in memory inc rax cmp rax, 10000 jne .LBB1_1 ret 执行此操作:

-march=haswell

奇怪的是,ICC使用test1(): xor eax, eax #30.16 ..B2.2: # Preds ..B2.2 ..B2.1 # optimization report # %s was not vectorized: ASM code cannot be vectorized xor rdx, rdx # breaks popcnt's false dep on the destination popcnt rdx, rax #475.16 inc rax #30.34 # value = edx cmp rax, 10000 #30.25 jl ..B2.2 # Prob 99% #30.25 ret #35.1 代替xor rdx,rdx。这浪费了REX前缀,并且在Silvermont / KNL上没有被识别为依赖性破坏。