【问题标题】:What's the difference between const array and static const array in C/C++C/C++ 中的 const 数组和静态 const 数组有什么区别
【发布时间】:2019-09-27 04:28:04
【问题描述】:

在 Visual Studio 2015(Win7、x64、Debug 配置)中编译以下代码需要 非常、非常、非常很长时间(即超过 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模式)

【问题讨论】:

  • 评论不用于扩展讨论;这个对话是moved to chat
  • @BennyK:是啊,这意义重大。有趣的。你能使用不同的编译器(MinGW 或 TinyC)吗?您是否可以访问非 Windows 系统进行比较?作为参考,我将您的 MyLBP.c 文件下载到我的 Macbook 并使用 gcc -c 编译,我发现构建时间没有明显差异。
  • @JohnBode : 好吧'我在 Windows 上工作,但你可以在这里看到:chat.stackoverflow.com/rooms/193093/… 有人用 gcc 4.8.5 进行了检查,没有发现任何问题,猜猜这是预期的.. ..(VS 问题)
  • 很难确定,但我怀疑这个错误报告实际上可能涵盖了相同的问题。等待 Visual Studio 2019 v16.1 发布,看看是否还有问题:developercommunity.visualstudio.com/content/problem/407999/…
  • 最近关于这个问题的所有流量来自哪里?它不再在 HNQ 上,最近也没有编辑。

标签: c++ c arrays static constants


【解决方案1】:

声明为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 在初始化局部变量方面效率很低。

此外,如 cmets 中所述,这是一个 confirmed bug,将在 VS 2019 中修复。

【讨论】:

  • 有趣的见解。根据这段代码,执行时间也应该受到那两条额外的移动指令的影响,所以每个const数组也应该用static来限定效率(如果没有理由不这样做的话),我我对吗?
  • @BennyK 对于较小的阵列,这无关紧要,但对于 const 阵列,这个大小肯定是。这样做还有助于避免堆栈溢出,因为这个数组会在堆栈上放置大约 1.2MB。
  • 关于“将在 VS 2019 中修复的错误”:如果我理解 @MichaelChourdakis 在聊天中的最后一条评论,该评论是今天早些时候从这里迁移的 chat.stackoverflow.com/rooms/193093/… 他已经在 VS 2019 中尝试过并得到了同样的问题,估计要等VS 2021了
  • @BennyK 不用,你只需要等到 Visual Studio 2019 v16.1,它已经是 Preview 3,应该在一两周内发布。
  • 可能是题外话,但关于 static '通常' 存储在数据段中 - 你能给我一个例子(或谷歌关键字)static 不存储在数据中部分?我总是被告知static无论如何都存储在数据段中。
【解决方案2】:

const 与否,无论何时输入函数并到达声明,都必须构造非static 函数本地。您的编译器会花时间生成代码以在运行时执行该操作,当初始化程序超长时,这可能会很费力。

相比之下,这种形式的static 可以将其初始值放入可执行文件的某个位置,而无需启动运行时。

如果您真的发现构建时间有很大差异(特别是 1.2MB 并不是那么多的数据),这听起来确实有点像编译器的 QoI 问题,但是两段代码本质上是不同的,对于注定要“在堆栈上”存在的东西的巨大初始化程序通常是要避免的。

【讨论】:

  • 根据 as if 规则,应该允许编译器像 static const double Arr[600 * 258]; 一样编译 const double Arr[600 * 258];,因为 dtor 的 ctor 是微不足道的。根本不需要。
  • 没错,@SergeBallesta,但问题是 OP 实际观察到的两种替代方案的编译器运行时大不相同。它可能会或可能不会产生相同的结果,但显然它正在做不同的事情。
  • @SergeBallesta:根据 OP,非static 版本的构建时间为 10 分钟,而static 版本的构建时间为半秒。不管是不是 As-if 规则,Visual Studio 肯定不会为每个版本做同样的事情。
  • @SergeBallesta 如果不是static&amp;Arr 是否可以在对函数的嵌套调用中相等?
  • 就是这个。知道有原因,但不记得了!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-09-14
  • 2011-12-04
  • 2014-06-25
  • 1970-01-01
  • 2011-09-26
  • 2010-11-11
相关资源
最近更新 更多