静态变量会影响数据缓存吗?

5

来自C++中的软件优化(第7.1节)

静态数据的优点在于它可以在程序启动之前初始化为所需的值。缺点是即使变量只在程序的一小部分中使用,内存空间在整个程序执行期间都被占用。这使得数据缓存效率降低。

此处使用static适用于C和C++中静态存储期的确切情况。

有人能解释一下为什么(或是否)静态存储期变量的数据缓存效率较低吗?以下是一个具体的比较:

void foo() {
  static int static_arr[] = {/**/};
}
void bar() {
  int local_arr[] = {/**/};
}

我不认为静态数据与其他类型的数据缓存方式有任何区别。在给定的示例中,我认为foo会更快,因为执行堆栈不需要加载static_arr,而在bar中,执行堆栈必须加载local_arr。无论哪种情况,如果这些函数重复调用,static_arrlocal_arr都将被缓存。我错了吗?

@Someprogrammerdude 不是的。这个问题同样适用于C和C++。两个标签都是必需的。如果你对这种情况下static的使用感到困惑,那么请进一步了解详情。 - okovko
静态数据将与任何其他数据完全缓存。我认为,涉及的书籍作者并不真正知道他在谈论什么。 - Johan
源代码是否提供了一个最小化、完整、可复现的示例来说明这个陈述?如果没有,你可以忽略这个陈述。我认为他们可能是在暗示静态数据通常存储在离栈变量较远的地方,但这并不意味着使用静态变量会自动使数据缓存效率降低。 - user7860670
@Johan 我怀疑他本来想说别的,但是就目前而言,这个陈述毫无意义。也许我会给他发电子邮件。他确实一直更新这本书。 - okovko
很遗憾,不能。您可以查看第7.1节的第26页和第27页以获取所有提供的解释材料。好的,我会继续阅读而不费力地考虑那个问题。也许在文本后面他的意思会更清楚。 - okovko
我认为重要的部分是这句话:“缺点是,即使变量仅在程序的一小部分中使用,内存空间也会在整个程序执行期间被占用。” 看起来作者举了一个例子,函数只有很少的使用,所以变量只会在程序的一小部分“存在”,而不是全局存在并一直占用内存。这似乎是一个有效的观点 - 不过应该表述得更清楚些。 - Blaze
3个回答

6
总的来说,Agner Fog通常知道自己在谈论什么。
如果我们将引文放在第7.1节“不同类型的变量存储”中的上下文中,我们就能看出他在该节开头所说的“缓存效率较低”的含义:
数据散布在内存中时,数据缓存效果很差。因此,了解变量的存储方式非常重要。简单变量、数组和对象的存储原理相同。
因此,说static变量的缓存效率较低的想法是,它们存储的内存位置处于“冷”状态(不在缓存中)的可能性比具有自动存储期限的变量存储在栈内存中的可能性更大。
考虑到缓存和分页,数据存储的物理和时间局部性影响性能。

1
将数据局部性的概念应用于静态数据感觉有些尴尬。我的意思是,我认为BSS内存总是被缓存,这样想是否有误?这感觉像是一个概念的误用。堆栈和堆被延迟映射到页面,但BSS是固定大小并且总是加载的。这需要进一步思考。 - okovko
我的意思是,编译器确切地知道何时需要BSS的哪些部分。因此,它肯定会指示CPU将其缓存。难道不是这样工作的吗?我很天真。 - okovko
编译器通常不涉及缓存决策,因为这些决策需要静态和动态知识(程序运行时的实际行为),实际目标 CPU 平台(不同供应商之间存在差异)以及应用程序运行的上下文环境(缓存与其他应用程序和操作系统共享)。 - rustyx
顺便提一下,.bss节用于零初始化的全局存储。未进行零初始化的全局数组将存储在.data节中(如果是const则为.rdata)。无论如何,内存页面将被预先分配,但它们是否在高速缓存中取决于应用程序的总内存使用模式(活动工作集)。 - rustyx
好的,非常感谢您为我澄清了我的误解。现在我对发生的事情更加清楚了! - okovko
显示剩余2条评论

