哪个更快?“结构体向量”还是“多个向量”?

15

解决方案1: 如果我有一个类似于,

class car{ public: int a; string b; bool c;};

我可以建立一个包含200辆车的向量:

std::vector<car>   allcas;  
allcars.resize(200)

在运行时,我只需要执行以下操作:

this_car=allcars[102];

然后....

解决方案2:

我有......

std::vector<int> a; a.resize(200);
std::vector<string>b; b.resize(200);
std::vector<bool> c; c.resize(200);

this_car_a = a[102];
this_car_b = b[102];
this_car_c = c[102];

问题:

哪个更快?

有人有想法吗?提前感谢!


5
更快的是用来做什么?你试过计时吗? - Oliver Charlesworth
6
进行基准测试并查看结果如何?除非这是代码中的瓶颈,否则我建议您选择更合乎逻辑和更安全的解决方案(汽车类别的向量)。 - GWW
3
我不理解为什么会有负评,这其实是一个非常合理的问题,而且我每天都会遇到。在现代处理器上设计数据结构以实现最佳内存访问,可以显著地提高性能。 - Remus Rusanu
2
@Remus:确实如此。但是,原帖并没有提供有关他/她打算如何处理数据的任何信息。因此,目前无法给出具体答案。 - Oliver Charlesworth
2
你的访问模式会是什么样子? - harold
显示剩余3条评论
6个回答

15

如果abc彼此关联且一起构成一个对象,为什么要将它们分开呢?首先要考虑清晰度和易读性。其他任何因素都应该在此之后考虑。此外,我认为v2会更慢一些。向量上的访问更多了。不过我没有计时。对于有关速度的问题,总是要进行计时


5
在进行大量向量化操作时,SOA(数组结构)通常比AOS更有意义。 有时,在可以获得巨大性能提升时,您需要牺牲清晰度。 - Oliver Charlesworth
3
@Oli:当然,你是对的,总会有特殊情况。但是,我感觉这不是其中之一。 ;) Translated: @Oli: 当然,你是正确的,总是有一些特殊情况。但我感觉这个并不是特殊情况之一。;) - Xeo
我完全同意。除非你有非常迫切的需要进行这种“优化”,否则我不会允许第二个示例存在于我的代码库中。 - Rob K
1
虽然这不是一个坏的一般原则,但我想讲一个现实世界的故事。CERNLIB(一个旧的粒子物理数据分析包)支持两种类型的“ntuples”:按行(实际上是结构体向量)和按列(并行数组,使用FORTRAN描述这些东西的方式)。在某些用例中,选择按列的ntuple可以使速度提高数个数量级(主要来自磁盘访问问题(这是当时的大量数据))。 - dmckee --- ex-moderator kitten
@dmckee:当然这取决于访问模式。如果OP选择了一个更复杂的例子,我的答案可能会不同。 ;) - Xeo

14
"向量结构体"相对于"结构体向量"有几个优点:
  • 如果内部循环未使用结构的每个元素,则向量结构体可以节省内存带宽,因为未使用的元素向量不会被加载到缓存中。
  • 更易于矢量化。向量结构体可以通过汇编、内在函数或聪明的编译器启用处理器的向量处理指令来加速内部循环。
另一方面,过度优化是万恶之源:
  • 使用向量结构体更加困难、笨拙和晦涩。
  • 通常直到代码运行之后你才知道性能瓶颈在哪里。那么让你的代码变得更冗长、更脆弱、更困难值得吗?你只有在实际进行性能剖析之后才知道。
  • 向量结构体编程的好处因情况而异。它并不总是能提高速度; 你可能会得到更差的表现。
  • 特别地,如果你的访问模式是随机的(而不是连续的或其他局部化的),那么如果每个缓存行包含多个附近对象的元素,向量结构体组织可能会从内存中加载更多的无用数据...
因此,我的建议是默认使用结构体向量,但要考虑向量结构体作为一种替代方案(即,确保你可以稍后切换,如果你预计顺序/局部访问模式,并且前期不需要花费太多精力)。一旦程序运行起来,您可以对其进行剖析以确定性能关键部分,并在它们最有帮助的地方尝试使用向量结构体和矢量化操作。

请查看Xeo的回答评论,了解并行数组在真实世界中取得的巨大胜利的实例。 - dmckee --- ex-moderator kitten
另一个 vector 的缺点是,向量的长度可能不相等。而使用 structvector 就不会出现这种情况。 - Paul Draper

