在以下情况下应该采取什么行为:
class C {
boost::mutex mutex_;
std::map<...> data_;
};
C& get() {
static C c;
return c;
}
int main() {
get(); // is compiler free to optimize out the call?
....
}
编译器是否允许优化对
get()
的调用?
这个想法是在多线程操作需要静态变量之前对其进行初始化
这是一个更好的选择吗?:
C& get() {
static C *c = new C();
return *c;
}
在 C++23 (N4950) 中,当进入静态局部变量的包含块时,可以观察到初始化静态局部变量的任何副作用。因此,除非编译器可以确定初始化变量没有明显的副作用,否则它将必须生成代码以在适当的时间调用
get()
(或执行 get()
的内联版本,视情况而定)是)。
与早期标准相反,C++ 23 不再允许“提前”完成静态局部变量的动态初始化(如下所述)。
[stmt.dcl]/3:
动态初始化具有静态存储持续时间(6.7.5.2)或线程存储持续时间的块变量 (6.7.5.3) 在控制第一次通过其声明时执行;这样一个变量被认为 初始化完成后进行初始化。
C 和 C++ 标准在一个相当简单的原则下运行,通常被称为“as-if 规则”——基本上,编译器可以自由地做几乎任何事情,只要没有一致的代码可以辨别它所做的和它所做的之间的区别。官方要求什么。
我没有看到一种方法可以让一致的代码辨别在这种情况下是否真正调用了
get
,所以在我看来可以自由地对其进行优化。
至少在 N4296 中,该标准包含对静态局部变量进行早期初始化的显式许可:
a 的不断初始化(3.6.2) 具有静态存储持续时间的块范围实体(如果适用)在首次进入其块之前执行。 允许实现使用 static 或 在允许实现静态初始化的相同条件下的线程存储持续时间 在命名空间范围内具有静态或线程存储持续时间的变量 (3.6.2)。否则这样的变量是 控件第一次通过其声明时初始化;这样的变量被认为是初始化的 初始化完成。
因此,根据此规则,局部变量的初始化可以在执行的早期任意发生,因此即使它具有明显的副作用,它们也可以在任何尝试观察它们的代码之前发生。因此,您不能保证看到它们,因此允许对其进行优化。
根据您的编辑,这是一个改进的版本,具有相同的结果。
输入:
struct C {
int myfrob;
int frob();
C(int f);
};
C::C(int f) : myfrob(f) {}
int C::frob() { return myfrob; }
C& get() {
static C *c = new C(5);
return *c;
}
int main() {
return get().frob(); // is compiler free to optimize out the call?
}
输出:
; ModuleID = '/tmp/webcompile/_28088_0.bc'
target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-v64:64:64-v128:128:128-a0:0:64-s0:64:64-f80:128:128-n8:16:32:64"
target triple = "x86_64-linux-gnu"
%struct.C = type { i32 }
@guard variable for get()::c = internal global i64 0 ; <i64*> [#uses=4]
declare i32 @__cxa_guard_acquire(i64*) nounwind
declare i8* @operator new(unsigned long)(i64)
declare void @__cxa_guard_release(i64*) nounwind
declare i8* @llvm.eh.exception() nounwind readonly
declare i32 @llvm.eh.selector(i8*, i8*, ...) nounwind
declare void @__cxa_guard_abort(i64*) nounwind
declare i32 @__gxx_personality_v0(...)
declare void @_Unwind_Resume_or_Rethrow(i8*)
define i32 @main() {
entry:
%0 = load i8* bitcast (i64* @guard variable for get()::c to i8*), align 8 ; <i8> [#uses=1]
%1 = icmp eq i8 %0, 0 ; <i1> [#uses=1]
br i1 %1, label %bb.i, label %_Z3getv.exit
bb.i: ; preds = %entry
%2 = tail call i32 @__cxa_guard_acquire(i64* @guard variable for get()::c) nounwind ; <i32> [#uses=1]
%3 = icmp eq i32 %2, 0 ; <i1> [#uses=1]
br i1 %3, label %_Z3getv.exit, label %bb1.i
bb1.i: ; preds = %bb.i
%4 = invoke i8* @operator new(unsigned long)(i64 4)
to label %invcont.i unwind label %lpad.i ; <i8*> [#uses=2]
invcont.i: ; preds = %bb1.i
%5 = bitcast i8* %4 to %struct.C* ; <%struct.C*> [#uses=1]
%6 = bitcast i8* %4 to i32* ; <i32*> [#uses=1]
store i32 5, i32* %6, align 4
tail call void @__cxa_guard_release(i64* @guard variable for get()::c) nounwind
br label %_Z3getv.exit
lpad.i: ; preds = %bb1.i
%eh_ptr.i = tail call i8* @llvm.eh.exception() ; <i8*> [#uses=2]
%eh_select12.i = tail call i32 (i8*, i8*, ...)* @llvm.eh.selector(i8* %eh_ptr.i, i8* bitcast (i32 (...)* @__gxx_personality_v0 to i8*), i8* null) ; <i32> [#uses=0]
tail call void @__cxa_guard_abort(i64* @guard variable for get()::c) nounwind
tail call void @_Unwind_Resume_or_Rethrow(i8* %eh_ptr.i)
unreachable
_Z3getv.exit: ; preds = %invcont.i, %bb.i, %entry
%_ZZ3getvE1c.0 = phi %struct.C* [ null, %bb.i ], [ %5, %invcont.i ], [ null, %entry ] ; <%struct.C*> [#uses=1]
%7 = getelementptr inbounds %struct.C* %_ZZ3getvE1c.0, i64 0, i32 0 ; <i32*> [#uses=1]
%8 = load i32* %7, align 4 ; <i32> [#uses=1]
ret i32 %8
}
值得注意的是,没有为 ::get 发出任何代码,但 main 仍然根据需要使用保护变量分配 ::get::c (在 %4 处)(在 %2 处以及 invcont.i 和 lpad.i 的末尾) 。这里的 llvm 内联了所有这些东西。
tl;dr:别担心,优化器通常会正确处理这些事情。您看到错误了吗?
编译器是否优化函数调用基本上是标准中未指定的行为。未指定的行为基本上是从一组有限可能性中选择的行为,但选择可能并非每次都一致。在这种情况下,选择是“优化”或“不优化”,标准没有指定,并且实现也不应该记录,因为给定的实现可能不会一致地采取这种选择。
如果这个想法只是“触摸”,如果我们只添加一个虚拟易失性变量并在每次调用中虚拟递增它会有帮助吗
例如
C& getC(){
volatile int dummy;
dummy++;
// rest of the code
}