【问题标题】:speed of references in C++C++中的引用速度
【发布时间】:2009-05-25 00:31:04
【问题描述】:

我一直在从事一个项目,并试图找到执行时间大幅放缓的根源,并将其缩小到我设法从逻辑中优化的单一方法。问题是我的解决方案涉及使用引用,这使得代码的另一部分运行得非常缓慢......我想回答的问题是为什么当地图是引用而不是引用时,内部循环需要更长的时间来评估局部变量?

这是优化之前的旧方法:

// old method: create an empty map, populate it
// and then assign it back to the path object later
map<int,float> screenline_usage; 

for (int i=0; i<numCandidates; ++i)
{
  // timing starts here. 
  map<int, float>& my_screenline_usage =
    path->get_combined_screenline_usage(legnr, stop_id);
  map<int, float>::iterator it = my_screenline_usage.begin(); 
  for (; it != my_screenline_usage.end(); ++it) 
    screenline_usage[it->first] += usage * it->second; 
  // timing ends here, this block evaluated 4 million times for overall execution time of ~12 seconds
}

// This function call is evaluated 400k times for an overall execution time of ~126 seconds
path->set_zone_screenline_usage(access_mode, zone_id, screenline_usage); 

// TOTAL EXECUTION TIME: 138 seconds. 

优化后的新方式:

// new method: get a reference to internal path mapping and populate it
map<int, float>& screenline_usage =
  path->get_zone_screenline_usage(access_mode, zone_id);
screenline_usage.clear();

for (int i=0; i<numCandidates; ++i)
{
  // timing starts here
  map<int, float>& my_screenline_usage =
    path->get_combined_screenline_usage(legnr, stop_id);
  map<int, float>::iterator it = my_screenline_usage.begin(); 
  for (; it != my_screenline_usage.end(); ++it) 
    screenline_usage[it->first] += usage * it->second; 
  // timing ends here, this block evaluated 4 million times for overall execution time of ~76 seconds
}

// New method... no need to assign back to path object (0 seconds execution :)
// TOTAL EXECUTION TIME: 76 seconds (62 second time saving) ... but should be able to do it in just 12 seconds if the use of reference didn't add so much time :(

以下是从该代码调用的相关子例程:

// This is the really slow routine, due to the copy assignment used. 
void set_zone_screenline_usage(int access_mode, int zone_id,
  map<int,float>& screenline_usage)
{
  m_container[access_mode][zone_id] = screenline_usage; 
}

map<int,float>& get_zone_screenline_usage(int access_mode, int zone_id)
{
  return m_container[access_mode][zone_id]; 
}

注意:计时信息适用于单次运行,其中上述代码被评估大约 400k 次。计时是使用我为访问 RDTSC 时间戳计数器而构建的一些类完成的(是的,我知道 TSC 表示时间戳计数器),numCandidates 的平均值为 10,放入 screenline_usage 映射的平均元素数为 25。


更新:首先感谢所有参与其中的人。我认为最终这与 C++ 引用完全无关,更多的是与缓存一致性有关。我已经用一个vector&和一个实现为成员变量map的散列函数替换了上面的优化代码

// newest method: get a reference to internal path mapping (as vector) and populate it 
// map<int,int> m_linkNum_to_SlNum declared in header and populated in constructor. 
vector<float>& screenline_usage = path->get_zone_screenline_usage(access_mode, zone_id);

for (int i=0; i<numCandidates; ++i)
{
  // timing starts here
  map<int, float>& my_screenline_usage =
    path->get_combined_screenline_usage(legnr, stop_id);
  map<int, float>::iterator it = my_screenline_usage.begin(); 
  for (; it != my_screenline_usage.end(); ++it) 
    screenline_usage[m_linkNum_to_SlNum[it->first]] += usage * it->second; 
  // timing ends here, this block evaluated 4 million times for overall execution time of ~9 seconds
}

// Newest method... again no need to assign back to path object (0 seconds execution :)
// TOTAL EXECUTION TIME: just 9 seconds (129 second time saving) ... this is even better than using a locally constructed map which took 12 seconds in the inner loop :)

在我看来,鉴于向量不是本地的,而是一个连续的内存块,并且散列函数 (m_linkNum_to_SlNum) 是本地成员变量,这种方法导致代码/数据能够适合缓存,而不必去主内存获取数据,从而显着提高速度。非常感谢根据这些发现得出的其他结论。

【问题讨论】:

  • 你必须整理一下——旧方法需要 12 秒还是 126 秒?您是在谈论传递参考还是返回参考?
  • 如果不是分配 screenline_usage 参数(此后无论如何都没用),您将使用旧方法获得更好的计时,而是将其与成员交换:screenline_usage.swap(get_zone_screenline_usage(access_mode, zone_id)) ;
  • 12s 是所有 400K 运行的总和,还是所有 400K 运行的平均值?你如何衡量?
  • 我们不是编译器,我们不需要#ifdefs。只需将两个版本并排展示给我们,作为单独的实现。没有交错和混合在一起。
  • 为什么在 i 甚至没有被使用的时候迭代它?这可能会无缘无故地花费大量时间。将 usage * it->second 与 numCandidates 相乘并将其添加到地图中。希望你明白我的意思。

标签: c++ optimization reference map timing


【解决方案1】:

也许您的 C++ 编译器能够为本地地图内联一些代码,但当地图是参考时则不行。

