`static_cast<volatile void>`对优化器有什么意义?

5

在人们尝试在不同的库中进行严格基准测试时,有时会看到这样的代码:

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是什么意思?从编译器和标准的角度来看它代表了什么含义?
在标准文件(N4296)的[dcl.type.cv]部分,它表示:
7 [注释:volatile是一种提示实现避免过于激进的优化涉及的对象,因为该对象的值可能会被不可检测的实现方式更改。此外,对于某些实现,volatile可能表示需要使用特殊的硬件指令来访问该对象。有关详细的语义,请参见1.9。总的来说,volatile的语义旨在与C中的语义相同。——结束注释]
在第1.9节中,它提供了执行模型的很多指导,但就volatile而言,它是关于"访问一个volatile对象"的。如果我正确理解代码,那么将一个声明为volatile void的语句执行意味着什么并不清楚,也不清楚是否产生任何优化屏障。

这似乎只是一份写得很差的测试。测试应该避免使用volatile或其他技巧来混淆编译器,以防止与实际代码相比潜在地改变测试结果。 - user7860670
volatile 在微基准测试中的作用取决于编译器。如果将其转换为 volatile void 可以让特定编译器计算一个值,但不会花费指令将其实际存储到内存中,那么这可能是您想要在重复循环中测试所需函数吞吐量的方式。 - Peter Cordes
将一个迭代的输出馈入下一个迭代的输入中,可以测试延迟。但是,计算其对其他周围代码的影响是另一回事。(例如,如果do_something()涉及FP除法或SQRT,则它的吞吐量可能非常低,但也会对不需要除法单元的周围代码产生较小的影响 - Peter Cordes
1个回答

1

static_cast<volatile void> (foo())不能作为一种方法来要求编译器在启用优化的情况下实际计算foo(),无论是在gcc / clang / MSVC / ICC中都不行。

#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()等发出汇编代码,但向下滚动查看其foo()的平凡定义。)

(此示例和下一个示例的源代码和汇编输出请参见Matt Godbolt的编译器浏览器


也许有一些编译器可以在static_cast<volatile void>()中执行某些操作,在这种情况下,这可能是编写重复循环的轻量级方法,它不会花费指令将结果存储到内存中,只计算它。(这有时可能是微基准测试中所需的)。
使用tmp += foo()(或tmp |=)累积结果,并从main()返回它或使用printf打印它,也可以代替存储到volatile变量中。或者使用各种编译器特定的内容,例如使用空的内联asm语句来破坏编译器的优化能力,而不实际添加任何指令。
请查看Chandler Carruth在CppCon2015上关于使用perf调查编译器优化的演讲,他展示了GNU C的逃逸优化函数。但是他的escape()函数需要将值存储在内存中(将一个void*传递给汇编语句,并使用"memory"占位符)。我们不需要这样做,我们只需要编译器将值存储在寄存器、内存或即时常量中。(由于编译器不知道汇编语句是零条指令,所以很可能无法完全展开我们的循环。)
这段代码在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往往会这样做。因此,对于此问题,它并不比Chandler的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

使用-march=haswell的ICC18会做以下事情:

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上没有被识别为依赖项破坏。


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