【问题标题】:C++ Statement ReorderingC++ 语句重新排序
【发布时间】:2020-02-14 18:54:25
【问题描述】:

这是关于钱德勒的回答的问题(我没有足够高的代表发表评论):Enforcing statement order in C++

在他的回答中,假设 foo() 没有输入或输出。这是一个黑匣子,最终可以观察到,但不会立即需要(例如执行一些回调)。所以我们没有本地的输入/输出数据来告诉编译器不要优化。但我知道 foo() 会在某处修改内存,结果最终会被观察到。在这种情况下,以下内容会阻止语句重新排序并获得正确的时间吗?

#include <chrono>
#include <iostream>

//I believe this tells the compiler that all memory everywhere will be clobbered?
//(from his cppcon talk: https://youtu.be/nXaxk27zwlk?t=2441)
__attribute__((always_inline)) inline void DoNotOptimize() {
  asm volatile("" : : : "memory");
}

// The compiler has full knowledge of the implementation.
static int ugly_global = 1; //we print this to screen sometime later
static void foo(void) { ugly_global *= 2; }

auto time_foo() {
  using Clock = std::chrono::high_resolution_clock;

  auto t1 = Clock::now();         // Statement 1
  DoNotOptimize();
  foo();                          // Statement 2
  DoNotOptimize();
  auto t2 = Clock::now();         // Statement 3

  return t2 - t1;
}

【问题讨论】:

  • 你不想要std::memory_order而不是编译器特定的黑客吗?
  • 写入内存副作用,因此如果foo 在某处修改内存,则编译器无法重新排序时钟调用。否则就不可能编写程序并让它们做你想做的事情。
  • @JesperJuhl 我不知道如何在这里使用 std::memory_order ...我没有使用内存中的特定位置(在黑盒之外已知)。也许我只是对他们不熟悉。
  • @P.Mattione 老实说,我也不知道。当我看到你的问题时,这只是我脑海中闪现的第一件事。所以我很可能完全跑题了/错了。
  • @MooingDuck:请在您的评论中添加@以获取通知 :-) 并且如前所述:硬件本身可以重新排序执行,这是编译器无法控制的,只要没有特殊说明发出停止这些重新排序!另请参阅:stackoverflow.com/questions/27471969/…。 memory_order 也没有阻止任何事情,因为可以重新排序没有副作用的耗时的事情

标签: c++ optimization memory time compiler-construction


【解决方案1】:

在这种情况下,以下是否会阻止语句重新排序并获得正确的时间?

这应该不是必需的,因为对Clock::now 的调用应该在语言定义级别强制执行足够的排序。 (也就是说,C++11 标准规定,高分辨率时钟应该在此处获取系统所能提供的尽可能多的信息,采用此处最有用的方式。请参阅下面的“次要问题”。)

但还有一个更一般的情况。值得思考的问题是:提供 C++ 库实现的人实际上是如何编写此函数的? 或者,将 C++ 本身排除在外。给定语言标准,实现者(编写该语言实现的个人或团体)如何为您提供所需的东西?从根本上说,我们需要区分语言标准的要求实施提供者如何实施要求

语言本身可以用抽象机来表达,C 和C++ 语言也是。这个抽象机器的定义相当松散:它执行某种指令来访问数据,但在许多情况下,我们不知道它是如何完成这些事情的,甚至不知道各种数据项有多大。 (固定大小的整数有一些例外,如int64_t)和操作系统。机器可能有也可能没有“寄存器”,这些“寄存器”以无法寻址的方式保存事物,以及 可以 寻址并且其地址可以记录在指针中的内存:

p = &var

使值存储在p(在内存或寄存器中),这样使用*p访问存储在var(在内存或寄存器中)的值——一些机器,尤其是在过去,有/ 有可寻址的寄存器)。1

尽管如此,尽管如此抽象,我们仍希望在真实机器上运行真实代码。真实的机器有真正的限制:一些指令可能需要特定寄存器中的特定值(想想 x86 指令集中的所有奇怪的东西,或者使用专用寄存器的宽结果整数乘法器和除法器,如在某些 MIPS 处理器上),或导致 CPU 同步,或其他原因。

GCC 尤其发明了一种约束系统,使用机器的指令集来表达您在机器本身上可以做什么或不可以做什么。随着时间的推移,这演变成用户可访问的asm 结构,具有输入、输出和clobber 部分。你展示的那个:

__attribute__((always_inline)) inline void DoNotOptimize() {
  asm volatile("" : : : "memory");
}

表示“这条指令”(asm;实际提供的指令是空白的)“无法移动”(volatile)“并且破坏了所有计算机的内存,但没有寄存器”("memory"作为clobber部分)。

这不是作为一种语言的 C 或 C++ 的一部分。它只是一个编译器构造,由 GCC 支持,现在也由 clang 支持。但它足以强制编译器在asm 之前发出所有存储到内存,并在asm 之后根据需要从内存中重新加载值,以防在计算机执行包含的(不存在的)指令时它们发生变化asm 行。无法保证这在某些其他编译器中会起作用,甚至完全可以编译,但只要我们是实施者我们 选择我们为/with 实现的编译器。

C++ 作为一种语言现在支持有序的内存操作,这是实现者必须实现的。实现者可以使用这些asm volatile 构造来实现正确的结果,前提是它们确实实现了正确的结果。例如,如果我们需要使机器本身同步——以发出内存屏障——我们可以将适当的机器指令,例如 mfencemembar #sync 或其他任何可能的指令粘贴到 asm 中指令部分子句。另请参阅 compiler reordering vs memory reorderingKlaus mentioned in a comment

实现者需要找到一个适当有效的技巧,无论是否特定于编译器,以获得正确的语义,同时最大限度地减少任何运行时减速:例如,我们可能希望使用 lfence 而不是 mfence if这已经足够了,或者membar #LoadLoad,或者任何适合机器的东西。如果我们的Clock::now 实现需要某种花哨的内联asm,我们就写一个。如果没有,我们不会。我们确保我们生产出需要的东西——然后系统的所有用户都可以使用它,而无需知道我们必须调用哪种肮脏的实现技巧。

这里还有一个次要问题:语言规范真的会像我们认为/希望的那样约束实现者吗? Chris Dodd's comment 说他是这么认为的,而且在这类问题上他通常是对的。其他一些评论者不这么认为,但我和克里斯·多德一起讨论这个问题。我认为没有必要。不过,您始终可以编译为汇编,或反汇编编译的程序来检查!

如果编译器没有做正确的事,asm 会强制它在 GCC 和 clang 中做正确的事。它可能不适用于其他编译器。


1特别是在 KA-10 上,寄存器只是内存的前 16 个字。作为the Wikipedia page notes,这意味着您可以将指令放入其中并调用它们。因为前 16 个字是寄存器,所以这些指令的运行速度比其他指令快得多。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-09-26
    • 1970-01-01
    • 2021-11-03
    • 1970-01-01
    • 1970-01-01
    • 2017-02-21
    • 1970-01-01
    相关资源
    最近更新 更多