我在包括Effective C ++在内的很多地方都读到,最好将数据存储在堆栈中,而不是作为数据的指针。
我可以理解用小对象做这个,因为新的和删除调用的数量也减少了,这减少了内存泄漏的机会。此外,指针可以占用比对象本身更多的空间。
但是对于大型对象,复制它们会很昂贵,将它们存储在智能指针中是不是更好?
因为对于大对象的许多操作,很少有对象复制,这是非常昂贵的(我不包括getter和setter)。
答案 0 :(得分:4)
现实情况是,这是微观优化。您应该编写代码以使其可读,可维护且健壮。如果您担心速度,可以使用分析工具来测量速度。你发现需要花费更多时间的东西,然后你才会担心速度优化。
一个对象显然应该只存在一次。如果您制作复制成本昂贵的对象的多个副本,则会浪费时间。你也有同一个对象的不同副本,这本身并不是一件好事。
“移动语义”避免了在您没有真正希望复制任何内容而只是将对象从此处移动到那里的情况下进行昂贵的复制。谷歌为它;理解这一点非常重要。
答案 1 :(得分:4)
让我们专注于效率。不幸的是,没有一个适合所有人的东西。这取决于你的优化。有一种说法,总是优化常见的情况。但是常见的情况是什么?有时答案在于了解您的软件设计。有时它甚至在高级别提前都是不可知的,因为您的用户会发现您没有预料到的新方法。有时您会扩展设计并揭示新的常见案例。因此,基于这种用户端知识和手中的分析器,优化(尤其是微优化)几乎总是最好地应用于后见之明。
对于常见情况,您通常可以有几次非常好的先见之明,那就是当您的设计强制它而不是响应它时。例如,如果您正在设计类似std::deque
的类,那么您强制将常见用例写入用法设为push_fronts
和push_backs
而不是插入到中间,因此要求为您提供有关优化的前瞻性。常见的情况嵌入到设计中,并且设计不可能想要有任何不同。对于更高级别的设计,您通常不那么幸运。即使在您事先了解广泛常见情况的情况下,了解导致速度减慢的微观指令也经常被错误猜测,即使是专家也没有分析器。因此,任何开发人员在考虑效率时应该首先感兴趣的是分析器。
但如果您遇到使用分析器的热点,请参考以下提示。
大多数当时,如果你有任何最大的微观热点将与内存访问有关。因此,如果你有一个大对象只是一个连续的块,所有成员都可以在一个紧密的循环中访问它,它将有助于提高性能。
例如,如果你有一个4分量数学向量的数组,你可以按照严格的算法顺序访问,那么如果他们像这样连续,你通常会走得更远,更好:
x1,y1,z1,w1,x2,y2,x2,w2...xn,yn,zn,wn
...使用像这样的单块结构(所有在一个连续的块中):
x
y
z
w
这是因为机器会将这些数据提取到一个缓存行中,该缓存行将具有相邻的向量'当它在内存中紧密堆积并且连续存在时,它内部的数据就像这样。
如果你在这里用std::vector
之类的东西代表每个单独的4分量数学向量,你可以非常快地减慢算法的速度,其中每个单独的数学成分将数学成分存储在内存中可能完全不同的位置。现在,您可能会在每个向量中发生缓存未命中。此外,您还需要为其他会员付费,因为它是一个可变大小的容器。
std::vector
是" 2-block"当我们将它用于数学4向量时,通常看起来像这样的对象:
size
capacity
ptr --> [x y z w] another block
它还存储了一个分配器,但为了简单起见,我省略了它。
另一方面,如果你有一个很大的" 1-block"在那些紧密的,性能关键的循环中只能访问其某些成员的对象,那么最好将它变成一个" 2-block"结构体。假设您有一些Vertex
结构,其中访问最多的部分是x / y / z位置,但它也有一个不太常用的相邻顶点列表。在这种情况下,最好将其提升并将相邻数据存储在内存中的其他位置,甚至可能完全在Vertex
类本身之外(或仅仅是指针),因为您的常见情况是性能关键算法不访问该数据将能够访问单个缓存行中附近的更多连续顶点,因为顶点将更小并指向内存中其他地方很少访问的数据。
当快速创建和销毁对象时,您还可以更好地在连续的内存块中创建每个对象。每个对象的单独内存块越少,它通常就越快(因为这些东西是否在堆或堆栈上进行,分配/解除分配的块将会更少)。
到目前为止,我一直在谈论更多关于连续性而不是堆栈与堆,因为堆栈与堆的关系更多地与客户端使用对象而不是对象有关。设计。当您设计对象的表示时,您不知道它是在堆栈还是堆上。你知道的是它是否会完全连续(1个街区)或不是(多个街区)。
但是,如果它不是连续的,那么它自然至少部分存在于堆中,如果将成本与硬件堆栈相关联,堆分配和解除分配可能会非常昂贵。但是,您可以通过使用高效的O(1)固定分配器来减轻此开销。它们提供了比malloc
或free
更特殊的用途,但我建议不要关注堆栈与堆的区别,更多关于对象内存布局的连续性。
最后但并非最不重要的一点是,如果你要复制/交换/移动物体很多,那么它们越小,它就会越便宜。因此,您可能希望有时将指针或索引排序到大对象,例如,而不是原始对象,因为即使是类型T的移动构造函数,其中sizeof(T)是一个大数字,复制/移动也会很昂贵
所以移动构建类似" 2-block" std::vector
这里不连续(它的动态内容是连续的,但是这是一个单独的块)并将其庞大的数据存储在一个单独的内存块中实际上比移动构建像#&34更便宜1块" 4x4矩阵是连续的。这是因为如果对象只是一个大的内存块而不是一个指向另一个的指针的小内存块,那么就没有廉价的shallow copy
这样的东西。出现的一个有趣的趋势是复制成本低廉的物品搬运成本高昂,复制成本昂贵的物品移动成本低廉。
但是,我不会让复制/移动开销影响您的对象实现选择,因为客户端总是可以添加一个间接级别,如果他需要一个特定的用例来复制和移动。当您设计内存布局类型的微效率时,首先要关注的是连续性。
优化规则是:如果您没有代码或没有测试或没有分析测量,请不要这样做。正如其他人明智地建议的那样,您最关心的问题始终是生产力(包括可维护性,安全性,清晰度等)。因此,不要将自己陷入假设的假设情景中,首先要做的是编写代码,测量它两次,如果你真的必须这样做就改变它。最好关注如何正确设计界面,这样如果你必须改变任何东西,它只会影响一个本地源文件。
答案 2 :(得分:0)
你所说的基本上是正确的。但是,移动语义减轻了对很多情况下对象复制的担忧。