【问题标题】:How does MSVC optimize static variable usage?MSVC 如何优化静态变量的使用?
【发布时间】:2011-04-19 13:49:30
【问题描述】:

我对 Microsoft Visual C++ 编译器如何处理/优化静态变量感兴趣。

我的代码:

#include <cstdlib>

void no_static_initialization()
{
    static int value = 3;
}

void static_initialization(int new_value)
{
    static int value = new_value;
}

int main()
{
    no_static_initialization();
    static_initialization(1);
    static_initialization(std::rand());

    return 0;
}

这是代码的程序集(经过优化编译):

我的主要兴趣领域是最后一个案例。

在这里,第一个语句得到了完全优化,第二个语句的两个调用被内联,它们实际上代表了类似的代码块。

如果测试不成功,他们每个人都会做test something something,然后做一个简短的jump(这些跳转显然指向相应程序的结束)。

编译器是否会明确检查每个函数调用是否是第一次调用该函数?
编译器是否真的有一个flag,它表明这是不是第一次调用函数?
它存储在哪里(我猜test 的所有内容都与它有关,但我不确定)?

【问题讨论】:

  • +1 用于包含编译器 输出,这样我就不必这样做了。
  • gcc 在函数内部未使用时也无法优化掉 static int:(godbolt.org/g/TAah2B 用于 x86-64 gcc 和 CL19 asm 输出)。它也没有内联到 main 中。但是,gcc 的标志检查要轻得多:在已经初始化的快速路径上只有 3 条指令,而不是 8 条。(在宏融合测试和分支的 CPU 上,2 条融合域微指令而不是 7 条)。实际上,gcc 确实优化了 static int 本身,但 不是 保护变量。

标签: c++ assembly visual-c++ optimization static


【解决方案1】:

是的,编译器必须添加一个隐藏标志来测试它是否是对函数的第一次调用并根据它进行初始化。在这两个 sn-ps 中,它都在测试标志,如果它被提升,它将跳转到函数的末尾,否则它将初始化静态变量。请注意,由于编译器已内联该函数,因此它也可以优化第二次测试,因为知道该标志仅在第一次调用时进行测试。

标志似乎位于地址 0x00403374,并占用一个字节,而变量本身位于地址 0x00403370。

【讨论】:

  • 据我所见,编译器可以完美地优化任何“首次运行”检查,以检查用文字初始化的静态本地,并使用与全局静态初始化相同的机制.这将使任何标志等变得不必要。您无法通过符合标准的方式来区分差异,因此“好像”规则适用。
  • @Pavel Minaev:我已经重读了你的评论几次。我对您的意思的理解是:由常量初始化的局部静态可以隐式转换为全局静态并避免额外的标志/检查。如果这就是你的意思,在一般情况下你不能。想象一下,一个局部静态变量有一个采用 int 字面量的构造函数。因为构造函数可能具有未知的副作用,编译器不能在主执行之前移动该构造函数的执行,否则它可能会破坏 as-if 规则,并可能在定义良好的程序中导致 UB。
  • 考虑:extern Logger logger; struct test { test( int ) { logger.log( "test created" ); }}; void foo() { static test t(5); } int main() { foo(); } 如果logger 是在不同翻译单元中初始化的全局变量,则程序(我们看到的部分)定义良好。在调用foo 时,logger 必须已经被初始化,所以调用它的方法是有效的。如果编译器决定将t 移出foo 并将其视为全局变量,则foo::tlogger 的初始化顺序是未定义的,并且程序属于未定义行为领域。
  • 与往常一样,在某些情况下,如果编译器能够保证初始化没有任何副作用(与算术类型一样)或者它是具有简单构造函数的用户定义类型或......可能是其他情况。除了最基本的示例之外,分析器必须非常努力地工作以确保它可以优化一个字节和一个测试。
  • 我想我不够清楚 - 我专门指的是问题中的情况(毕竟内联和常量折叠,最终结果与静态初始化相同)。当然,如果有任何可观察到的副作用,情况就不同了。
【解决方案2】:

