变量零初始化会降低性能吗?

3

我正在修正静态分析(MISRA-C-2012)违规情况,其中一个规则(规则9.3)要求在使用变量前进行初始化。

例如:

void bar_read(int * array)
{
    printf("array[1]: %u\n",array[1]);    
}

void bar_write(int * array)
{
    array[1]=1;    
}

int main(void)
{
    #define FOO_SIZE 12
 #ifdef MISRA_VIOLATION_DISABLED
    int foo[FOO_SIZE]  = {0}; //ok
 #else
    int foo[FOO_SIZE]; //violation
 #endif
    bar_read(foo);
    bar_write(foo);
    bar_read(foo); 

    return 0;
}

我的一些同事宣称他们在处理大数组时取消了变量初始化 foo[FOO_SIZE] = {0};,因为这会降低性能,这让我感到困惑。

据我所知,零初始化的变量在编译时被放置在bss段中,不会对运行时性能产生影响。

我可能错了吗?这可能取决于编译器吗?是否有任何优化使其成立?


int foo[FOO_SIZE] = {0};int foo[FOO_SIZE]; 之间的差异应在编译时解决。通常最好进行零初始化,因为编译器可能不会为您执行此操作。foo 是本地变量,因此它不会使用 .bss 部分,而是将位于堆栈上,无论是否进行零初始化。虽然具体实现可能因编译器而异,但不应有任何显着的运行时性能影响。 - h0r53
2
@h0r53 这不是真的。int foo[FOO_SIZE] = {0}; 添加了运行时初始化,这会影响性能。我在一些架构上看到它被编译成某种内部 memset 调用的变体。 - Eugene Sh.
公平的说,除非你绝对确定不会发生无效的数组访问,否则我认为初始化值得花费看似微小的性能代价。 - h0r53
@h0r53 当然,除了性能之外可能还有其他考虑因素。 - Eugene Sh.
补充以上评论:我是一个嵌入式程序员,喜欢在决定是否将本地数组初始化为0之前考虑它们的用途。 a)如果我知道我会立即使用它进行sprintf(),这将为我零终止字符串,我可以节省运行时开销并且不初始化数组:char buff [FOO_SIZE]; b)如果我使用该数组进行任何读取,并且必须确保变量以已知值开始,则会投资运行时初始化时间:char buff [FOO_SIZE] = {0}; - EmbeddedGuy
3个回答

6

在函数内部定义一个未使用 staticextern 关键字声明的数组 int foo[FOO_SIZE],它具有自动存储期,意味着每次执行到它所在的块时都会“创建”(为其保留内存),并且在该块的执行结束时会“销毁”(释放内存)。由于函数可以递归调用,自动变量的内存不能实现在 .bss 段中保留。通常情况下,它们使用堆栈。

此外,即使它们位于 .bss 段中,在 C 模型中它们的生命周期仍然是每当它们所在的块开始和结束时始终如一。因此,如果它们被初始化,则必须在每次新生命周期开始时重新初始化。将它们存储在 .bss 段中在这方面不会节省任何东西。

另外,如果 .bss 段进行了零初始化,那么这并不是免费的。每当操作系统提供内存来支持零初始化段时,必须清除该内存。


除此之外:“由于函数可以递归调用”,通过选择静态分析,可以将一个函数识别为不可重入并使用固定的内存位置。尽管每次调用仍然可能会产生对象初始化,但是这种方法在一个固定的位置上是可行的。 - chux - Reinstate Monica

4

没有使用 static 关键字定义的函数内变量具有自动存储期。这些变量通常在进入作用域时在堆栈上创建。

这意味着如果这些变量被初始化,则运行时会有初始化成本。

只有具有静态存储期的变量,即在文件范围内声明或使用 static 关键字声明的变量,通常在 .data 中明确初始化或在未初始化情况下在 .bss 中定义。

