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

What does `static_cast<volatile void>` mean for the optimizer?

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

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是为了防止优化器注意到被测代码的结果被丢弃,进而丢弃整个计算。

当被测代码没有 return 值时,说它是 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 [ Note: volatile is a hint to the implementation to avoid aggressive optimization involving the object because the value of the object might be changed by means undetectable by an implementation. Furthermore, for some implementations, volatile might indicate that special hardware instructions are required to access the object. See 1.9 for detailed semantics. In general, the semantics of volatile are intended to be the same in C ++ as they are in C. — end note ]

在1.9节中,它指定了很多关于执行模型的指导,但就volatile而言,它是关于"accessing a volatile object"的。我不清楚 执行已转换为 volatile void 的语句 意味着什么,假设我正确理解代码,以及如果产生任何优化障碍究竟会怎样。

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().

的琐碎定义

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


也许有些编译器 static_cast<volatile void>() 确实做了一些事情,在这种情况下,它可能是一种更轻量级的方式来编写一个不花费指令将结果存储到内存中的重复循环,只计算它。 (这有时可能是您在微基准测试中想要的)。

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


参见 Chandler Carruth's CppCon2015 talk on using perf to investigate compiler optimizations, where he shows an optimizer-escape function for GNU C。但是他的 escape() 函数被编写为要求值在内存中(将 asm a void* 传递给它,并带有 "memory" 破坏)。我们不需要那个,我们只需要编译器将值保存在寄存器或内存中,甚至是一个立即常量。 (它不太可能完全展开我们的循环,因为它不知道 asm 语句是零指令。)


此代码在 gcc.

上编译为 只是 popcnt,没有任何额外存储
// 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);
    }
}

#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

Clang 选择将值放在内存中以满足 "g" 约束,这很愚蠢。但是当你给它一个包含内存作为选项的内联汇编约束时,clang 确实倾向于这样做。所以它并不比钱德勒的 escape 函数好。

# 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

ICC18 和 -march=haswell 这样做:

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

真奇怪,ICC 使用 xor rdx,rdx 而不是 xor eax,eax。这浪费了一个 REX 前缀,并且不被认为是对 Silvermont/KNL.

的依赖性破坏