在C/C++中,const数组和静态const数组有什么区别?

64

在Visual Studio 2015中编译以下代码(Win7,x64,调试配置)需要非常非常非常长的时间(即超过10分钟)

double tfuuuuuuu(int Ind)
{
  const double Arr[600 * 258] = {3.5453, 45.234234234, 234234.234,// extends to 258 values for each line
                                // 599 lines here.....
                                };                     
  return Arr[Ind];
}

但是当我添加了 static 关键字后,编译只花费了半秒钟。

double tfuuuuuuu(int Ind)
{
  static const double Arr[600 * 258] = {3.5453, 45.234234234, 234234.234,// extends to 258 values for each line
                                // 599 lines here.....
                                };                     
  return Arr[Ind];
}
我知道static意味着变量在调用之间保持其值,但如果数组已经是const,添加static会有什么区别?为什么编译时间会发生如此巨大的变化?编辑:实际代码可在此处找到:here(编译模式为Debug)。

评论不适合进行长时间的讨论;此对话已被移至聊天室 - Samuel Liew
@BennyK:哇塞,这真的很重要。有趣。你能使用不同的编译器(MinGW或TinyC)吗?你有没有可以比较的非Windows系统?作为参考,我将你的MyLBP.c文件下载到我的Macbook上,并使用“gcc -c”进行编译,我没有看到任何显著的构建时间差异。 - John Bode
@JohnBode:嗯,我在Windows上工作,但正如您在此处所见:https://chat.stackoverflow.com/rooms/193093/discussion-on-question-by-benny-k-whats-the-difference-between-const-array-to-s 有人使用gcc 4.8.5进行了检查,并没有发现任何问题,我猜这是意料之中的...(VS问题) - Benny K
1
很难确定,但我怀疑这个错误报告可能实际上涵盖了同样的问题。等待Visual Studio 2019 v16.1发布,看看那时是否仍然存在这个问题:https://developercommunity.visualstudio.com/content/problem/407999/clexe-using-20gb-of-memory-compiling-small-file-in.html - niemiro
最近这个问题的所有流量都从哪里来?它不再在 HNQ 上了,也没有最近的编辑。 - dbush
2个回答

48

声明为static的局部变量在整个程序运行期间具有生命周期,并通常存储在数据段中。编译器通过拥有一个包含这些值的部分来实现此功能。

未声明为静态的局部变量通常位于堆栈上,并且每次进入变量作用域时必须进行初始化。

查看static情况下的汇编代码,MSVC 2015输出以下内容:

; Listing generated by Microsoft (R) Optimizing Compiler Version 19.00.24215.1 

    TITLE   MyLBP.c
    .686P
    .XMM
    include listing.inc
    .model  flat

INCLUDELIB LIBCMT
INCLUDELIB OLDNAMES

CONST   SEGMENT
?Arr@?1??tfuuuuuuu@@9@9 DQ 04060c00000000000r   ; 134   ; `tfuuuuuuu'::`2'::Arr
    DQ  03fe15efd20a7955br      ; 0.542845
    DQ  03fdf59701e4b19afr      ; 0.489834
    DQ  0bfd8e38e9ab7fcb1r      ; -0.388889
    DQ  0bfe59f22c01e68a1r      ; -0.675676
    DQ  0bfeb13b15d5aa410r      ; -0.846154
    DQ  0bfe2c2355f07776er      ; -0.586207
    DQ  03fefffffbf935359r      ; 1
    ...
    ORG $+1036128
CONST   ENDS
PUBLIC  _tfuuuuuuu
EXTRN   __fltused:DWORD
; Function compile flags: /Odtp
_TEXT   SEGMENT
_Ind$ = 8                       ; size = 4
_tfuuuuuuu PROC
; File c:\users\dennis bush\documents\x2.c
; Line 4
    push    ebp
    mov ebp, esp
; Line 106
    mov eax, DWORD PTR _Ind$[ebp]
    fld QWORD PTR ?Arr@?1??tfuuuuuuu@@9@9[eax*8]
; Line 107
    pop ebp
    ret 0
_tfuuuuuuu ENDP
_TEXT   ENDS
END

gcc 4.8.5输出如下内容:

    .file   "MyLBP.c"
    .text
    .globl  tfuuuuuuu
    .type   tfuuuuuuu, @function
tfuuuuuuu:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    %edi, -4(%rbp)
    movl    -4(%rbp), %eax
    cltq
    movq    Arr.1724(,%rax,8), %rax
    movq    %rax, -16(%rbp)
    movsd   -16(%rbp), %xmm0
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   tfuuuuuuu, .-tfuuuuuuu
    .section    .rodata
    .align 32
    .type   Arr.1724, @object
    .size   Arr.1724, 1238400
