【问题标题】:What does `static_cast<volatile void>` mean for the optimizer?`static_cast<volatile void>` 对优化器意味着什么?
【发布时间】:2018-04-25 12:07:28
【问题描述】:

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

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 是对实现的提示,以避免涉及对象的激进优化 因为对象的值可能会通过实现无法检测到的方式进行更改。此外, 对于某些实现,易失性可能表明需要特殊的硬件指令才能访问 物体。详细语义见 1.9。一般来说, volatile 的语义旨在 在 C++ 中与在 C 中相同。 — 尾注 ]

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

【问题讨论】:

  • 这实际上似乎只是一个写得很糟糕的测试。测试应避免使用volatile 或其他技巧来混淆编译器,以防止与实际代码相比可能改变测试结果。
  • volatile 在微基准测试中所做的完全取决于编译器。如果强制转换为volatile void 得到一个特定的编译器来计算一个值,但没有花费一条指令将它实际存储到内存中,那么这可能是您在重复循环中想要的,该循环正在测试 just 只是函数的吞吐量你想测试。
  • 将一次迭代的输出馈送到下一次迭代的输入可以让您测试延迟。计算它对 other 周围代码的影响是另一回事。 (例如,如果 do_something() 涉及 FP 划分或 SQRT,它的吞吐量可能很差,但 low impact on surrounding code that doesn't need the divide unit 也可能。

标签: c++ benchmarking void volatile microbenchmark


【解决方案1】:

static_cast&lt;volatile void&gt; (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&lt;volatile void&gt;() 确实做了一些事情,在这种情况下,它可能是一种更轻量级的编写重复循环的方法,它不花费指令将结果存储到内存,只计算它。 (这有时可能是您在微基准测试中想要的)。

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


参见Chandler Carruth's CppCon2015 talk on using perf to investigate compiler optimizations,他在其中显示了optimizer-escape function for GNU C。但是他的escape() 函数被编写为要求值在内存中(将asm 传递给void*,并带有"memory" clobber)。我们不需要那个,我们只需要编译器将值保存在寄存器或内存中,甚至是立即数。 (它不太可能完全展开我们的循环,因为它不知道 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" 约束,这非常愚蠢。但是当你给它一个包含内存作为选项的 inline-asm 约束时,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 的依赖破坏。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-05-10
    • 1970-01-01
    • 2011-12-28
    • 2020-10-12
    • 2011-06-11
    • 2010-10-02
    • 2013-01-16
    相关资源
    最近更新 更多