5
rustyx的回答解释了这个问题。局部变量存储在栈上。当函数返回并被下一个函数调用时,栈空间会被释放并重新使用。对于局部变量来说,缓存更加高效,因为同一内存空间会被反复重用,而静态变量则分散在不同的内存地址上,永远不能为其他目的重用。无论静态数据存储在DATA段(已初始化)还是BSS段(未初始化),在这方面没有区别。栈顶空间很可能在程序执行期间保持缓存,并被多次重用。
另一个优点是,有限数量的局部变量可以通过相对于栈指针的8位偏移量进行访问,而静态变量需要32位绝对地址(在32位x86中)或32位相对地址(在x86-64中)。换句话说,局部变量可以使代码更加紧凑,提高代码缓存以及数据缓存的利用率。
// Example
int main () {
  f();
  g();
  return 0;
}

void f() {
   int x; 
   ...
}

void g() {
   int y;  // y may occupy the same memory address as x
   ...
}

我明白了!所以如果 xy 是静态的,那么栈顶就不会被重复使用,程序将使用“冷”数据。这是一个很好的回答,我认为能够让相关手册的作者给出如此清晰的回答真是不可思议。你的手册可能会受益于在第7.1节中包含这个例子。谢谢! - okovko

4
这个声明的意义取决于你如何标点它:
读法1:
静态数据的优点是可以在程序启动之前初始化为期望的值。缺点是内存空间会在整个程序执行期间被占用,即使变量只在程序的一小部分中使用也是如此。所有这些都会使数据缓存效率降低。
这是无意义的。
读法2:
静态数据的优点是可以在程序启动之前初始化为期望的值。
缺点是内存空间会在整个程序执行期间被占用,即使变量只在程序的一小部分中使用也是如此。如果静态变量的存储已分配到不经常交换的页面或很少使用的高速缓存线中,则可能会导致数据缓存效率降低。
在特定情况下,答案是"取决于情况"。
是的,static_arr的初始化是一个仅需操作,因此可以认为是无成本的。
是的,local_arr的初始化在每次调用函数时都会发生,但这可能是以下两种情况之一:
这种初始化是微不足道的;或者
初始化被编译器省略为优化器的一部分
通常情况下,除非您有特定的优化目标,否则最好编写明确说明所需行为的代码,例如:
当变量/数组的值需要在函数的连续调用中保留时,使用静态变量(具有静态存储期的变量)。
当现有值在函数进入或退出时毫无意义时,使用局部变量(严格来说是自动存储期的变量)。
您会发现编译器在几乎所有情况下都会在优化通过之后执行最有效的操作。
特定情况下,静态初始化更好。例如,缓冲区需要动态分配的情况。您可能不想在每次调用时都承担分配/释放成本。您可能希望缓冲区在需要时动态增长,并基于未来操作可能再次需要内存的基础上保持增长。
在这种情况下,变量的实际状态是其分配缓冲区的大小。因此,在函数的进入和退出时,状态很重要,例如:
  std::string const& to_string(json const& json_object)
  {
    static thread_local buffer;               // thread-safe, auto-expanding buffer storage
    buffer.clear();                           // does not release memory
    serialise_to_string(buffer, json_object); // may allocate memory occasionally
    return buffer;
  }

你能解释一下你所说的静态持续时间变量在给定页面上是什么意思吗?据我所知,静态变量从不在惰性映射堆栈/堆上。应该在BSS上,它具有固定的大小并随程序本身一起加载。你应该更新你的答案以更正,或者也许我错了。 - okovko
@okovko 如果操作系统的内存资源负载过重,BSS部分可以被替换掉。这就是我发表“你还有其他问题”的评论的原因。 - Richard Hodges

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