在C语言中,函数内使用静态变量能提高程序运行速度吗?

24

我的函数将被调用数千次。如果我想加快它的速度,将局部函数变量更改为静态变量是否有用?我的逻辑是这样的,因为静态变量在函数调用之间是持久存在的,它们只在第一次分配内存,因此,每个后续调用不会再为它们分配内存,并且会更快,因为内存分配步骤没有执行。

另外,如果上述说法是正确的,那么使用全局变量而不是参数来传递信息到每次调用函数,是否会更快?我认为参数空间也会在每次函数调用时分配,以允许递归(这就是递归使用更多内存的原因),但由于我的函数不是递归的,如果我的推理是正确的,那么去掉参数理论上会使它更快。

我知道这些做法都是可怕的编程习惯,但请告诉我它是否明智。我打算尝试它,但请给我您的意见。


14
在进行性能优化之前,不要对代码进行优化! - Mitch Wheat
1
https://dev59.com/im865IYBdhLWcg3wnP2a - jamesdlin
为了实现微不足道的速度提升而进行一般性的糟糕操作,这是一个非常糟糕的想法。如果你可以在被调用数千次的函数中每次调用节省大约10纳秒...那么你已经节省了10微秒的多个倍数,这是微不足道的,除非你正在处理硬实时系统并且时间分片存在关键问题。 - David Thornley
对于所有建议使用分析器的人:我可以在哪里获取一个?具体来说,是针对GCC x86_64 C的。 - salvador p
10个回答

26

局部变量的开销为零。每次调用函数时,参数、返回值等的堆栈已经被设置好了。添加局部变量意味着将一个稍微大一点的数字加到堆栈指针上(这个数字是在编译时计算的)。

此外,由于高速缓存的本地性,局部变量可能会更快。

如果您只调用该函数"数千次"(而不是数百万或数十亿次),那么您应该在运行分析器之后查找算法的优化机会。


关于高速缓存本地性(阅读更多信息): 经常访问的全局变量可能具有时间局部性。它们在函数执行期间也可能被复制到寄存器中,但在函数返回后将被写回内存(缓存),否则将无法被其他任何东西访问;寄存器没有地址。

局部变量通常具有时间和空间局部性(因为它们在堆栈上创建)。此外,它们可以直接“分配”到寄存器中,并且永远不会被写入内存。


3
以现代 CPU 速度为准,"一秒钟一千次" 相当于 "每几百万个周期一次"。 - György Andrasek
2
+1,当然这取决于编译器生成代码的方式。对于智能编译器来说,sub sp, 20sub sp, 24之间的区别根本不存在。 - paxdiablo
1
我并不是很理解栈。我原以为只有函数调用时参数才会被推入栈中,而局部变量则是通过某种形式的动态内存分配来实现的。现在我做了一些研究,对动态和基于栈的内存分配之间的区别有了一定的理解。谢谢 :D - salvador p
根据这个问题的最佳答案:https://dev59.com/e2kv5IYBdhLWcg3wiRWo,似乎通过将函数变量设置为静态变量,可以通过设置较小的堆栈而不是较大的堆栈来节省时间,但会失去内存局部性...那么性能变化是否不确定? - dragonxlwang
1
@dragonxlwang - 我猜你提到的帖子中的变量主要是为了作用域而静态化 - 它们是全局变量,仅在一个函数中使用 - 而不是为了性能。此外,由于编译器知道静态变量是特殊的,将静态变量用作局部变量可能会混淆编译器并导致更慢的代码。这实际上取决于确切的函数、编译器、优化模式和 CPU。一般来说,如果你编写简单、直接的代码(任何语言都可以),编译器会很好地使其快速运行。 - Seth
显示剩余2条评论

12

了解情况的最佳方法是实际运行分析器。这可以简单地执行多个定时测试,使用两种方法并平均结果进行比较,也可以考虑一个完整的分析工具,它会附加到进程并在时间和执行速度上绘制出内存使用情况。

不要随意进行微调代码,因为您有一种直觉认为它会更快。编译器都有略微不同的实现方式,对于一个编译器在一个环境中正确的东西,在另一个配置中可能是错误的。