Arr.1724:
    .long   0
    .long   1080082432
    .long   547853659
    .long   1071734525
    .long   508238255
    .long   1071602032
    .long   2595749041
    .long   -1076305010
    .long   3223218337
    ...
    .ident  "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-16)"
    .section    .note.GNU-stack,"",@progbits

因此,两者都全局定义数据并直接引用该全局数组。

现在让我们看看非静态代码。首先是VSMC2015:

; Listing generated by Microsoft (R) Optimizing Compiler Version 19.00.24215.1 

    TITLE   MyLBP.c
    .686P
    .XMM
    include listing.inc
    .model  flat

INCLUDELIB LIBCMT
INCLUDELIB OLDNAMES

PUBLIC  _tfuuuuuuu
PUBLIC  __real@3e45798ee2308c3a
PUBLIC  __real@3f40e1cf9350aa3c
PUBLIC  __real@3f43b1f90beff84b
PUBLIC  __real@3f4c6220dc6e8066
PUBLIC  __real@3f4ea4c648794089
PUBLIC  __real@3f50023666188dc0
PUBLIC  __real@3f53957e56f300e9
PUBLIC  __real@3f55235d7d33b25f
PUBLIC  __real@3f5828f66e5bd33a
PUBLIC  __real@3f5c044284dfce31
PUBLIC  __real@3f5c87c05341c674
...
EXTRN   @__security_check_cookie@4:PROC
EXTRN   __chkstk:PROC
EXTRN   _memset:PROC
EXTRN   ___security_cookie:DWORD
EXTRN   __fltused:DWORD
;   COMDAT __real@bff0000000000000
CONST   SEGMENT
__real@bff0000000000000 DQ 0bff0000000000000r   ; -1
CONST   ENDS
;   COMDAT __real@bfefffffdfc9a9ad
CONST   SEGMENT
__real@bfefffffdfc9a9ad DQ 0bfefffffdfc9a9adr   ; -1
CONST   ENDS
;   COMDAT __real@bfefffffbf935359
CONST   SEGMENT
__real@bfefffffbf935359 DQ 0bfefffffbf935359r   ; -1
CONST   ENDS
;   COMDAT __real@bfefffff9f5cfd06
CONST   SEGMENT
__real@bfefffff9f5cfd06 DQ 0bfefffff9f5cfd06r   ; -1
CONST   ENDS
;   COMDAT __real@bfefffff7f26a6b3
CONST   SEGMENT
__real@bfefffff7f26a6b3 DQ 0bfefffff7f26a6b3r   ; -1
CONST   ENDS
;   COMDAT __real@bfefffff5ef05060
CONST   SEGMENT
__real@bfefffff5ef05060 DQ 0bfefffff5ef05060r   ; -1
CONST   ENDS
...
; Function compile flags: /Odtp
_TEXT   SEGMENT
_Arr$ = -1238404                    ; size = 1238400
__$ArrayPad$ = -4                   ; size = 4
_Ind$ = 8                       ; size = 4
_tfuuuuuuu PROC
; File c:\users\dennis bush\documents\x2.c
; Line 4
    push    ebp
    mov ebp, esp
    mov eax, 1238404                ; 0012e584H
    call    __chkstk
    mov eax, DWORD PTR ___security_cookie
    xor eax, ebp
    mov DWORD PTR __$ArrayPad$[ebp], eax
; Line 5
    movsd   xmm0, QWORD PTR __real@4060c00000000000
    movsd   QWORD PTR _Arr$[ebp], xmm0
    movsd   xmm0, QWORD PTR __real@3fe15efd20a7955b
    movsd   QWORD PTR _Arr$[ebp+8], xmm0
    movsd   xmm0, QWORD PTR __real@3fdf59701e4b19af
    movsd   QWORD PTR _Arr$[ebp+16], xmm0
    movsd   xmm0, QWORD PTR __real@bfd8e38e9ab7fcb1
    movsd   QWORD PTR _Arr$[ebp+24], xmm0
    movsd   xmm0, QWORD PTR __real@bfe59f22c01e68a1
    movsd   QWORD PTR _Arr$[ebp+32], xmm0
    movsd   xmm0, QWORD PTR __real@bfeb13b15d5aa410
    movsd   QWORD PTR _Arr$[ebp+40], xmm0
    movsd   xmm0, QWORD PTR __real@bfe2c2355f07776e
    movsd   QWORD PTR _Arr$[ebp+48], xmm0
    ...
    push    1036128                 ; 000fcf60H
    push    0
    lea eax, DWORD PTR _Arr$[ebp+202272]
    push    eax
    call    _memset
    add esp, 12                 ; 0000000cH
