C ++中的快速内存访问?

时间:2011-07-20 13:17:55

标签: c++ memory memory-management

在C ++中快速内存访问方面开发游戏时应该考虑什么?

我加载的内存是静态的,所以我应该把一个连续的内存块放进去吗?

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

6 个答案:

答案 0 :(得分:11)

内存性能非常模糊。

我认为您正在寻找的是处理CPU缓存,因为缓存中的访问与主内存中的访问之间存在大约10倍的因素。

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

简而言之:

瞄准地点

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

瞄准可预测性

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

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

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

使用array[i][j] += 1;更改array[j][i] += 1;,效果会有所不同......优化级别较低;)

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

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

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

一个典型的经验法则是通过减小尺寸来订购结构中的项目(使用sizeof)。这是愚蠢的,但效果很好。如果您对大小和路线更了解,请避免漏洞:) 注意:仅对具有大量实例的结构有用...

但是,要注意虚假分享。在多线程程序中,对两个足够接近共享同一缓存行的变量的并发访问成本很高,因为它涉及大量缓存失效和CPU争夺缓存行所有权。

配置文件

不幸的是,这是 HARD 要弄清楚。

如果您正好在Unix上进行编程,Callgrind(Valgrind套件的一部分)可以使用缓存模拟运行,并识别触发缓存未命中的代码部分。

我想还有其他工具,我从来没用过它们。

答案 1 :(得分:6)

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

答案 2 :(得分:1)

我同意先前的陈述。你应该编写你的游戏,然后找出花费的时间并尝试改进。

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

  • 函数指针和虚方法提供了很多设计灵活性,但如果非常频繁地使用它们,你会发现它们比可以内联的东西慢。这主要是因为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)位置。

当然,游戏的整体设计应该推动您的早期决策,而衡量应该指导您的性能改进。如果它阻止您完成“实际工作”或使项目更难理解,那么您不应该试图实施这些要点。但是,这些要点旨在提供一些示例,说明您可能会遇到一些问题,并在实践中介绍可能导致性能问题的一些背景,除了算法复杂性等其他措施。

答案 3 :(得分:0)

在遇到问题之前找到解决方案效率不高。

你最好专注于你的设计,留下这些细节以供日后使用,谁知道,由于良好的整体设计,你最终可能不会遇到任何性能问题。

答案 4 :(得分:0)

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

在结构组织方面,您应该将字节保持在一起,然后将短路组合在一起,依此类推。否则,编译器将浪费内存对齐较小的字节和短路到双字位置。

另一个提示。如果你正在使用一个类,你可以把它放在堆栈上,而不是用新的。

分配它

我的意思是

CmyClass x;

instead of 

Cmyclass px = new CmyClass;
...
delete px;

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

答案 5 :(得分:0)

从缓存读取的地址比从主存储器读取的地址快得多。因此,尽量让你正在阅读的任何地址尽可能接近彼此。

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