静态局部变量能减少内存分配时间吗?

15

假设我有一个单线程程序中的函数,它看起来像这样:

void f(some arguments){
    char buffer[32];
    some operations on buffer;
}

“f”似乎在一些经常调用的循环内部,因此我希望将其尽可能快速。在我看来,每次调用“f”时都需要分配缓冲区,但是如果我将其声明为静态变量,则不会发生这种情况。这个推理正确吗?这是免费的加速吗?仅仅因为这个事实(这是一个简单的加速),优化编译器是否已经为我做了类似的事情?”

11个回答

14

不,这并不是免费的加速。

首先,开始时分配几乎是免费的(因为它仅包括将32添加到堆栈指针中),其次,有至少两个原因可能使静态变量更慢

  • 你会失去缓存本地性。在堆栈上分配的数据已经在CPU缓存中了,所以访问它非常便宜。静态数据分配在内存的另一个区域,因此可能无法被缓存,这将导致缓存未命中,并且您需要等待数百个时钟周期才能从主存储器中获取数据。
  • 你会失去线程安全性。如果两个线程同时执行该函数,它将崩溃和失败,除非放置了锁,以便一次只允许一个线程执行该代码段。那就意味着你会失去拥有多个CPU核心的好处。

因此,这并不是免费的加速。但可能在您的情况下更快(虽然我怀疑)。 因此,请尝试运行、测试并查看在您特定的场景中哪种方法最有效。


我还想补充一点,这样你也会失去可重入性:每次进入函数时,您不再拥有一个清洁的变量(即使对于内置函数来说,这始终如此...)。 - Matthieu M.
静态变量可能更快,因为它们通常分配在与全局变量相同的位置。假设您有一个全局变量和一个被同一代码触及的静态变量;那么相对于全局变量,该静态变量可能会被缓存到本地,因此读取一个变量也可能会将另一个变量读入CPU缓存行中。递归函数、大型非静态数组,尤其是同时使用这两种情况可能会耗尽堆栈,在这种情况下,谨慎使用静态变量是个好主意(这样您的程序就不会崩溃!) - Jody Bruchon
@JodyLeeBruchon,你是在说本地堆栈分配的变量不会被缓存本地化吗? - jalf
@jalf 缓存以 CPU 特定长度的行加载。如果您访问堆栈分配的变量,则其他堆栈变量 可能 被加载到缓存中,使它们“免费”访问。我的意思是相同的效果也适用于静态/全局变量:如果您读取静态或全局变量,则将另一个变量加载到相同的 CPU 缓存行中可能会产生副作用,这样之后就可以“免费”访问,具体取决于它们在内存中的存储方式。这就是为什么像 Cachegrind 这样的工具非常重要的原因:它们会告诉您是否实际上降低了缓存未命中率,这样您就不必猜测了。 - Jody Bruchon
是的,但你的论点是“静态变量可能不会更慢”(这是正确的),而不是“静态变量可能更快”。如果你想要局部性,真正无法超越栈。这是CPU 不断读写的地方。 - jalf
@jalf 不,我的观点是在某些情况下静态变量可能比栈变量更快。谨慎使用静态变量也可以加速栈变量。简化的例子:char x; char foo[8192]; int y; 创建了一个8KB的变量,它可能被分配在栈上的 xy 之间,将 xy 强制分别进入不同的缓存行,从而使得读取 x 不会把 y "免费" 加载到缓存中。使用 static char foo[8192];foo 放入 BSS 中,消除了间隙,并且(可能但不总是)将 xy 放入同一 CPU 缓存行中。 - Jody Bruchon

10

在几乎所有系统上,增加栈上的32个字节几乎不会带来任何代价。但你应该测试一下。对静态版本和本地版本进行基准测试,并发布结果。


+1 建议使用基准测试工具。在验证某个程序是否“更快”或“更慢”时,使用分析器应始终是第一步。 - linuxuser27
我已经从分析中知道在我的特定应用程序中这无关紧要,但是这让我好奇是否会有影响。现在我明白为什么它不应该有影响了。 - pythonic metaphor

9

对于使用堆栈存储局部变量的实现,通常分配涉及推进寄存器(将值添加到其中),例如堆栈指针(SP)寄存器。这个时间非常短暂,通常只有一条指令或更少。

然而,初始化堆栈变量需要更长的时间,但也不多。请查看您的汇编语言清单(由编译器或调试器生成)以获取确切的细节。标准中没有关于初始化变量所需持续时间或指令数的规定。

静态局部变量的分配通常是不同的。一种常见的方法是将这些变量放置在与全局变量相同的区域中。通常,在调用main()之前,该区域中的所有变量都会被初始化。在这种情况下,分配是将地址赋给寄存器或将区域信息存储在内存中的问题。这里没有浪费太多执行时间。

动态分配是执行周期被消耗的情况。但这不在您的问题范围之内。


3

我建议更一般的方法是,如果您有一个需要一些本地变量的函数被多次调用,则考虑将其包装在类中,并使这些变量成为成员函数。如果需要使大小动态化,则可以使用std::vector<char> buffer(requiredSize)代替char buffer[32]。但这比每次循环都初始化数组要昂贵。