解决关于更少参数的评论:"内联"函数的过程基本上消除了与调用函数相关的开销。一个小的函数很可能会被编译器自动内联,但您也可以建议内联函数

在另一种语言C++中,即将推出的新标准支持完美转发,并使用rvalue引用进行完美移动语义,从而消除了某些情况下临时变量的需要,这可以减少调用函数的成本。

我怀疑您正在过早地优化,但在发现真正的瓶颈之前,您不应该过于关注性能。


+1 非常明智,抵制猜测的冲动。 :) - Ben Zotto
谢谢!我试了一下,就像我说的那样。我的程序已经有了一个计算它所需时间的代码片段。在静态/全局变量之前,它在60秒内完成的任务现在只需要49秒。我仍然不能说这是个好主意,但这次似乎确实起作用了,给出了一致的结果 :) 我不知道编译器优化或者堆栈也用于函数的局部变量(我还是个新手)。此外,当C++0x到来时,我肯定会研究它的所有功能:我认为rvalue和lambda已经在GCC中了:D。谢谢! - salvador p

4
绝对不是!唯一的“性能”区别在于变量初始化。
    int anint = 42;
 vs
    static int anint = 42;

在第一种情况下,每次调用函数时,整数都将设置为42;在第二种情况下,当程序加载时它将被设置为42。然而,这个差别微不足道,几乎无法察觉。有一个常见的误解,即必须在每次调用时为“自动”变量分配存储空间。但实际上,C语言使用栈中已经分配好的空间来存储这些变量,因此并不需要额外的存储空间。静态变量可能会使程序变慢,因为一些积极的优化对于静态变量不可行。另外,由于局部变量在栈的连续区域中,因此可以更有效地进行缓存。

3
这个问题没有一个标准答案。它会随着CPU、编译器、编译器标志、本地变量的数量、CPU在调用函数之前所做的操作以及月相等因素而有所不同。
考虑两个极端情况:如果你只有一个或几个本地变量,它们可能很容易被存储在寄存器中,而不是分配内存位置。如果寄存器“压力”足够低,甚至可以在不执行任何指令的情况下实现这一点。
在另一个极端情况下,有些机器(例如IBM大型机)根本没有堆栈。在这种情况下,我们通常认为的堆栈帧实际上是在堆上分配的链接列表。正如你可能猜到的那样,这可能非常慢。
当涉及访问变量时,情况有些类似——访问机器寄存器几乎可以保证比分配在内存中的任何东西都要快。另一方面,对堆栈上的变量进行访问可能会非常缓慢——通常需要类似于索引间接访问的操作,这在旧CPU上往往相当缓慢。另一方面,访问全局变量(静态变量也是如此,即使它的名称不是全局可见的)通常需要形成一个绝对地址,一些CPU也会对此进行一定程度的惩罚。
总之:即使是对您的代码进行分析的建议可能是错误的——差异可能非常微小,以至于甚至分析器也不能可靠地检测到它,唯一确定的方法是检查生成的汇编语言(并花费几年时间学习汇编语言,以便在查看时能够说出任何东西)。另一方面,当你处理一个你甚至不能可靠地测量的差异时,它对实际代码速度产生影响的可能性是如此之小,以至于可能不值得费那么多的劲。

2

看起来静态 vs 非静态的问题已经完全解决,但对于全局变量而言还有一些需要注意的问题。通常情况下,全局变量会降低程序的执行速度,而不是提高它。

原因是,变量作用域越小,编译器就越容易进行优化。如果编译器需要在整个应用程序中查找全局变量的使用实例,那么它的优化效果就会变得不那么好。

当引入指针时,情况会更加复杂,比如以下代码:

int myFunction()
{
    SomeStruct *A, *B;
    FillOutSomeStruct(B);
    memcpy(A, B, sizeof(A);
    return A.result;
}

编译器知道指针A和B永远不会重叠,因此可以优化复制。如果A和B是全局的,则可能指向重叠或相同的内存,这意味着编译器必须“保险起见”,这会减慢速度。这个问题通常称为“指针别名”,不仅在内存复制中,还可能发生在许多其他情况下。
参考链接:http://en.wikipedia.org/wiki/Pointer_alias

2
使用静态变量可能会让函数稍微快一点。然而,如果你想让程序支持多线程,这将会导致问题。由于静态变量在函数调用之间是共享的,同时在不同线程中调用该函数将导致未定义的行为。多线程是你未来可能想要做的事情,以真正加速你的代码。
你提到的大部分内容都被称为微观优化。通常,担心这些东西是一个坏主意。它使你的代码更难读懂、维护。它也很有可能引入错误。你可能会在更高的层次上做出更有效的优化。
正如M2tM所建议的那样,运行分析器也是个好主意。看看gprof,这是一个相当容易使用的分析器。

1
您可以始终计时应用程序以真正确定最快的方法。以下是我的理解:(当然,所有这些都取决于处理器的架构)
C函数创建堆栈帧,其中传递的参数和局部变量以及返回指针放置在其中,指向调用者调用该函数的位置。这里没有内存管理分配。通常只是一个简单的指针移动。从堆栈中访问数据也非常快。处罚通常在处理指针时发挥作用。
至于全局或静态变量,它们是相同的......从它们分配在同一内存区域的角度来看。访问这些可能会使用不同的访问方法,这取决于编译器。
您的场景之间的主要区别在于内存占用,而不是速度。

2
这是一个重要的观点 - 只要您的变量没有初始化,分配100个自动变量与分配一个变量的速度一样快。 - caf
需要注意的是,编译器正在“分配”内存,而不是内存管理系统。 - KFro

1

使用静态变量实际上可能会使您的代码显着变慢。静态变量必须存在于内存的“数据”区域中。为了使用该变量,函数必须执行一个加载指令以从主内存中读取,或者执行一个存储指令以写入它。如果该区域不在缓存中,则会损失许多周期。生存在堆栈上的局部变量肯定会有一个在缓存中的地址,甚至可能在CPU寄存器中,根本不出现在内存中。


每次调用该函数时,它都必须检查静态变量是否已经初始化。这是不正确的。在main()运行之前,所有的静态变量都会被初始化(在__start()中)。全局变量也在此时初始化。 - Kevin Vermeer
通常,加载指令将用于堆栈上的本地变量或数据区域中的本地变量。第一次初始化变量是一个好的起点,良好的编码要求使用if-then-else语句。了解编译器/环境是否在程序启动时将该内存清零是一种快捷方式,这是一种冒险、不良的编码风格,但通常可以实现更快的速度。 - old_timer
@dwelch:重点是本地变量可能根本不会出现在主存储器中,它可以被优化(安全地)仅存在于寄存器中。 - SingleNegationElimination
是的,我明白,无论是否加上 static,你都可以得到相同的优化。加上 static 会在 .data 内存中保留一个内存位置用于该变量,但可能永远不会被使用。没有 static 时,有时会在堆栈中保留一个变量位置,但可能永远不会被使用。 - old_timer
我不得不在Linux上运行基准测试以亲自查看,发现静态方式大约慢了20%。我给你点赞,但你值得更多。 - JohnMudd

0
我同意其他人对于剖析以查找这种东西的评论,但一般来说,函数静态变量应该会更慢。如果你想要它们,实际上你真正需要的是全局变量。函数静态变量会插入代码/数据来检查这个东西是否已经被初始化过,这会在每次调用你的函数时运行。

0

性能分析可能看不出差异,但反汇编和知道要查找什么可能会有所帮助。

我怀疑你每个循环只会得到几个时钟周期的变化(平均取决于编译器等)。有时候变化会非常明显,或者明显变慢,并不一定是因为变量的位置已经从堆栈中移动了。假设你在一个2ghz处理器上每个函数调用节省了四个时钟周期,对于10000个调用,大致计算:节省20微秒。相对于您当前的执行时间,20微秒是多还是少?

通过将所有char和short变量转换为int等方法,您可能会获得更好的性能提升。微观优化是一件好事,但需要大量的实验、反汇编、计时代码的执行以及理解较少的指令并不一定意味着更快等知识。

拿出你的具体程序,反汇编涉及的函数和调用它的代码。有静态和无静态两种情况。如果你只获得了一两个指令,而且这是你要做的唯一优化,那么这可能不值得。您可能无法在性能分析中看到差异。例如,缓存行命中的位置变化可能会在代码更改之前显示出来。


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