Google的"DoNotOptimize()"函数如何强制执行语句排序



我正试图准确理解谷歌的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()DoNotOptimize(bench_inputs)的所需顺序丢失,编译器可以自由执行:

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

显然我误解了什么。有人能帮忙解释一下吗?谢谢:)

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

摘要:DoNotOptimize是由"memory"clobber按照wrt.time()排序的,就好像它是对一个可以修改任何全局状态的不透明函数的另一个函数调用一样。

DoNotOptimize是通过计算对输入的数据依赖性对来自输入的输出的计算排序的,并且输出对计算排序,如Chandler Carruth在Q&A你链接了。"memory"撞击器与本部分无关。


"memory"clobber类似于非内联函数调用

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

由于time()本身在任何标头中都没有内联定义,它在编译时不能用DoNotOptimize重新排序,原因与编译器在看不到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++函数可以内联,让by-reference函数arg优化掉(甚至声明inline __attribute__((always_inline))强制内联,即使禁用了优化,尽管在这种情况下引用变量不会优化掉。)

最终结果就像asm语句直接用于传递给DoNotOptimize的C++变量一样。例如DoNotOptimize(foo)类似于asm volatile("" : "+r,m"(foo) :: "memory")

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

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

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 missing优化错误,该错误使它总是为"rm"约束选择内存,即使寄存器中的值已经。首先溢出它,即使它必须为表达式的值创建一个临时位置作为输入。

相关内容

  • 没有找到相关文章

最新更新