; Line 106
    mov ecx, DWORD PTR _Ind$[ebp]
    fld QWORD PTR _Arr$[ebp+ecx*8]
; Line 107
    mov ecx, DWORD PTR __$ArrayPad$[ebp]
    xor ecx, ebp
    call    @__security_check_cookie@4
    mov esp, ebp
    pop ebp
    ret 0
_tfuuuuuuu ENDP
_TEXT   ENDS
END

初始值仍然全局存储。不过请注意每个值在内部都有一个名称,并且为数组中的每个值生成了2次移动指令。创造这些名称和明确的移动操作是导致代码生成时间如此漫长的原因。
现在是gcc 4.8.5版本:
    .file   "MyLBP.c"
    .section    .rodata
    .align 32
.LC0:
    .long   0
    .long   1080082432
    .long   547853659
    .long   1071734525
    .long   508238255
    .long   1071602032
    .long   2595749041
    .long   -1076305010
    .long   3223218337
    .long   -1075470558
    ...
    .text
    .globl  tfuuuuuuu
    .type   tfuuuuuuu, @function
tfuuuuuuu:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $1238416, %rsp
    movl    %edi, -1238404(%rbp)
    leaq    -1238400(%rbp), %rax
    movl    $.LC0, %ecx
    movl    $1238400, %edx
    movq    %rcx, %rsi
    movq    %rax, %rdi
    call    memcpy                       ;   <--------------  call to memcpy
    movl    -1238404(%rbp), %eax
    cltq
    movq    -1238400(%rbp,%rax,8), %rax
    movq    %rax, -1238416(%rbp)
    movsd   -1238416(%rbp), %xmm0
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   tfuuuuuuu, .-tfuuuuuuu
    .ident  "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-16)"
    .section    .note.GNU-stack,"",@progbits

与其生成每个值的显式复制指令,gcc只是调用memcpy将全局数据中的值复制到本地数组中,因此生成初始化代码的速度更快。
所以故事的寓意是,MSVC在初始化局部变量时非常低效。
此外,正如评论中所指出的那样,这是一个已确认的错误,将在VS 2019中修复。

1
有趣的见解。根据这段代码,执行时间也应该受到这两个额外移动指令的影响,因此每个“const”数组也应该用“static”限定以提高效率(如果没有不这样做的理由),我是对的吗? - Benny K
1
@BennyK 对于较小的数组可能没有关系,但对于这个大小的const数组来说肯定会有影响。这样做也有助于避免堆栈溢出,因为这个数组会在堆栈上占用约1.2MB的空间。 - dbush
关于“在VS 2019中需要修复的错误”:如果我理解@MichaelChourdakis在今天早些时候从这里迁移过来的聊天中的最后一条评论,他已经在VS 2019中尝试过它并遇到了同样的问题,我想我们将不得不等待VS 2021。 - Benny K
@BennyK 不需要等太久,只需等待Visual Studio 2019 v16.1即可。目前已经发布了Preview 3版本,预计一两周内正式发布。 - niemiro
可能有些偏题,但关于 static 存储在数据段中是“通常”的说法 - 你能给我一个例子(或谷歌关键字),表明 static 不存储在数据段中吗?我一直被告知无论如何 static 都存储在数据段中。 - ik1ne

29
无论是`const`还是非`static`的局部函数,都必须在函数调用并到达声明点时构造。编译器在运行时需要花费时间生成执行该操作的代码,当初始化程序非常长时可能会很麻烦。
相比之下,这种形式的`static`变量可以在可执行文件的某个位置放置其初始值,而不需要运行时启动。
如果你真的看到了编译时间上的巨大差异(特别是1.2MB并不是非常多的数据),那么这似乎是编译器的一个QoI问题。但这两段代码本质上是不同的,对于那些注定要“在栈上”生存的庞大的初始化程序,通常要避免它们。

7
根据“仿佛”规则,编译器应该将const double Arr[600 * 258];编译为static const double Arr[600 * 258];,因为构造函数和析构函数都是微不足道的。但实际上这并非必须的。 - Serge Ballesta
没错,@SergeBallesta,但问题在于OP实际观察到的两种选择的编译器运行时非常不同。它可能会产生等效的结果,但显然它确实在做一些不同的事情。 - John Bollinger
7
根据原帖,非静态版本编译需要约 10 分钟,而静态版本只需半秒钟。无论是否遵循"as-if"规则,Visual Studio 明显对每个版本采取了不同的处理方式。 - John Bode
1
@SergeBallesta 如果它不是 static,在调用嵌套函数时,&Arr 可以相等吗? - Barmar
就是这个。知道有个原因,但想不起来了! - Lightness Races in Orbit

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