【问题标题】:Efficient passing of std::vectorstd::vector 的高效传递
【发布时间】:2010-12-18 18:47:28
【问题描述】:

当 C++ 函数接受 std::vector 参数时,通常的模式是通过 const 引用传递它,例如:

int sum2(const std::vector<int> &v)
{
   int s = 0;
   for(size_t i = 0; i < v.size(); i++) s += fn(v[i]);
   return s;
}

我相信这段代码在访问向量元素时会导致双重取消引用,因为 CPU 应该首先取消引用 v 以读取指向第一个元素的指针,而需要再次取消引用该指针才能读取第一个元素。我希望在堆栈上传递向量对象的浅表副本会更有效。这种浅拷贝会封装一个指向第一个元素的指针和大小,该指针引用的内存区域与原始向量相同。

int sum2(vector_ref<int> v)
{
   int s = 0;
   for(size_t i = 0; i < v.size(); i++) s += fn(v[i]);
   return s;
}

类似的性能,但通过传递随机访问迭代器对可以实现的便利性要小得多。 我的问题是:这个想法有什么缺陷?我希望聪明人应该有充分的理由接受支付向量引用的性能成本,或者处理迭代器的不便。 p>

编辑:根据下面的评论,如果我只是将建议的 vector_ref 类重命名为 slicerange,请考虑这种情况。目的是使用语法更自然的随机访问迭代器对。

【问题讨论】:

  • 你真的对比过两种情况下生成的机器码吗?还是测量性能?
  • 你有没有检查过它在组装时确实如此?我也不认为迭代器对的约定来自性能考虑,它是设计决策(实际上,boost 更倾向于迭代器范围对象的更方便的概念,即使我还没有研究它们是如何工作的)。
  • 我认为您的 cmets 实际上没有任何意义。引用是对象的别名。因此,访问对对象的引用并不像访问原始对象那样昂贵。这种略带煽动性的谩骂是你的猜测,如果你做了两分钟的尽职调查,你就不需要这篇文章了。
  • @Martin:引用是一个带有语法糖的指针。说真的。
  • 引用是不能重新定位的指针(指向不同的对象)。由于此属性,编译器可能能够更有效地优化使用引用而不是指针的代码。但是引用确实与对象本身有很大的不同。

标签: c++ stl vector arguments


【解决方案1】:

我相信这段代码在访问向量元素时会导致双重取消引用

不一定。编译器非常聪明,应该能够 eliminate common subexpressions. 他们可以看到运算符 [] 不会更改“指向第一个元素的指针”,因此他们不需要让 CPU 在每次循环迭代时从内存中重新加载它.

【讨论】:

  • 好的,因此编译器理论上可以将每次调用此方法的双重取消引用次数减少到 1。确定的 0 不是更好吗?
  • 根据内存访问计算您的成本,而不是取消引用的数量。在您提出的“浅拷贝”中,您将在调用者而不是被调用者中进行内存访问,因此最终结果(充其量)是相同的。事实上,如果编译器不内联函数,它可能会更糟,因为你传递给它一个更大的参数。
【解决方案2】:

你的想法有问题的是你已经有了两个非常好的解决方案:

  • 按原样传递向量,通过值(编译器通常会消除副本)或通过 (const) 引用,并相信编译器会消除双重间接,或者
  • 传递一个迭代器对。

当然,您可以争辩说迭代器对是“不那么自然的语法”,但我不同意。对于习惯了 STL 的任何人来说,这都是非常自然的。它非常高效,并为您提供了使用标准算法或您自己的函数处理范围所需的确切内容。

迭代器对是一种常见的 C++ 习语,阅读您的代码的 C++ 程序员会毫无问题地理解它们,而他们会对您自制的向量包装器感到惊讶。

如果您真的对性能有偏执,请传递这对迭代器。如果语法真的让您感到困扰,请传递向量并相信编译器。

