全局变量对性能的影响(C,C++)

21

我目前正在开发一个非常快的算法,其中一部分是极快的扫描器和统计功能。

在这个过程中,我追求任何性能上的优势,并且对保持代码的“多线程友好”也很感兴趣。

现在有一个问题:

我注意到将一些非常频繁访问的变量和数组放入“全局变量”或“静态局部变量”(它们做的事情相同)中可以获得可衡量的性能提升(在+10%范围内)。

我正试图理解其中的原因,并找到一个解决方案,因为我更希望避免使用这些类型的分配。

请注意,我不认为差异来自“分配”,因为在堆栈上分配几个变量和小型数组几乎是瞬时的。我相信差异来自于“访问”和“修改”数据。

在这个搜索过程中,我找到了 stackoverflow 上的这篇旧帖子:

C++ performance of global variables

但我对那里的答案非常失望。很少有解释,大多数都是关于“你不应该这样做”的抱怨(嘿,那不是问题!),而且非常粗略的陈述,比如“它不会影响性能”,这显然是不正确的,因为我正在使用精确的基准测试工具来测量它。

如上所述,我正在寻找一个解释,并且,如果有的话,解决这个问题的方法。到目前为止,我觉得计算本地(动态)变量的内存地址的代价比全局变量(或局部静态变量)要高一些。也许像 ADD 操作差异之类的东西。但这并不能帮助找到一个解决方案......


你是在比较每次重新计算值所需的时间和从全局变量中检索它所需的时间吗?是的,这样做总是更快的。但没有理由认为全局变量比本地变量更快。我不确定这里的问题是什么。 - Cody Gray
我正在比较使用相同代码收集统计信息所需的时间,但一种将结果存储/更新到本地结构中,另一种使用全局(或本地静态)结构。在其他条件相等的情况下,“本地静态”毫无疑问胜出。 - Cyan
1
没有看到实际代码很难做出任何判断。我们甚至不知道你要替换的是什么机制。 - Alex B
1
@Cyan:你有对你提供的那个特定示例进行基准测试吗?它显示了10%的性能差异吗?如果没有,那么这不是一个有用的例子。请反汇编真实代码,查看访问变量的部分,看看它是否可以合理地解释你运行时间的10%。如果不是,那么可能存在一些微妙的连锁反应效应,这可能与你的真实代码有些特定关系。如果你不能提供一个显示该问题的示例,除非原因非常明显,否则除了你之外的任何人都很难分析其性能。在这里似乎并不是这样的 :-) - Steve Jessop
问题:你是否正在编写一个分析器? - Mike Dunlavey
显示剩余2条评论
4个回答

9

这真的取决于您的编译器、平台和其他细节。但是,我可以描述一个全局变量更快的场景。

在许多情况下,全局变量位于一个固定的偏移量上。这使生成的指令可以直接使用该地址。(类似于MOV AX,[MyVar]之类的操作。)

然而,如果您有一个相对于当前堆栈指针或类或数组成员的变量,则需要进行一些计算,以获取数组的地址并确定实际变量的地址。

显然,如果您需要在全局变量上放置某种互斥锁以使其线程安全,则几乎肯定会失去任何性能提升。


我完全同意你的说法:互斥锁只会减慢一切。我甚至尝试在函数开始时分配几个表格并选择一个“空闲的”表格;虽然它能够运行,但是和局部变量一样存在性能惩罚,因此这是毫无用处的复杂性。 - Cyan
“需要一些数学知识” - 通常来说,对于全局或局部静态变量,地址可以是一个常量修正值。对于自动变量,地址将是应用于当前堆栈指针的常量偏移量。根据CPU提供的寻址模式,这可能会或可能不会实际影响性能。 - Steve Jessop
@Steve:是的,这就是我说它取决于编译器和平台的原因。请注意,我是从非静态局部变量的角度考虑的。我认为大多数静态局部变量的存储方式与全局变量类似。 - Jonathan Wood
1
@Jonathan:抱歉,我不是想反驳你,只是想扩展一下。此外,如果优化器注意到它经常在做sp+187,并且这可能会浪费时间,那么它可以将sp+187的值缓存到寄存器中,或者在例程的一部分中移动sp(如果操作系统不介意的话)。这些都是提问者在得出你的例子适用于这种情况之前需要检查的事情。 - Steve Jessop
@Steve:是的,为了达到这个优化级别,应该生成编译器产生的汇编语言转储。 - Jonathan Wood
显示剩余7条评论

7

