为什么编译器不会优化掉这个载入操作

3
在下面的C++程序中:
struct O
{
   const int id;
};

extern void externalFunc();

int func(O* o)
{
    //first load of o->id happens here:
    int first = o->id;
    externalFunc();

    //second load happens here, but why?
    return o->id + first;
}

无论是Clang还是MSVC,启用所有优化选项编译此代码时,o->id值会以一种方式从内存中加载两次。

为什么这些编译器无法删除第二个加载?我试图通过将id成员标记为const来告诉编译器该值保证不会更改,但显然两个编译器都没有找到足够的保证。如果我去掉对externalFunc()的调用,它们会优化掉第二个加载。我如何说服编译器这个值真的不会改变?


1
externalFunc 可以在指向 o 的内存中摧毁并重新创建 O 对象。 - Igor Tandetnik
如果你的 func() 接受了一个 const O & o 或者一个 const O * const o,会发生什么? - Christian Severin
1
*ofunc 函数下的变化演示:http://rextester.com/TISOA68957 - Igor Tandetnik
尝试使用 int func(O* __restrict o) { - jxh
4个回答

3

请考虑以下内容:

#include <iostream>

struct O
{
   const int id;
   O(int x) : id(x) {}
};

O* global = nullptr;

void externalFunc() {
    global->~O();
    new(global) O(42);
}

int func(O* o)
{
    int first = o->id;
    externalFunc();
    // o->id has changed, even though o hasn't    
    return o->id + first;
}

int main()
{
    O o(1);
    global = &o;
    std::cout << func(&o);
}

输出:43演示


谢谢!你知道有什么方法可以向编译器保证这种情况不会发生吗? - Lucas Meijer
传值,我猜。这个练习的最终目标是什么?这个问题听起来很像一个XY问题 - Igor Tandetnik

2

externalFunc()可能会更改o->id。(不是o,它是一个本地变量。)


怎么可能呢?id是常量,如果你尝试o->id=123;它会编译失败。 - Lucas Meijer
它可以在该位置使用“放置新对象”将一个对象进行实例化。 - Bathsheba
“const” 实际上并不能保证它永远不会被写入。您可以通过 const_cast 添加/删除 const 属性。这通常是一个不好的想法,但仍然有效,并且编译器必须在所有情况下生成正确的代码。 - Rambo Ramon
2
@RamboRamon:不正确,将最初为const的变量的constness去除是未定义的行为。编译器因此允许忽略这种可能性。 - Bathsheba
@Bathsheba 我可能有点太简单了。我想的是像 const int* p = &(o->id); *(const_cast<int*>(p)) = 5; 这样的东西。如果这仍然是未定义行为,那么我学到了新东西。 - Rambo Ramon

1
为什么这些编译器无法删除第二个加载项?我试图通过将id成员标记为const来告诉编译器该值保证不会更改,但显然两个编译器都没有找到足够的保证。
因为它并不是。考虑以下示例。
static O mg {5};

void
externalFunc()
{
  mg.~O();
  new (&mg) O {6};
}

int
main()
{
  std::cout << mg.id << '\n';
  func(&mg);
  std::cout << mg.id << '\n';
}

第一次加载读取值5,第二次将读取6。
如何说服编译器这个值确实不会改变?
简单地缓存该字段。这仍然不能说服编译器o->id不会改变,但它可以保证如果它确实改变了,你并不关心。
int
func(O* o)
{
  const int id = o->id;
  externalFunc();
  return id + id;
}

我已经养成了一个通用的习惯,即将我通过指针访问的所有原始字段(包括this指针)的值缓存到本地(const)变量中。如果编译器能够确保这些值不会更改,则没有额外的成本,如果不能,则可能会产生稍微更好的代码。作为一个很好的附带效果,它还允许您为在函数上下文中最有意义的值命名。


0
编译器在编译func()时本身没有externalFunc()的代码,因此它不知道它可能会做什么。因此,它充当一个障碍。
如果您链接静态库,这将属于链接时优化(可以使用-flto在GCC上启用),并且也受到MSVC的支持。
为了提示GCC / Clang(看起来MSVC不支持这个)该函数不改变全局内存,您可以使用pure属性标记它:
extern void __attribute__((pure)) void externalFunc();

然后它将停止提出那个障碍。


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