【讨论】:

    【解决方案3】:

    这个想法有什么缺陷?

    简单:这是过早的优化。替代方案:接受vector&lt;int&gt; const&amp; 并使用迭代器或将迭代器直接传递给函数。

    【讨论】:

    • 当代码比朴素版本更简单时是否过早优化?当我们可以创建与迭代器具有相同性能的更自然的语法时,为什么还要使用迭代器?
    • 因为您可能只是通过值或常量引用传递了原始迭代器,并信任编译器来优化您的代码。在许多情况下,编译器能够消除双重间接。而在它无法做到这一点的情况下,性能差异几乎肯定不会真正重要
    • 一对迭代器已经是“你想要的浅拷贝”了。此外,这是过早的优化,因为您可能会在错误的地方寻找来提高性能。先衡量,再采取行动。
    • @shojtsy:您可以检查v[i]int* p = &amp;v[0]; ... p[i] 之间是否存在任何性能差异并报告回来。 :)
    【解决方案4】:

    你说得对,这里有一个额外的间接性。如果编译器(在链接时代码生成的帮助下)将其优化掉,这是可以想象的(尽管这会令人惊讶)。

    您提出的建议有时称为切片,它在某些情况下被广泛使用。虽然,总的来说,我不确定这是否值得冒险。您必须非常小心使您的切片(或其他人的切片)无效。

    请注意,如果您对循环使用迭代器而不是索引,那么您只会取消引用几次(调用begin()end())而不是n 次(索引到向量中)。

    int sum(const vector<int> &v)
    {
       int s = 0;
       for (auto it = v.begin(); it != v.end(); ++it) {
           s += fn(*it);
       }
       return s;
    }
    

    (我假设优化器会将end() 调用提升到循环之外。您可以明确地执行此操作以确定。)

    传递一对迭代器而不是容器本身似乎是 STL 的习惯用法。这会给你更多的通用性,因为容器的类型可能会有所不同,但所需的取消引用的数量也会有所不同。

    【讨论】:

      【解决方案5】:

      按值传递,除非您确定按引用传递会提高性能。

      当您按值传递时,可能会发生复制省略,这将导致类似甚至更好的性能。

      戴夫在这里写过:

      http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/

      【讨论】:

      • 我强烈反对。这不仅仅是性能优化的问题。如果一个方法不改变向量的内容,那么我总是主张传递一个vector const&。由于复制省略取决于调用您的方法的上下文,我仍然认为在大多数情况下传递引用是正确的选择。
      • 如果它不改变向量,按值传递也很好。来自我的 +1。
      • 有趣。我将不得不更仔细地阅读。尽管有标题,但看起来他专注于返回,而不是参数传递,所以我不确定它是否适用于这样的情况。
      • 查看了这篇文章。据我了解,复制省略仅适用于编译器可以确定您不再需要原始对象时。这不是这里的情况。 sum2 的调用者也可以继续使用原始向量进行其他计算,因此它不能被“窃取”到这个函数中。
      • 如果你真的想复制,它似乎建议按值传递,而不是传递引用并从中复制。
      【解决方案6】:

      没有双重取消引用,因为编译器可能会将真正的指针作为参数传递给向量,而不是指向指针的指针。您可以简单地尝试一下并检查 IDE 的反汇编视图,了解幕后实际发生的情况:

      void Method(std::vector<int> const& vec) {
       int i = vec.back();
      }
      
      
      void SomeOtherMethod() {
        std::vector<int> vec;
        vec.push_back(1);
        Method(vec);
      }
      

      这里发生了什么?向量在堆栈上分配。第一个推回翻译成:

      push        eax  // this is the constant one that has been stored in eax
      lea         ecx,[ebp-24h] // ecx is the pointer to vec on the stack
      call        std::vector<int,std::allocator<int> >::push_back
      

      现在我们调用 Method(),传递向量 const&:

      lea         ecx,[ebp-24h] 
      push        ecx  
      call        Method (8274DC0h) 
      

      不出所料,指向向量的指针是作为引用传递的,只不过是永久取消引用的指针。现在在 Method() 内部,再次访问向量:

      mov         ecx,dword ptr [ebp+8] 
      call        std::vector<int,std::allocator<int> >::back (8276100h)
      

      向量指针直接从栈中取出并写入ecx。

      【讨论】:

      • 为此,编译器必须更改函数签名,这似乎不太可能。更改签名会破坏函数的调用者。如果您有高级链接时代码生成,这是可能的,但我不确定是否有那么高级的实现。
      • 我检查过了。没有双重取消引用。
      • 向量包含一个指向实际数据的指针,所以传递一个指向向量的指针确实是传递一个指向指针的指针。第二次取消引用发生在对 back() 的调用中。如果您改为传递迭代器,那么(在大多数实现中,您将直接传递指向向量中包含的数据的指针,从而消除了间接级别。
      • 现在我终于明白你所说的双重取消引用是什么意思了。我不认为张贴者的意思是,因为即使在访问堆栈上的向量时,这种双重取消引用总是会发生。
      • Sebastian,在你的最后一段代码中,第一个解引用是 mov,第二个是在解引用存储为向量对象字段的指针时在 back 方法内,不是吗?
      猜你喜欢
      • 2012-08-07
      • 2021-08-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-09-17
      相关资源
      最近更新 更多