仿佛规则和分配的删除

9
"仿佛规则"赋予编译器权利,在特定规则下可以优化掉或重新排序不影响程序输出和正确性的表达式。其中,§1.9.5规定符合要求的实现执行良好形式的程序时,应该生成与相同程序和输入在抽象机的对应实例中可能执行的相同观察行为。我上面提供的cppreference网址特别提到了C++14下易失性对象值的特殊规则,以及“new expressions”的规则:

new-expression有另一个例外,即使提供了具有可观测副作用的用户定义替换,编译器也可以删除对可替换分配函数的调用。

我认为这里所说的“可替换”是指例如在...中讨论的内容。

§18.6.1.1.2

可替换的:C++程序可以定义一个带有此函数签名的函数,以代替C++标准库定义的默认版本。

根据as-if规则,下面的mem是否可以被删除或重新排序?

  {
  ... some conformant code // upper block of code

  auto mem = std::make_unique<std::array<double, 5000000>>();

  ... more conformant code, not using mem // lower block of code
  }

有没有一种方法可以确保它不被移除,并留在上下代码块之间?一个放置得当的 volatile(或者是 volatile std::array 或者是 auto 左侧)会让人想起,但是由于没有读取 mem,我认为即使在 as-if 规则下也无济于事。
顺便说一下;我一直无法让 Visual Studio 2015 优化掉 mem 和分配。
澄清一下:观察这个的方法是,在两个代码块之间的任何 i/o 之间调用操作系统进行分配。这样做的目的是为了测试用例和/或尝试让对象分配到新位置。

1
我相信使用volatile可以解决这个问题,但还有另一个问题:没有任何东西依赖于mem的值,因此编译器可以将分配移动到代码块中的任何位置。它可能在开头、结尾或任何其他地方进行分配。 - Revolver_Ocelot
@Revolver_Ocelot 我认为使用volatile关键字甚至不会有任何区别。make_unique创建了一个新对象,据我所知,C++编译器不会因为副作用的可能性而优化掉对象的创建。它们可能省略不必要的构造函数调用,但始终确保至少调用一次构造函数。否则,具有具有副作用的构造函数的RAII对象的代码将无法安全地依赖于该模式,除非包含内存屏障。 - JAB
[intro.execution]/8说:“对于volatile对象的访问严格按照抽象机器的规则进行评估”。我理解为“不能将as-if规则应用于volatile对象”,所以这应该有所帮助。 - Revolver_Ocelot
1
@JAB,std::array和double构造函数都没有任何副作用,即使存在副作用,内存分配本身也可以被优化掉。RAII的工作原理正是因为有副作用(如果没有副作用,您将无法检查它是否存在或已经过去)。证明:http://goo.gl/lCCyBx。正如您所看到的,编译器优化了内存分配。 - Revolver_Ocelot
@Revolver_Ocelot 我不确定这里的 volatile 到底有多远:new volatile int(10)。规范说明访问易失性变量是可观察的副作用,而不是调用分配函数。因此,如果编译器安排在其他地方分配内存,则仍然可以省略对分配函数的调用!?只是根据那个 cppreference 引用..我还没有查找相关规范。 - Johannes Schaub - litb
显示剩余6条评论
1个回答

4
是;不。 不能在C++内部。
C++的抽象机制根本不涉及系统分配调用。 C++仅固定了此类调用的副作用,这些副作用影响抽象机器的行为,并且即使编译器自由地进行一些其他操作,只要它在程序的抽象机器中具有相同的可观察行为,就可以允许。
在抽象机中,“auto mem = std::make_unique<std::array<double, 5000000>>();”创建一个变量“mem”。如果使用,它将使您访问打包成数组的大量双精度浮点数。抽象机自由地抛出异常或向您提供该大量双精度浮点数的访问权;两者都可以接受。
请注意,合法的C++编译器可以通过无条件抛出分配失败(或对于无throw版本返回nullptr)来替换所有通过new进行的分配,但这将是较差的实现质量。
在分配的情况下,C++标准并没有真正说明它来自哪里。例如,编译器可以自由地使用静态数组,并将delete调用设置为无操作(请注意,它可能必须证明它捕获了缓冲区上调用delete的所有方法)。
接下来,如果您有一个静态数组,并且没有人读取或写入它(并且构建无法观察),则编译器可以自由地消除它。
话虽如此,以上大部分都依赖于编译器知道正在发生什么。
因此,一种方法是使编译器无法知道。让您的代码加载DLL,然后在要求其状态为已知的点上将指针传递给unique_ptr。
由于编译器无法在运行时DLL调用上进行优化,因此变量的状态基本上必须符合您所预期的状态。
不幸的是,C++中没有标准的动态加载代码的方法,因此您必须依靠当前系统。
该DLL可以单独编写为noop;或者,您可以检查某些外部状态,并根据外部状态有条件地加载和传递数据到DLL。只要编译器无法证明该外部状态将发生,它就不能优化掉未进行的调用。然后,永远不要设置该外部状态。
在块的顶部声明变量。在未初始化时将其传递给fake-external-DLL。在初始化之前,再次重复。然后,在销毁变量之前,在该块的末尾执行此操作, .reset()它,然后再次执行此操作。

你的“不”回答了哪个问题? - Ben Voigt
2
@BenVoigt 我看到两个?。我现在按顺序回答了它们。 - Yakk - Adam Nevraumont
1
你提到的 throw 很有趣,因为异常会在代码块之外被观察到,并且不能在上方代码的副作用之前或下方代码的副作用之后发生。 - Johan Lundberg
1
@JohanLundberg 是的,除非编译器在那种情况下被强制抛出。它允许抛出,有时基于资源、月相、总是或甚至从不。所有这些都是根据C++标准对new调用做出响应的有效方式。作为一种优化,通过不分配来删除抛出的可能性是合法的。 "总是抛出"的部分只是说明C++标准赋予实现的极端自由度。 - Yakk - Adam Nevraumont
@joh,我不明白。你能澄清一下这个问题吗?“它”是什么?“new”?你是说C++标准谈到了一个明确的“分配函数”步骤,会抛出异常吗?还是你在谈论分配器? - Yakk - Adam Nevraumont
显示剩余4条评论

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