我喜欢使用 LLVM,因为它生成的代码更明确地告诉你它在做什么:

实际代码如下,因为它有点长。是的,LLVM 为静态值创建保护条件变量。注意static_initialization/bb: 是如何获得保护的,检查它是否有一个与已经初始化相对应的特定值,如果需要初始化,则分支到 bb1,如果不需要,则分支到 bb2。这不是可能解决单个初始化要求的唯一方法,但它是通常的方法。

; ModuleID = '/tmp/webcompile/_31867_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"

@guard variable for static_initialization(int)::value = internal global i64 0 ; <i64*> [#uses=3]
@static_initialization(int)::value = internal global i32 0 ; <i32*> [#uses=1]

define void @no_static_initialization()() nounwind {
entry:
  br label %return

return:                                           ; preds = %entry
  ret void
}

define void @static_initialization(int)(i32 %new_value) nounwind {
entry:
  %new_value_addr = alloca i32                    ; <i32*> [#uses=2]
  %0 = alloca i8                                  ; <i8*> [#uses=2]
  %retval.1 = alloca i8                           ; <i8*> [#uses=2]
  %"alloca point" = bitcast i32 0 to i32          ; <i32> [#uses=0]
  store i32 %new_value, i32* %new_value_addr
  %1 = load i8* bitcast (i64* @guard variable for static_initialization(int)::value to i8*), align 1 ; <i8> [#uses=1]
  %2 = icmp eq i8 %1, 0                           ; <i1> [#uses=1]
  br i1 %2, label %bb, label %bb2

bb:                                               ; preds = %entry
  %3 = call i32 @__cxa_guard_acquire(i64* @guard variable for static_initialization(int)::value) nounwind ; <i32> [#uses=1]
  %4 = icmp ne i32 %3, 0                          ; <i1> [#uses=1]
  %5 = zext i1 %4 to i8                           ; <i8> [#uses=1]
  store i8 %5, i8* %retval.1, align 1
  %6 = load i8* %retval.1, align 1                ; <i8> [#uses=1]
  %toBool = icmp ne i8 %6, 0                      ; <i1> [#uses=1]
  br i1 %toBool, label %bb1, label %bb2

bb1:                                              ; preds = %bb
  store i8 0, i8* %0, align 1
  %7 = load i32* %new_value_addr, align 4         ; <i32> [#uses=1]
  store i32 %7, i32* @static_initialization(int)::value, align 4
  store i8 1, i8* %0, align 1
  call void @__cxa_guard_release(i64* @guard variable for static_initialization(int)::value) nounwind
  br label %bb2

bb2:                                              ; preds = %bb1, %bb, %entry
  br label %return

return:                                           ; preds = %bb2
  ret void
}

declare i32 @__cxa_guard_acquire(i64*) nounwind

declare void @__cxa_guard_release(i64*) nounwind

define i32 @main() nounwind {
entry:
  %retval = alloca i32                            ; <i32*> [#uses=2]
  %0 = alloca i32                                 ; <i32*> [#uses=2]
  %"alloca point" = bitcast i32 0 to i32          ; <i32> [#uses=0]
  call void @no_static_initialization()() nounwind
  call void @static_initialization(int)(i32 1) nounwind
  %1 = call i32 @rand() nounwind                  ; <i32> [#uses=1]
  call void @static_initialization(int)(i32 %1) nounwind
  store i32 0, i32* %0, align 4
  %2 = load i32* %0, align 4                      ; <i32> [#uses=1]
  store i32 %2, i32* %retval, align 4
  br label %return

return:                                           ; preds = %entry
  %retval1 = load i32* %retval                    ; <i32> [#uses=1]
  ret i32 %retval1
}

declare i32 @rand() nounwind

【讨论】:

    猜你喜欢
    • 2011-01-29
    • 2011-09-07
    • 1970-01-01
    • 2019-04-15
    • 2021-10-28
    • 2016-03-05
    • 2018-03-15
    • 2023-03-22
    • 1970-01-01
    相关资源
    最近更新 更多