不加MOV*指令也能达到'volatile'的效果?
Achieving the effect of 'volatile' without the added MOV* instructions?
我有一段代码在任何情况下都必须 运行,因为它修改了自己范围之外的东西。让我们将这段代码定义为:
// Extremely simplified C++ as an example.
#include <iostream>
#include <map>
#include <cstdint>
#if defined(__GNUC__) || defined(__GNUG__) || defined(__clang__)
#include <x86intrin.h>
#elif defined(_MSC_VER)
#include <intrin.h>
#endif
uint64_t some_time_function() {
unsigned int waste;
return __rdtscp(&waste);
};
void insert_into_map(std::map<uint64_t, float>& data, uint64_t t1, uint64_t t0, float v) {
data.emplace((t1 - t0), v);
};
void fn(std::map<uint64_t, float>& map_outside_of_this_scope) {
const float a = 1;
const float b = 2;
float v = 0;
for (uint32_t i = 0; i < 1000000; i++) {
uint64_t t0 = some_time_function();
v = (v + b) - a;
uint64_t t1 = some_time_function();
insert_into_map(map_outside_of_this_scope, t1, t0, v);
}
}
int main(int argc, const char** argv) {
std::map<uint64_t, float> my_map;
fn(my_map);
std::cout << my_map.begin()->first << std::endl;
return 0;
}
这看起来像是编译器中优化器的最佳目标,这也是我在代码中观察到的情况:map_outside_of_this_scope
最终为空。不幸的是 map_outside_of_this_scope
对操作至关重要并且必须包含数据,否则应用程序会崩溃。解决此问题的唯一方法是将 v
标记为 volatile
,但这会使应用程序比基于程序集的等效函数慢得多。
没有volatile
的MOV
指令,有没有办法达到volatile
的效果?
volatile
与优化器直接相关,因为 volatile
变量的读写是 可观察到的行为 。这意味着无法删除读取和写入。
同样,优化器无法删除可通过其他方式观察到的变量写入 - 无论您是将变量写入 std::cout
、文件还是套接字。并且证明的负担在编译器上——只有在写入被证明是死的情况下才能消除写入。
在上面的例子中,例如,mymap.begin()->first
被写入std::cout
。这是可观察到的行为,因此即使没有 volatile
也必须保持该行为。但是 exact 细节并不重要。优化器可能会发现在此特定示例中仅观察到 ->first
成员。因此,v
(->second
值)未被观察到,可以合法地优化掉。
但是如果您将 mymap.begin()->second
复制到 volatile float sink
,那么写入 sink
是可观察到的行为,编译器必须确保写入正确的值。这几乎意味着您需要保留循环内的 v
计算,即使 v
本身不是 volatile
.
编译器可以执行影响如何读取和写入 v
的循环展开,因为单个v
更新不再可见。只有最终写入 volatile float sink
的值才算数。
因为您在评论中断言您最感兴趣的是问题的狭义答案...
Is there a way to achieve the effect of volatile
, without the MOV
instructions of volatile
?
... 我们唯一可以说的是,C 和 C++ 没有出于任何目的在任何地方指定涉及 MOV
或任何其他特定的汇编指令。如果您在已编译的二进制文件中观察到此类指令,那么这些指令反映了编译器开发人员的实现决策。此外,在您看到它们的地方,MOV
很可能对实现这些语义很重要。
此外,C 和 C++ 均未指定任何替代功能来复制 volatile
访问的相当具体的语义(他们为什么要这样做?)。但是,您也许可以使用内联汇编来实现自定义的、不同 效果以满足您的目的。
关于激发上述问题的更普遍的问题,我们真正可以说的是,执行不需要的优化的多个编译器这样做可能是合理的,原因从所提供的代码中不清楚.考虑到这一点,我建议您将解决问题的重点扩大到搜索为什么编译器认为他们可以在不涉及 volatile
时执行优化。为此,构造一个 MRE —— 不是为了我们,而是因为 MRE 构造的练习本身就是一种强大的调试技术。
我有一段代码在任何情况下都必须 运行,因为它修改了自己范围之外的东西。让我们将这段代码定义为:
// Extremely simplified C++ as an example.
#include <iostream>
#include <map>
#include <cstdint>
#if defined(__GNUC__) || defined(__GNUG__) || defined(__clang__)
#include <x86intrin.h>
#elif defined(_MSC_VER)
#include <intrin.h>
#endif
uint64_t some_time_function() {
unsigned int waste;
return __rdtscp(&waste);
};
void insert_into_map(std::map<uint64_t, float>& data, uint64_t t1, uint64_t t0, float v) {
data.emplace((t1 - t0), v);
};
void fn(std::map<uint64_t, float>& map_outside_of_this_scope) {
const float a = 1;
const float b = 2;
float v = 0;
for (uint32_t i = 0; i < 1000000; i++) {
uint64_t t0 = some_time_function();
v = (v + b) - a;
uint64_t t1 = some_time_function();
insert_into_map(map_outside_of_this_scope, t1, t0, v);
}
}
int main(int argc, const char** argv) {
std::map<uint64_t, float> my_map;
fn(my_map);
std::cout << my_map.begin()->first << std::endl;
return 0;
}
这看起来像是编译器中优化器的最佳目标,这也是我在代码中观察到的情况:map_outside_of_this_scope
最终为空。不幸的是 map_outside_of_this_scope
对操作至关重要并且必须包含数据,否则应用程序会崩溃。解决此问题的唯一方法是将 v
标记为 volatile
,但这会使应用程序比基于程序集的等效函数慢得多。
没有volatile
的MOV
指令,有没有办法达到volatile
的效果?
volatile
与优化器直接相关,因为 volatile
变量的读写是 可观察到的行为 。这意味着无法删除读取和写入。
同样,优化器无法删除可通过其他方式观察到的变量写入 - 无论您是将变量写入 std::cout
、文件还是套接字。并且证明的负担在编译器上——只有在写入被证明是死的情况下才能消除写入。
在上面的例子中,例如,mymap.begin()->first
被写入std::cout
。这是可观察到的行为,因此即使没有 volatile
也必须保持该行为。但是 exact 细节并不重要。优化器可能会发现在此特定示例中仅观察到 ->first
成员。因此,v
(->second
值)未被观察到,可以合法地优化掉。
但是如果您将 mymap.begin()->second
复制到 volatile float sink
,那么写入 sink
是可观察到的行为,编译器必须确保写入正确的值。这几乎意味着您需要保留循环内的 v
计算,即使 v
本身不是 volatile
.
编译器可以执行影响如何读取和写入 v
的循环展开,因为单个v
更新不再可见。只有最终写入 volatile float sink
的值才算数。
因为您在评论中断言您最感兴趣的是问题的狭义答案...
Is there a way to achieve the effect of
volatile
, without theMOV
instructions ofvolatile
?
... 我们唯一可以说的是,C 和 C++ 没有出于任何目的在任何地方指定涉及 MOV
或任何其他特定的汇编指令。如果您在已编译的二进制文件中观察到此类指令,那么这些指令反映了编译器开发人员的实现决策。此外,在您看到它们的地方,MOV
很可能对实现这些语义很重要。
此外,C 和 C++ 均未指定任何替代功能来复制 volatile
访问的相当具体的语义(他们为什么要这样做?)。但是,您也许可以使用内联汇编来实现自定义的、不同 效果以满足您的目的。
关于激发上述问题的更普遍的问题,我们真正可以说的是,执行不需要的优化的多个编译器这样做可能是合理的,原因从所提供的代码中不清楚.考虑到这一点,我建议您将解决问题的重点扩大到搜索为什么编译器认为他们可以在不涉及 volatile
时执行优化。为此,构造一个 MRE —— 不是为了我们,而是因为 MRE 构造的练习本身就是一种强大的调试技术。