12

中央处理器喜欢预取。

如果您要以以下模式线性遍历数据...

abcabcacb...

如果你要访问它们,那么从性能方面考虑,方案#1是更好的选择。

aaa...bbb..ccc...

如果你不需要进行线性遍历,或者你没有真正基准测试你的代码并得出结论,认为你需要从这段代码中挤出每一点性能,那么请为了代码可维护性的缘故坚持使用解决方案#1。

然而,如果你要做的是非线性遍历,或者你没有实际进行基准测试并得出需要从代码中挤出每一点性能的结论,那么请为了代码可维护性的缘故坚持使用解决方案#1。

--- 编辑 ---

在多线程环境中,数据的物理布局可能会导致伪共享。实际上,将被不同线程同时访问的数据部分过于靠近可能会引起高速缓存争用,破坏可扩展性。

因此,如果你从一个线程并发地访问a,而另一个线程访问b,则将它们物理上分开并实施解决方案#2可能是值得的。如果另一方面,你访问两个“兄弟”a,那么请坚持使用解决方案#1。

--- 编辑 2 ---

关于这个主题的优秀论述,我强烈推荐Herb Sutter的演讲“Things Your Programming Language Never Told You”,仍然可在以下网址中找到:

https://www.youtube.com/watch?v=L7zSU9HI-6I https://nwcpp.org/talks/2007/Machine_Architecture_-_NWCPP.pdf


我一直在想,如果我的访问模式是abcabcabc...,那么如果我有一个向量的结构体,CPU会预测下一个缓存行应该获取abc,一旦它们被获取,我将拥有更多的热数据abc,因为我有三个缓存行都是热的,而不仅仅只有一个。 - Ben
如果CPU注意到你目前请求的缓存行是连续的,它可能会在你甚至还没有请求时开始获取下一个。这就是“预取”。在你描述的情况下,CPU必须足够聪明以进行一种“三路”预取。今天的CPU是否足够聪明?我不知道。这就是为什么测量性能比预测性能更可靠,尤其是当你开始调整内存布局时。 - Branko Dimitrijevic

2

这真的取决于您希望如何使用您的数据。例如,如果您只想访问一个字段:

car this_car = allcars[12];
cout << this_car.a;

这会导致您创建此_car的副本。在这种情况下,您将不必要地复制字段b和c。当然,您可以通过引用来解决这个问题:

car & this_car = allcars[12];

这可能仍然比直接执行更慢
a = a[12];

然而,如果你想访问类的多个属性,那么最好将它们存储在一起。这样做可以提高性能,因为有“局部性原理”,但这真的取决于编译器、内存管理器等因素。
最终,关于哪种方法的性能更好的答案是:取决于具体情况。这绝对不会成为瓶颈决策,保持属性在单个结构体中对于代码可读性和您自己的理解肯定更好。

2
首先,为了可维护性的原因,将它们分开是一个可怕的想法,这应该是您最关心的问题。
其次,您只是将分配时间(三个分配而不是一个)、释放时间(相同)和破坏缓存局部性的参考(可能会减速)增加了三倍。
第三,唯一的好处是如果您只读取所有汽车中的一个成员,并且很少更改汽车。

我同意分配时间,尤其是缓存未命中的机会增加了,但是解除分配不会使您的进程进入等待状态,并且是一个非常快速的操作。 - Elliott
@Elliott-ReinstateMonica 你确定吗?快速搜索表明情况并非如此。有时候释放内存比分配内存更慢。https://dev59.com/aJjga4cB1Zd3GeqPJF1j - Mooing Duck

1

这取决于结构成员的大小和您的模式访问。一个单例访问是无关紧要的,但是考虑一下,如果您在向量上进行迭代,并且只对成员a感兴趣。结构越宽,适合缓存行的结构条目就越少,您将遇到更多的缓存未命中。将所有a成员分开放在一个向量中可以增加缓存行密度,从而提高性能。这可能非常显著(1.5倍、2倍甚至更多)。

然而,更重要的是专注于代码可维护性,使其易于阅读、调试和重构。代码应该清楚地表达意图。您所询问的这些微小优化应该仅在经过测量的瓶颈时才考虑。获取软件优化食谱的副本。


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