c++中的快速内存访问

Fast memory access in C++?

本文关键字:内存 访问 c++      更新时间:2023-10-16

在开发一款基于c++的快速内存访问游戏时我应该考虑什么?

我加载的内存是静态的,所以我应该把它放在一个连续的内存块中,对吗?

另外,我应该如何组织结构内部的变量来提高性能?

内存性能非常模糊。

我认为你正在寻找的是关于处理CPU缓存,因为在缓存中访问和在主存中访问之间有大约10个因子。

关于缓存背后机制的完整参考,您可能希望阅读lwn.net上Ulrich Drepper的这一系列优秀文章。

简而言之:

瞄准位置

你不应该在内存中跳来跳去,所以(如果可能的话)尽量把要一起使用的项目组合在一起。

以可预测性为目标

如果您的内存访问是可预测的,那么CPU可能会为下一个工作块预取内存,以便在完成当前块后立即或很快可用。

典型的例子是数组上的for循环:

for (int i = 0; i != MAX; ++i)
  for (int j = 0; j != MAX; ++j)
    array[i][j] += 1;

array[j][i] += 1;改变array[i][j] += 1;,性能变化…在低优化水平;)

编译器应该捕捉到那些明显的情况,但有些情况更隐蔽。例如,使用基于节点的容器(链表,二叉搜索树)而不是基于数组的容器(向量,一些哈希表)可能会降低应用程序的速度。

不要浪费空间…谨防虚假分享

尝试打包你的结构。这与对齐有关,您可能会由于结构中的对齐问题而浪费空间,这会人为地增加结构大小并浪费缓存空间。

典型的经验法则是通过减小尺寸来排列结构中的项(使用sizeof)。这是愚蠢的,但效果很好。如果你对大小和对齐有更多的了解,只要避免洞:)注意:只对有很多实例的结构有用…

但是,要小心虚假共享。在多线程程序中,并发访问两个足够接近的变量来共享同一条缓存线是昂贵的,因为它涉及大量的缓存无效和CPU争夺缓存线的所有权。

概要文件

不幸的是,这是很难弄清楚。

如果你碰巧在Unix上编程,Callgrind (Valgrind套件的一部分)可以与缓存模拟一起运行,并识别触发缓存失败的代码部分。

我想还有其他的工具,我只是从来没用过。

你不在乎。这样的事情很可能是最小性质的微优化。首先让它工作,如果它太慢了,然后找出哪些部分是慢的,并优化那些(提示:它可能是你调用库等的方式,而不是内存访问)。

我同意前面的说法。你应该先写好你的游戏,然后找出时间应该花在哪里,并尝试着去完善它。

然而,本着提供一些可能有用的[可能分散对实际问题的注意力:-)]建议的精神,您可能会发现一些常见的陷阱:

  • 函数指针和虚拟方法提供了很大的设计灵活性,但是如果它们被非常非常频繁地使用,你会发现它们比内联的东西要慢。这主要是因为CPU很难通过函数指针对调用执行分支预测。在c++中,一个很好的缓解方法是使用模板,它可以在编译时为您提供类似的设计灵活性。

    这种方法的一个潜在的缺点是内联会增加代码的大小。好消息是,您的编译器可以决定是否内联,并且它可能比您做出更好的决定。在许多情况下,你的优化器知道你特定的CPU架构,并可以做出一些适当的猜测。

  • 在频繁访问的数据结构中避免间接访问。

例如:

struct Foo
{
   // [snip] other members here...
   Bar *barObject;  // pointer to another allocation owned by Foo structure
};

有时创建的内存布局效率不如下面的:

struct Foo
{
   // [snip] other members here...
   Bar barObject;  // object is a part of Foo, with no indirection
};

这可能听起来很傻,在大多数情况下你不会注意到任何区别。但总的想法是,避免"不必要的间接"是件好事。不要特意去做这件事,但这是要记住的。

这种方法的一个潜在缺点是它可能使您的Foo对象不再整齐地适合缓存…

  • 沿着前面两个项目的线…在c++中,STL容器和算法可以生成一些非常高效的目标代码。在<algorithm>的情况下,传递给各种算法的函子可以很容易地内联,帮助您避免不必要的指针调用,同时仍然允许通用例程。在容器的情况下,STL可以适当地在列表节点等内部声明类型参数T的对象,这有助于避免数据结构中不必要的间接。

  • 是的,内存访问可以产生影响…一个例子是在大图像中的像素之间循环。如果一次处理一列图像,可能比一次处理一行图像更糟糕。在最常见的图像格式中,(x, y)处的像素通常与(x +1, y)处的像素相邻,而(x, y)处的像素通常距离(x, y+1)(宽度)像素。

  • 与第二项相同,有一次在一个图像处理项目中工作(尽管是在今天的标准下的旧硬件上),我发现即使是确定像素位置所涉及的算法也会导致速度变慢。例如,如果您正在处理坐标(x, y),那么直观的做法是在buf[y * bytes_per_line + x]处引用像素。如果您的CPU在进行乘法运算时速度很慢,而您的映像又很大,那么这可能会增加。在这种情况下,最好一次循环一行,而不是继续计算(x, y)在各种坐标下的位置。

当然,你的游戏的整体设计应该推动你的早期决策,测量应该指导你的性能改进。如果这些要点妨碍了你完成"真正的工作"或使项目更难理解,你就不应该刻意去执行这些要点。但是,这些要点旨在提供一些示例,说明您可能在哪里看到一些问题,并介绍一些上下文,说明除了算法复杂性等其他度量之外,在实践中可能导致性能问题的因素。

在出现问题之前找到解决方案是没有效率的。

你最好专注于你的设计,把这些细节留到以后,谁知道呢,也许你最终会因为一个好的整体设计而永远不会有任何性能问题。

从缓存中读取地址要比从主存中读取快得多。因此,尽量让你正在阅读的所有地址保持在尽可能近的位置。

例如,在构建链表时,您最好为所有节点(可以或多或少按顺序放置)使用一个大块,而不是每个节点使用一个malloc(这可能会使您的数据结构碎片化)

内存使用不必是连续的。如果你能将所使用的内存大小减半,那可能会有所帮助。

在结构体组织方面,应该把字节放在一起,然后把short放在一起,等等。否则,编译器将浪费内存,将较小的字节和short对齐到双字位置。

是另一个技巧。如果你正在使用一个类,你可以把它放在堆栈上,而不是用new来分配它。

我的意思

CmyClass x;
instead of 
Cmyclass px = new CmyClass;
...
delete px;

* *编辑当您在c++堆中调用new()或malloc时,堆有时会在几个周期内返回一个新的内存块,有时则不会。当你在堆栈上声明一个类时,你仍然消耗了相同数量的内存(可能比这更复杂),但类只是被"推"到堆栈上,不需要函数调用。永远。当函数退出时,堆栈被清理,堆栈收缩。