class BufferMunger {
public:
   BufferMunger() {};
   void DoFunction(args);
private:
   char buffer[32];
};

BufferMunger m;
for (int i=0; i<1000; i++) {
   m.DoFunction(arg[i]);  // only one allocation of buffer
}

将缓冲区设置为静态的另一个含义是,该函数在多线程应用程序中不安全,因为两个线程可能同时调用它并覆盖缓冲区中的数据。另一方面,在需要使用缓冲区的每个线程中使用单独的BufferMunger是安全的。


+1:既然你提到了,我记得为实现傅里叶变换的一个课程做过类似的事情。 - user180326

3

现在的写法,分配没有成本:32个字节在栈上。唯一需要做的工作是零初始化。

在这里使用本地静态变量不是一个好主意。它不会更快,而且你的函数不能再从多个线程中使用,因为所有调用都共享同一个缓冲区。更不用说本地静态变量的初始化不能保证是线程安全的了。


1
通常情况下,栈变量没有零初始化。 - mmmmmmmm

3
请注意,C++中的块级static变量(与C不同)在首次使用时初始化。这意味着您将引入额外的运行时检查成本。分支可能会使性能变得更糟,而不是更好。(但实际上,应该进行性能分析,正如其他人所提到的那样。)
无论如何,我认为这不值得,特别是因为您将有意放弃可重入性。

2
如果你正在为PC编写代码,那么两种方式都不太可能有任何实际的速度优势。在某些嵌入式系统上,避免所有局部变量可能是有利的。在其他一些系统上,局部变量可能更快。
例如,在Z80上,用于带有任何局部变量的函数设置堆栈帧的代码非常长。此外,访问局部变量的代码仅限于使用(IX+d)寻址模式,该模式仅适用于8位指令。如果X和Y都是全局/静态变量或都是局部变量,则语句“X=Y”可以组装为以下任一项:
; 如果两者都是静态或全局变量:6字节;32个周期 ld HL,(_Y) ; 16个周期 ld (_X),HL ; 16个周期 ; 如果两者都是局部变量:12字节;56个周期 ld E,(IX+_Y) ; 14个周期 ld D,(IX+_Y+1) ; 14个周期 ld (IX+_X),D ; 14个周期 ld (IX+_X+1),E ; 14个周期
这除了设置堆栈帧所需的代码和时间外,还需要100%的代码空间惩罚和75%的时间惩罚!
在ARM处理器上,单个指令可以加载一个位于地址指针的+/-2K内的变量。如果函数的局部变量总计2K或更少,则可以使用单个指令访问它们。全局变量通常需要两个或更多指令才能加载,具体取决于它们存储的位置。

1

使用gcc编译器,我确实看到了一些加速:

void f() {
    char buffer[4096];
}

int main() {
    int i;
    for (i = 0; i < 100000000; ++i) {
        f();
    }
}

还有时间:

$ time ./a.out

real    0m0.453s
user    0m0.450s
sys  0m0.010s

将缓冲区更改为静态:

$ time ./a.out

real    0m0.352s
user    0m0.360s
sys  0m0.000s

1
什么,没有优化?那不是一个有意义的基准测试。当然,启用优化后,整个对 f 的调用很可能会被省略。 - Konrad Rudolph
我刚刚注意到这个问题被标记为C++,所以我再次使用g++编译而不是gcc。有趣的是,非静态版本运行时间约为0.5秒,而静态版本仍然运行在约0.35秒。 - Colin
哦耶...优化!使用O2标志,两个版本都在0.002秒内运行。 - Colin
时间差异并不显著。那段时间可能会因任务切换、其他程序运行和其他操作系统开销而发生变化。优化或概念验证所浪费的时间比从程序销售中收回的成本(以开发人员时间计算)更高。 - Thomas Matthews
好的,请给我一点信任。我已经运行了多次测试,以确保时间稳定。 - Colin
显示剩余2条评论

1
根据变量的具体作用和使用方式,加速几乎没有什么效果。因为(在x86系统上),对于所有本地变量,堆栈内存是同时分配的,只需要一个简单的单个func(sub esp,amount)即可,因此只有另一个堆栈变量消除了任何增益。唯一的例外是非常大的缓冲区,在这种情况下,编译器可能会插入_chkstk来分配内存(但如果您的缓冲区那么大,您应该重新评估您的代码)。编译器无法通过优化将堆栈内存转换为静态内存,因为它不能假定函数将在单线程环境中使用,而且它会干扰对象构造函数和析构函数等。

1
如果函数中有任何局部自动变量,堆栈指针都需要进行调整。调整所需的时间是恒定的,并不会因声明的变量数量而变化。如果你的函数没有任何局部自动变量,那么你可能会节省一些时间。
如果静态变量被初始化,就会有一个标志位来确定变量是否已经被初始化。检查标志位需要一些时间。在你的例子中,变量未被初始化,所以可以忽略此部分。
如果你的函数有可能被递归调用或从两个不同的线程中调用,应该避免使用静态变量。

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