问题 全局变量性能影响(c,c ++)


我目前正在开发一种非常快速的算法,其中一部分是极快的扫描和统计功能。 在这个任务中,我追求任何性能优势。 因此,我也有兴趣保持代码“多线程”友好。

现在提问: 我注意到将一些非常频繁访问的变量和数组放入“全局”或“静态本地”(它们都是相同的),有一个可衡量的性能优势(在+ 10%的范围内)。 我试图理解为什么,并找到解决方案,因为我宁愿避免使用这些类型的分配。 请注意,我不认为差异来自“分配”,因为在堆栈上分配一些变量和小数组几乎是瞬间完成的。我认为差异来自“访问”和“修改”数据。

在这个搜索中,我从stackoverflow找到了这个旧帖子: C ++全局变量的性能

但我对那里的答案感到非常失望。很少解释,大多是咆哮“你不应该这样做”(嘿,这不是问题!)和非常粗略的陈述,如'它不影响性能',这显然是不正确的,因为我用精确测量它基准工具。

如上所述,我正在寻找解释,并且,如果存在,则解决此问题。到目前为止,我已经感觉计算本地(动态)变量的内存地址比全局(或本地静态)花费更多。也许类似ADD操作的差异。但这无助于找到解决方案......


6418
2018-03-06 13:35


起源

您是否正在比较每次重新计算值所需的时间与从全局变量中检索值所需的时间?是的,这总是会更快。但是没有理由全局变量比局部变量更快。我不确定是什么 题 在这儿。 - Cody Gray♦
我正在比较使用相同代码收集统计信息所花费的时间,但是将结果存储/更新到本地结构中,另一个使用全局(或本地静态)结构。 “本地静态”赢得胜利,其他一切都是平等的。 - Cyan
这很难说 什么 没有看到实际的代码。我们甚至不知道您要替换的机制。 - Alex B
不确定“提供源”是否有用,但是,如果一个例子是有用的,那么就说这个:“int stats [256]; while(p <end)stats [* p ++] ++;”和我们将它与几乎相同:“static int stats [256]; while(p <end)stats [* p ++] ++;”在大多数情况下,“静态”版本运行得更快 - Cyan
@Cyan:你有没有对你给出的特定例子进行基准测试?它是否显示10%的性能差异?如果没有,那么这不是一个有用的例子。反汇编真实代码,查看访问变量的部分,看看它是否可以合理地占用运行时的10%。如果没有,那么必须有一些微妙的连锁效应,这可能与您的真实代码有些相关。如果你不能产生一个显示问题的例子,那么除了你的表现非常明显之外,除了你之外,任何人都很难分析它的表现。它似乎不在这里:-) - Steve Jessop


答案:


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

在许多情况下,全局变量处于固定偏移量。这允许生成的指令直接简单地使用该地址。 (有些东西) MOV AX,[MyVar]。)

但是,如果您有一个相对于当前堆栈指针或类或数组成员的变量,则需要一些数学运算来获取数组的地址并确定实际变量的地址。

显然,如果你需要在你的全局变量上放置某种互斥量以保持线程安全,那么你几乎肯定会失去任何性能增益。


6
2018-03-06 13:40



我完全同意你的说法:互斥体只会减慢一切。我甚至尝试分配几个表并在启动函数时选择“free one”;它有效,但与Local Variables具有相同的性能损失;所以这是无用的复杂性 - Cyan
“需要一些数学” - 通常不同之处在于全局或局部静态,地址可能是一个不变的修正。对于自动变量,地址将是应用于当前堆栈指针的常量偏移量。根据CPU提供的寻址模式,这可能会或可能不会实际影响性能。 - Steve Jessop
@Steve:是的,这就是为什么我说这取决于编译器和平台。请注意,我在思考非静态局部变量。我假设大多数静态局部变量的存储方式与全局变量的存储方式类似。 - Jonathan Wood
@Jonathan:对不起,不是试图不同意,只是扩大。此外,如果优化器注意到它正在执行 sp+187 很多,这很可能花费时间,然后它可以缓存价值 sp+187 如果操作系统不介意,在寄存器中,或移动sp作为例程的一部分。这些是提问者在得出你的例子适用于这种情况之前需要检查的事情。 - Steve Jessop
@Steve:是的,对于这种优化级别,编译器生成的汇编语言的转储应该是有序的。 - Jonathan Wood


如果它们是POD类型,那么创建局部变量就可以完全免费。您可能会溢出具有太多堆栈变量的缓存行或其他类似的基于对齐的原因 非常 特定于您的代码。我经常发现非局部变量会显着降低性能。


6
2018-03-06 13:37



你的陈述在大多数“正常”的情况下是正确的,但在这里我是对非局部变量的差异进行基准测试。所以这不是“如果它发生”的问题,因为它确实如此。问题是“为什么”。 - Cyan
@Cyan:我相信我可能已经建议了一个答案,比如缓存行溢出。请看看 整个 回答。 - Puppy
什么是C中的POD类型? - ) - Jens Gustedt
@Jens Gustedt:普通旧数据,例如整数等 - George
不要觉得被冒犯!是的,我已经阅读了你的答案,我已经同意这样一个事实,即分配没有区别。现在你对缓存行的评论对我来说并不是那么清楚。堆栈中创建的数据量(~3K)远低于L1缓存大小(~32K),因此它应完全适合L1缓存。我不知道如何确保它是否是缓存行对齐(即64的地址倍数),也不知道它是否有任何区别。事实上,我已经对此进行了一些测试(通过使其中一个表更大或更小),它似乎没有帮助。 - Cyan


很难打破速度的静态分配,虽然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++]++;

不要指望编译器为你做这件事。它可能会或可能不会弄明白。

其他可能的优化会浮现在脑海中,但它们取决于您实际上要做的事情。


2
2018-03-06 14:39



谢谢迈克。好建议。事实上,我已经在展开了。这就是为什么我说这只是一个“例子”,因为完整的代码有点复杂,并没有为这个讨论带来任何有用的东西。 - Cyan
@Cyan:想到的问题是什么 - 为什么是间接数组?设置多久更改一次?这可能是预编译的候选者吗?像这样的东西。 - Mike Dunlavey
@Mike:有趣的观点。你怎么称呼“间接阵列”?预编译是一种很好的优化路线。这就是为什么我喜欢吸引模板的想法,这是预编译其中一个参数的一种方式(只要它具有有限数量的值)。 - Cyan
@Cyan:在这种情况下你正在编制索引 stats 首先索引另一个数组 index_array 而你正在经历这一切。这意味着你只想增加一个子集 stats (因为显然订单无关紧要)。如果设置 index_array 只能在低频下完成,你也可以通过简单地打印一个函数来增加所需的成员 stats,动态编译和链接一个DLL,动态加载它,并进行递增 真的很快。这就是我的意思,你真的想做什么? - Mike Dunlavey
好; index_array特定于您的示例,但不反映我正在处理的代码。在我的例子中,我们从缓冲区开始,缓冲区被定义为char *和length。然后我们通过缓冲区,计算其中的char值。因此,值介于0到255之间。 - Cyan


如果你有类似的东西

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 因为初始化是在编译时(或程序启动)完成的。

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

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


0
2018-03-06 15:53



好的,两个版本在进入该功能时都被初始化。我没有在这个例子中转录这个部分,但它确实从一个简单的开始:“for(= 0; i <256; i ++)stats [i] = 0;”这两个都是一样的,所以没有“初始化”区别。 - Cyan