如果局部变量是POD类型,它们可以是完全免费的。您可能会使用太多堆栈变量或其他基于对齐方式的原因来溢出缓存行,这些原因非常特定于您的代码片段。我通常发现非局部变量会显著降低性能。


你的说法在大多数“正常”情况下是正确的,但在这里我正在测试非局部变量的优势差异。因此,这不是“如果发生”的问题,因为它确实会发生。问题是“为什么”。 - Cyan
1
@Cyan:我认为我可能已经提出了一个答案,比如缓存行溢出。请务必阅读整个答案。 - Puppy
普通旧数据,例如整数等。 - George
1
不要生气!是的,我已经看了你的回答,并且我已经同意分配没有区别的事实。现在你关于缓存行的评论对我来说不是很清楚。在堆栈中创建的数据量(约3K)远低于L1缓存大小(约32K),因此应该完全适合L1缓存。我不知道如何确保它是否缓存行对齐(即地址是64的倍数),也不知道它是否会有任何区别。实际上,我已经进行了一些测试(通过使其中一个表稍微变大或变小),但似乎并没有帮助。 - Cyan
变量很重要,请不要开玩笑。 - user15307601

1

静态分配的速度很难被超越,虽然10%的差异相对较小,但可能是由于地址计算造成的。

但是,如果你想要速度, 你在评论中提到的例子while(p<end)stats[*p++]++;显然适合展开:

static int stats[M];
static int index_array[N];
int *p = index_array, *pend = p+N;
// ... initialize the arrays ...
while (p < pend-8){
  stats[p[0]]++;
  stats[p[1]]++;
  stats[p[2]]++;
  stats[p[3]]++;
  stats[p[4]]++;
  stats[p[5]]++;
  stats[p[6]]++;
  stats[p[7]]++;
  p += 8;
}
while(p<pend) stats[*p++]++;

不要指望编译器会为你做这件事。它可能能够理解,也可能无法理解。

还有其他可能的优化方法,但这取决于你实际要做什么。


谢谢Mike。好建议。事实上,我已经在做展开循环了。这就是为什么我说这只是一个“例子”,因为完整的代码有点更复杂,并且对这个讨论没有任何有用的贡献。 - Cyan
@Cyan:脑海中浮现的问题是- 为什么要使用间接数组?设置有多频繁更改?这是否适合预编译?类似这样的东西。 - Mike Dunlavey
@Mike:有趣的观点。你所说的“间接数组”是什么?预编译是一条不错的优化路线。这就是为什么我喜欢模板的想法,它是一种预编译参数的方法(只要它具有有限数量的值)。 - Cyan
@Cyan:在这种情况下,您正在通过首先索引另一个数组“index_array”并循环遍历该数组来索引“stats”。这意味着您只想增加“stats”的子集(因为显然顺序无关紧要)。如果设置“index_array”仅在低频率下完成,则还可以通过简单地打印出一个函数来增加“stats”的所需成员,即时编译和链接dll,动态加载它,并快速执行增量操作。这就是我所说的,你真正想做什么? - Mike Dunlavey
好的;index_array是针对您的示例特定的,但不反映我正在处理的代码。在我的情况下,我们从一个缓冲区开始,该缓冲区被定义为char*和长度。然后我们遍历缓冲区,在其中计算char值的数量。因此,这些值介于0和255之间。 - Cyan
显示剩余2条评论

1
如果你有类似以下的内容:
int stats[256]; while (p<end) stats[*p++]++;

static int stats[256]; while (p<end) stats[*p++]++;

你并没有在比较同样的东西,因为在第一个实例中,你没有对数组进行初始化。明确地写出来,第二行等价于

static int stats[256] = { 0 }; while (p<end) stats[*p++]++;

所以为了公平比较,你应该先阅读第一篇

 int stats[256] = { 0 }; while (p<end) stats[*p++]++;

如果编译器已知变量的状态,它可能会推断出更多的东西。

现在,static情况下可能存在运行时优势,因为初始化是在编译时(或程序启动时)完成的。

为了测试这是否弥补了差异,您应该使用静态声明和循环运行相同的函数多次,以查看如果调用次数增加,差异是否消失。

但正如其他人已经说过的那样,最好检查编译器生成的汇编代码,以查看所产生的代码中有哪些有效差异。


好的,进入函数时两个版本都被初始化了。我在示例中没有记录这部分,但它确实以简单的方式开始: " for (=0; i<256; i++) stats[i]=0; " 这对于两个版本来说是相同的,因此没有“初始化”差异。 - Cyan

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