3

我试图确切地了解谷歌DoNotOptimize()应该如何工作。

为了完整起见,这里是它的定义(对于clang和非常量数据):

template <class Tp>
inline BENCHMARK_ALWAYS_INLINE void DoNotOptimize(Tp& value) {
  asm volatile("" : "+r,m"(value) : : "memory");
}

据我了解,我们可以在这样的代码中使用它:

start_time = time();
bench_output = run_bench(bench_inputs);
result = time() - start_time;

为了确保基准保持在关键部分:

start_time = time();
DoNotOptimize(bench_inputs);
bench_output = run_bench(bench_inputs);
DoNotOptimise(bench_output);
result = time() - start_time;

具体来说,我不明白的是为什么这保证(是吗?)run_bench()没有移到上面start_time = time()

(有人在此评论中确切地问了这个问题,但是我不明白答案)。

据我了解,上面DoNotOptimze()做了几件事:

  • 它强制value堆栈,因为它是通过 C++ 引用传递的。你不能有一个指向寄存器的指针,所以它必须在内存中。
  • 因为value现在在堆栈上,所以随后破坏内存(如在 asm 约束中所做的那样)将强制编译器假定value调用DoNotOptimize(value).
  • (我不清楚+r,m约束是否相关。据我所知,这表明指针本身可能存储在寄存器或内存中,但指针值本身可能被读取和/或写入。)

这就是我变得模糊的地方。

如果start_time还分配了堆栈,则内存破坏DoNotOptimize()将意味着编译器必须假定DoNotOptimize()可能读取start_time. 因此语句的顺序只能是:

start_time = time(); // on the stack
DoNotOptimize(bench_inputs); // reads start_time, writes bench_inputs
bench_output = run_bench(bench_inputs)

但是如果start_time不存储在内存中,而是存储在寄存器中,那么破坏内存不会破坏start_time,对吧?在这种情况下,所需的start_time = time()and顺序DoNotOptimize(bench_inputs)会丢失,编译器可以自由地执行以下操作:

DoNotOptimize(bench_inputs); // only writes bench_inputs
bench_output = run_bench(bench_inputs)
start_time = time(); // in a register

显然我误解了一些东西。谁能帮忙解释一下?谢谢 :)

我想知道这是否是因为重新排序优化发生在寄存器分配之前,因此所有东西都被认为是当时堆栈分配的。但如果是这样的话,那DoNotOptimize()将是多余的,ClobberMemory()就足够了。

4

1 回答 1

4

摘要:DoNotOptimize按顺序订购。time()"memory"破坏者,好像它是对可以修改任何全局状态的不透明函数的另一个函数调用。

DoNotOptimize是订购的。正如 Chandler Carruth 在您链接的问答中解释的那样,通过计算对输入的数据依赖性以及计算对输出的数据依赖性来计算输入的输出。clobber 与这"memory"部分无关。


"memory"clobber 就像一个非内联函数调用

DoNotOptimizeasm语句包含一个"memory"clobber。就优化器而言,这相当于一个不透明的函数调用:必须假定它读取和写入每个全局可访问对象1。(即使是这个编译单元可能不知道的。)

由于time()它本身在任何标头中都没有内联定义,因此它无法DoNotOptimize在编译时重新排序 with ,原因与编译器无法重新排序调用foo()以及bar()无法查看这些函数的定义时相同。同样的原因,编译器不需要任何特殊的逻辑来阻止它们重新排序puts("hi"); puts("mom");

time()(可以内联且仅包含asm语句的假设必须用于asm volatile确保重复调用不只是使用第一个的输出。 asm volatile语句不能相互重新排序或访问volatile变量,所以这也可以,出于不同的原因。)

脚注 1:全局可达 = 任何假设的全局变量可能指向的任何对象。即除了这个函数中的局部变量,或者新分配的内存之外的任何东西new,如果逃逸分析可以证明这个函数之外的任何东西都不能有指向它们的指针。


asm声明的工作原理

我认为您严重误解了 asm 的工作原理。 "+r,m"告诉编译器在寄存器(或内存,如果需要)中实现值,然后使用(空)asm 模板末尾的值作为该 C++ 对象的新值。

所以它迫使编译器在某处实际实现(产生)值,这意味着它必须被计算。这意味着必须忘记它先前对值的了解(例如,它是编译时间常数 5,或非负数,或任何东西),因为"+"修饰符声明了一个读/写操作数。

输入的重点DoNotOptimize是击败会让基准优化消失的持续传播。

并在输出上确保最终结果实际上在寄存器(或内存)中实现,而不是优化导致未使用结果的所有计算。(这是asm volatile相关的地方;击败恒定传播仍然适用于非易失性asm。)

因此,您要进行基准测试的计算必须发生在两个DoNotOptimize()语句之间,并且这两个语句不能单独重新排序time()

编译器必须假设 asm 语句修改val ^= random它所知道的值,同时更改任何/所有其他对象的内存中的值,除了不是操作数的私有局部变量,例如,"memory"clobber 不会停止编译器不会将本地循环计数器保存在内存中。(这里不是特殊情况下的空 asm 模板字符串;程序不包含这样的 asm 语句是偶然的,所以没有人希望它们被优化掉。)


关于参考 arg 和选取的误解"m"

"+r,m"在决定从头开始解释可能会更好之前,我只详细介绍了您尝试推理操作数和引用函数 arg 的细节。正确的原因并不复杂。但是有几件事值得特别纠正:

包含该asm语句的 C++ 函数可以内联,让按引用函数 arg 优化掉。 (它甚至被声明inline __attribute__((always_inline))为强制内联,即使在禁用优化的情况下,尽管在这种情况下引用变量不会优化掉。)

最终结果就像是直接在传递给 DoNotOptimize 的 C++ 变量上使用了 asm 语句。例如DoNotOptimize(foo)就像asm volatile("" : "+r,m"(foo) :: "memory")

如果需要,编译器总是可以选择寄存器,例如选择在asm语句之前将变量的值加载到寄存器中。(如果 C++ 语义要求更新内存中的变量值,那么在 asm 语句之后也会发出存储指令。)

例如,我们可以看到 GCC 确实选择这样做。(我想我可以用作incl %0示例,但我只是选择了一种方式来显示编译器为操作数位置选择的内容作为纯注释nop的替代方法,因此Godbolt 编译器资源管理器不会将其过滤掉。)# %0

void foo(int *p)
{
    asm volatile("nop # operand picked %0" : "+r,m" (p[4]) );
}
# GCC 11.2 -O2
foo(int*):
        movl    16(%rdi), %eax
        nop # operand picked %eax
        movl    %eax, 16(%rdi)
        ret

与 clang 选择将值保留在内存中,因此 asm 模板中的每条指令都将访问内存而不是寄存器。(如果有任何说明)。

# clang 12.0.1 -O2 -fPIE
foo(int*):                               # @foo(int*)
        nop     # operand picked 16(%rdi)
        retq

有趣的事实:"r,m"试图解决一个 clang 错过优化的错误,该错误使它总是为"rm"约束选择内存,即使该值已经在寄存器中。首先将其溢出,即使它必须为表达式的值创造一个临时位置作为输入。

于 2021-09-22T16:40:33.480 回答