【讨论】:

  • 如果是这种情况,那么您可以通过使用 /FAcs (MSVC) 或 -c -g -Wa,-a,-ad (GCC) 进行编译来查看它。
  • 这似乎是最合理的解决方案,但是当我尝试使用 /FAcs 标志时,我无法得出结论(主要是因为我不是汇编大师)。我已经确定了有问题的行,并且生成的 asm 在偏移量方面存在一些小的差异(即“mov eax,DWORD PTR [-208+ebp]”变为 -224+ebp 供参考)唯一的重大变化是不是局部变量版本中的“lea ecx, DWORD PTR [-336+ebp]”在参考版本中变成了“map mov ecx, DWORD PTR [-124+ebp]”
【解决方案2】:

您的问题不清楚。你的意思是问,为什么通过引用传递映射比通过值传递更快?如果是这样,答案很简单:按值返回地图意味着复制整个地图,而复制大地图是一项非常昂贵的操作。

另一方面,如果您的问题是:为什么获取对现有地图的引用并填充它比制作新地图更快,那么一个假设是它与

 screenline_usage[it->first] += usage * it->second; 

由于path->get_zone_screenline_usage内的map中已经存在key [it->first],那么这只是一个简单的更新操作,不需要分配内存。但是,如果 screenline_usage 是一个空映射,那么每次访问一个新的 [it->first] 意味着它首先必须从堆中分配一个新的映射节点,这很昂贵。

【讨论】:

  • 实际上,空局部变量映射的评估速度明显快于参考,而我试图理解的正是这种差异。我已更新问题以反映这一点。
  • 好吧,与其做更多的猜测,我最好的建议是连接一个采样分析器,看看慢速算法的紧密循环在哪里花费时间。
【解决方案3】:

如果我正确理解您的问题,您的意思是插入到本地堆栈分配的映射 比插入到您通过引用检索的现有堆分配的映射 快得多。

有几种可能会影响性能。不过,我怀疑这与 C++ 参考性能有什么关系。

第一种可能性是,通过将screenline_usage 更改为引用,您“本质上”是在检索指向现有对象的指针。对象的实际实例可能不是map&lt;int,float&gt;,它可以是从 map 继承的任何东西。例如,它可以是一个为其定义了自定义比较器函数的地图。或具有线程同步逻辑的子类。由于您不知道从对m_container[access_mode][zone_id] 的调用中得到什么类型,因此您很可能会得到一个在插入时性能不佳的子类。 (顺便说一下,您可以在调试器中看到返回的类型。)然而,通过创建一个空的 map&lt;int,float&gt;,您可以保证该类型是实际的映射,而不是子类。

假设您正在获取一个实际的地图实例。

第二种可能性是,在您使用的特定 STL 风格中,map::clear() 函数不能有效地清除用于维护关联索引的内部数据结构。我记得, stl::map 使用一些复杂的内部散列和桶结构来提供高效的插入和检索功能。但是,尚不清楚调用 clear() 时会发生什么。因此,screenline_usage.clear() 可能会导致插入的地图效率低于您从空地图开始的情况。

最后,让我们假设我错了,这两种可能性都不是。有一种简单的方法可以确定差异是否是使用参考变量的结果。您可以尝试在堆上分配一个新映射并将其分配给代码中的引用,如下所示:

map<int, float>& screenline_usage = new map<int,float>();

这将帮助您确定插入现有地图与新地图是否存在差异,或者是否确实是 screenline_usage 是影响性能的参考。顺便说一句,不要忘记释放这个堆分配的映射,否则最终会导致内存泄漏。

【讨论】:

  • 你是对的。但我想提供一个示例来评估 OP 问题中引用(如果有的话)的性能差异。
  • 只是为了纠正这里的一些错误信息:stl::map 是一个有序容器,通常实现为红黑树。您在上面描述的是一个哈希表,或者 TR1 中的 unordered_map。
  • 我不认为您的“第一种可能性”逻辑是有效的。 std::map 不是接口,它的所有功能都不是虚拟的。比较器是其类型(模板参数)的一部分,不受继承影响。对 std::map 的引用可能是对较大类型的基类子对象的引用,但这与性能无关,因为在任何一种情况下,对象上调用的数据结构和方法都是相同的,因为不可能没有通过引用的多态行为。
【解决方案4】:

引用通常在幕后以指针的形式实现。一个例外情况是,如果为它们分配了一个临时值,那么值的生命周期将延长到引用的生命周期;本质上,引用就是对象。所以,这取决于:

get_combined_screenline_usage

按引用或按值返回。如果是通过引用,那么引用方式可能会更快。如果是按值,那么旧方式和新方式本质上是一样的,假设编译器做了返回值优化。

在任何一种情况下,编译器所做的其他优化(例如内联)可能会掩盖这两种选择的效果;实际上,在不知道您的慢速部分到底是哪条线的情况下,这有点像猜谜游戏。

我建议尝试获取更精细的信息,并将其缩小到确切需要这么长时间的行,这会使优化变得容易得多。

(注:

map<int, float>::iterator it = my_screenline_usage.begin(); 
for (; it != my_screenline_usage.end(); ++it) 

如果写成这样可能会更高效

for (map<int, float>::const_iterator it(my_screenline_usage.begin()), end(my_screenline_usage.begin()); it != end; ++it)

)

【讨论】:

    【解决方案5】:

    我试图弄清楚的问题是为什么当地图是参考时,内部循环需要更长的时间来评估?

    我猜想花费很长时间的是对 get_zone_screenline_usage: 的调用,而花费很长时间的原因是特定元素在 m_container 中尚不存在,因此必须创建并插入在它可以被退回之前。

    【讨论】:

      【解决方案6】:

      根据我的更新,我认为这很可能是缓存一致性问题,而不是 c++ 参考问题。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-04-28
        • 2010-10-15
        • 2012-05-27
        • 1970-01-01
        • 1970-01-01
        • 2022-11-03
        • 1970-01-01
        • 2021-08-11
        相关资源
        最近更新 更多