【问题标题】:Performance: vector of classes or a class containing vectors性能:类的向量或包含向量的类
【发布时间】:2009-04-23 00:15:25
【问题描述】:

我有一个包含许多双精度值的类。这存储在一个向量中,其中类的索引很重要(它们是从其他地方引用的)。这个类看起来像这样:

类向量

class A
{
  double count;
  double val;
  double sumA;
  double sumB;

  vector<double> sumVectorC;
  vector<double> sumVectorD;
}

vector<A> classes(10000);

需要尽可能快地运行的代码是这样的:

vector<double> result(classes.size());
for(int i = 0; i < classes.size(); i++)
{
  result[i] += classes[i].sumA;
  vector<double>::iterator it = find(classes[i].sumVectorC.begin(), classes[i].sumVectorC.end(), testval);
  if(it != classes[i].sumVectorC.end())
    result[i] += *it;
}

替代方案是代替一个巨大的循环,将计算分成两个单独的循环,例如:

for(int i = 0; i < classes.size(); i++)
{
  result[i] += classes[i].sumA;
}
for(int i = 0; i < classes.size(); i++)
{
 vector<double>::iterator it = find(classes[i].sumVectorC.begin(), classes[i].sumVectorC.end(), testval);
  if(it != classes[i].sumVectorC.end())
    result[i] += *it;
}

或将类的每个成员存储在一个向量中,如下所示:

向量类

vector<double> classCounts;
vector<double> classVal;
...
vector<vector<double> > classSumVectorC;
...

然后操作为:

for(int i = 0; i < classes.size(); i++)
{
  result[i] += classCounts[i];
  ...
}

哪种方式通常更快(跨 x86/x64 平台和编译器)?前瞻和缓存行是这里要考虑的最重要的事情吗?

更新

我在这里进行线性搜索(即查找)而不是哈希映射或二进制搜索的原因是 sumVectors 非常短,大​​约 4 或 5 个元素。分析显示哈希映射较慢,二分查找稍慢。

【问题讨论】:

  • 不,绝对不是,我正在优化聚类算法中的内部循环,并且想知道这两种方法之间的权衡。
  • @Khaki 实现似乎很容易构建两者并进行良好的旧测量:-)

标签: c++ performance arrays stl


【解决方案1】:

由于这两种变体的实现似乎很容易,我会构建这两个版本并分析它们以找到最快的版本。

经验数据通常胜过推测。

【讨论】:

    【解决方案2】:

    附带问题:目前,最内层循环中的find()classes[i].sumVectorC 的所有元素进行线性扫描,直到找到匹配值。如果该向量包含许多值,并且您没有理由相信 testVal 出现在向量的开头附近,那么这将很慢 - 考虑使用具有更快查找速度的容器类型(例如 std::map 或其中之一非标准但通常实现的hash_map 类型)。

    作为一般准则:在低级实现优化之前考虑算法改进。

    【讨论】:

      【解决方案3】:

      正如lothar 所说,您真的应该测试一下。但是要回答您的最后一个问题,是的,缓存未命中将是这里的主要问题。

      此外,您的第一个实现似乎会遇到编码时的加载命中存储停顿,但我不确定 x86 上的问题有多大(这在 XBox 360 和 PS3 上是个大问题)。

      【讨论】:

        【解决方案4】:

        看起来优化 find() 将是一个巨大的胜利(肯定知道配置文件)。根据不同的大小,除了用另一个容器替换向量之外,您还可以尝试对 sumVectorC 进行排序并使用 lower_bound 形式的二进制搜索。这会将您的线性搜索 O(n) 变成 O(log n)。

        【讨论】:

          【解决方案5】:

          如果您可以保证 std::numeric_limits&lt;double&gt;::infinity 不是一个可能的值,请确保数组在最后使用虚拟无限条目进行排序,然后手动编码查找,以便循环条件是单个测试:

           array[i]<test_val
          

          然后是相等测试。

          那么您知道在未找到的情况下,查看值的平均数量为 (size()+1)/2。当然,如果搜索数组变化非常频繁,那么保持排序的问题就是一个问题。

          当然,关于 sumVectorC 或 A 的其余部分,您不会告诉我们太多,因此很难确定并给出真正好的建议。例如,如果 sumVectorC 永远不会更新,那么可能会找到一个非常便宜的哈希(例如强制转换 ULL 和位提取),它在 sumVectorC 值上完美,适合 double[8]。然后开销是位提取和 1 与 3 或 6 的比较

          另外,如果你对 sumVectorC.size() 有一个合理的界限(你提到了 4 或 5,所以这个假设似乎不错),你可以考虑使用聚合数组,甚至只是一个 boost::array&lt;double&gt; 并添加你自己的动态大小例如:

          class AggregatedArray : public boost::array<double>{
             size_t _size;
             size_t size() const {
                return size;
             }
             ....
             push_back(..){...
             pop(){...
             resize(...){...
          };
          

          这消除了对 sumVectorC 分配的数组数据的额外缓存行访问。

          在 sumVectorC 很少更新的情况下,如果找到一个完美的哈希(在您的哈希算法类别之外)相对便宜,那么当 sumVectorC 更改时,您可以从中获利。这些小的查找可能是有问题的,算法复杂性通常是无关紧要的——它是占主导地位的常量。这是一个工程问题,而不是理论问题。

          除非您可以保证小地图在缓存中,否则几乎可以保证使用 std::map 会产生大约 130% 的性能下降,因为树中的每个节点都将位于单独的缓存行中

          因此,每次搜索不是访问 (4 次 1+1 次 2)/5 = 1.2 个缓存行(前 4 个在第一个缓存行中,第 5 个在第二个缓存行中,您将访问 (1 + 2 times 2 + 2 乘以 3) = 9/5) + 1 对于树本身 = 每次搜索 2.8 个缓存线(1 是根节点的 1 个节点,2 个节点是根节点的子节点,最后 2 个节点是根节点的孙子节点,加上树本身)

          所以我预测使用 std::map 需要 2.8/1.2 = 233% 的 sumVectorC 有 5 个条目

          这就是我所说的:“这是一个工程问题,而不是理论问题。”

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-06-11
            • 1970-01-01
            • 2018-03-05
            • 2013-03-28
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多