在使用 gcc 4.8.5 编译此代码并使用 -O0 定义 MISRA_VIOLATION_DISABLED 时,将产生以下额外的代码:

subq    $48, %rsp
leaq    -48(%rbp), %rsi
movl    $0, %eax
movl    $6, %edx
movq    %rsi, %rdi
movq    %rdx, %rcx
rep stosq

-O2 会有什么不同吗? - tstanisl
如果其他函数被简化为声明,前两行略有不同,但其余部分相同。 - dbush

2

auto变量在运行时实例化,因此任何初始化也必须在运行时发生,这将产生一些性能损失-具体损失的程度取决于编译器和优化级别。

话虽如此,你的同事不应该在不做以下两个操作之一的情况下删除初始化:

  • 证明没有代码会尝试在分配之前读取任何数组元素;

  • 量化性能损失显示其超出某些要求或规范-例如,“要求X指示此操作必须在100毫秒内完成,但使用初始化需要120毫秒”等。

编辑

例如,我将代码更改为将初始化程序定义为构建命令的一部分,然后使用clock库函数进行了一些简单的仪器测量:

#include <stdio.h>
<strong>#include <time.h></strong>

void bar_read( int *array )
{
  printf( "array[1]: %d\n", array[1] );
}

void bar_write( int *array )
{
  array[1] = 1;
}

int main( void )
{
  <strong>clock_t start = clock();</strong>
#ifndef FOO_SIZE
#define FOO_SIZE 2000
#endif

#ifndef INIT 
#define INIT
#endif

  int foo[FOO_SIZE] INIT ; // will expand to nothing or ={0} depending on build command
  bar_read( foo );
  bar_write( foo );
  bar_read( foo );

  <strong>clock_t end = clock();</strong>
  printf( "operation took %lu clocks (%f seconds)\n", end-start, (double)(end-start)/CLOCKS_PER_SEC );
  <strong>return (int)(end-start);</strong>
}

我可以进行带初始化和不带初始化的构建,看看运行所需时间是否有差异:

$ gcc -o init -std=c11 -pedantic -Wall -Werror -DFOO_SIZE=2000 -DINIT="" init.c
$ ./init
array[1]: -1898976766
array[1]: 1
operation took 39 clocks (0.000039 seconds)

$ gcc -o init -std=c11 -pedantic -Wall -Werror -DFOO_SIZE=2000 -DINIT="={0}" init.c
$ ./init
array[1]: 0
array[1]: 1
operation took 53 clocks (0.000053 seconds)

我让main返回程序主要部分使用的时钟数。然后,我编写了一个shell脚本来构建带有和不带有数组初始化器的代码,运行每个版本100次(比我们需要的样本大,但运行时间不长),并对这些运行取平均值(整数平均值,但足以说明问题):

#!/bin/bash

INIT_PARAMS=( '""' '"={0}"' )
let runs=100

for INIT in "${INIT_PARAMS[@]}"
do
  cmd="gcc -o init -std=c11 -pedantic -Wall -Werror -DFOO_SIZE=2000 -DINIT=${INIT} init.c"
  echo $cmd
  eval $cmd
  let x=0
  for i in `seq 1 1 $runs`
  do
    ./init >/dev/null # suppress output from init itself
    let x=$x+$?
  done
done

我得到的输出是:

$ . init_test.sh 
gcc -o init -std=c11 -pedantic -Wall -Werror -DFOO_SIZE=2000 -DINIT="" init.c
Average clocks per run for INIT="" is 24
gcc -o init -std=c11 -pedantic -Wall -Werror -DFOO_SIZE=2000 -DINIT="={0}" init.c
Average clocks per run for INIT="={0}" is 33

因此,在声明的同时初始化一个包含2000个int元素的数组会受到明显的惩罚,平均需要9个时钟周期(0.000009秒),增加了37%,并且没有进行任何优化。提高优化级别可以减少这种成本(可能),但无法完全消除它。


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