【问题标题】:Efficient accumulate高效累积
【发布时间】:2013-11-08 23:03:27
【问题描述】:

假设我有字符串向量,我想通过 std::accumulate 连接它们。

如果我使用以下代码:

std::vector<std::string> foo{"foo","bar"};
string res=""; 
res=std::accumulate(foo.begin(),foo.end(),res,
  [](string &rs,string &arg){ return rs+arg; });

我可以肯定会有临时对象构造。

this 的回答中,他们说 std::accumulate 的效果是这样指定的:

通过初始化累加器 acc 来计算结果 初始值 init 然后用 acc = acc + *i 或 acc = 修改它 binary_op(acc, *i) 对于 [first,last) 范围内的每个迭代器 i 顺序。

所以我想知道避免不必要的临时对象构造的正确方法是什么。

一个想法是用这种方式改变 lambda:

[](string &rs,string &arg){ rs+=arg; return rs; }

在这种情况下,我认为我强制有效地连接字符串并帮助编译器(我知道我shouldn't)省略了不必要的副本,因为这应该等同于(伪代码):

accum = [](& accum,& arg){ ...; return accum; }

因此

accum = & accum;

另一个想法是使用

accum = [](& accum,& arg){ ...; return std::move(accum); }

但这可能会导致类似:

accum = std::move(& accum);

这在我看来非常可疑。

编写此代码的正确方法是什么以最大程度地减少不必要地创建临时对象的风险?我不仅对 std::string 感兴趣,我很高兴有一个解决方案,它可能适用于任何实现了复制和移动构造函数/赋值的对象。

【问题讨论】:

  • 你应该只为连接创建一个函数...
  • 一个丑陋的替代方法是使用指向本地std::string 变量的指针作为累加器,并且可能事先使用reserve。虽然现在,accumulate 仅简化为 for_each,并不比下面大卫的解决方案好多少。
  • 看来std::accumulate总是制作临时副本。如果这是不可接受的,那么您需要使用其他东西。
  • @MarkRansom std::accumulate 不会制作临时副本;它调用的operator+ 会生成额外的副本。 (operator= 也可能最终被复制;使用 C++ 和移动语义,它可能不会,但在早期版本中它会。)
  • C++20 将其指定为acc = move(acc) + rhs,这可以大大减少复制成本不高的类型的累积。例如,一个好的std::string 实现将有一个operator+(string&amp;&amp; lhs, T) 劫持lhs,附加到它,然后返回它(这是RVOable)。 @R.MartinhoFernandes 一个不那么丑陋的等价物是在reference_wrapper 中积累,就像在this answer 中一样,但是是的,我确实想知道这是否真的比for[_each] 具有捕获的参考更好/更好。我猜可能有点,语义上?

标签: c++ algorithm c++11 vector accumulate


【解决方案1】:

试试下面的

res=std::accumulate(foo.begin(),foo.end(),res,
  [](string &rs, const string &arg) -> string & { return rs+=arg; });

在这个电话之前,也许有一种感觉可以打电话

std::string::size_type n = std::accumulate( foo.begin(), foo.end(), 
   std::string::size_type( 0 ),
   [] ( std::string_size_type n, const std::string &s ) { return ( n += s.size() ); } );

res.reserve( n );

【讨论】:

  • 这将复制到accumulate内部使用的累加器中(它相当于accum = op(accum, *it);
  • 没有任何应对措施。有一个复制赋值运算符会看到字符串试图分配给自己。
  • @VladfromMoscow:你确定吗?仅在这种情况下会发生这种情况,还是对返回 rs 有效?我在我的问题中也说过吗?如果我可以依靠它,或者至少可以肯定,这对我来说就足够了。
  • @JamesKanze,分配运算符需要处理您分配给自​​己的情况,但这种情况可能很少见。但是还有其他技术,例如通过复制而不是引用获取参数(让复制构造函数完成工作)并交换内容 - 它可以工作,但不能避免此答案所暗示的开销。
  • @JamesKanze:好点,虽然现实略有不同。它创建了一个新字符串,其副本作为+ 的参数,但结果不会被复制;它被交换了。这是因为operator+(std::string,std::string) 是根据std::string::operator+= 定义的(类似于:std::string r = a; r += b; return r;
【解决方案2】:

我会将其分解为两个操作,首先是 std::accumulate 获取需要创建的字符串的总长度,然后是带有更新本地字符串的 lambda 的 std::for_each

std::string::size_type total = std::accumulate(foo.begin(), foo.end(), 0u, 
                [](std::string::size_type c, std::string const& s) {
                    return c+s.size() 
                });
std::string result;
result.reserve(total);
std::for_each(foo.begin(), foo.end(), 
              [&](std::string const& s) { result += s; });

对此的常见替代方法是使用表达式模板,但这不适合答案。基本上,您创建一个映射操作的数据结构,但不执行它们。当表达式最终被评估时,它可以预先收集它需要的信息并使用它来保留空间并进行复制。使用表达式模板的代码更好,但更复杂。

【讨论】:

  • 我试图让 std::accumulate 有效地工作——这意味着避免不必要地创建临时对象。我不介意重新分配。我能够避免 std::accumulate 并以其他方式强制执行有效的行为,但这不是我想要的。
  • 好主意!它可能不会比这更有效,而且它的代码也足够短。 +1
  • @tach:你可以选择你想要的行为或你使用的工具,但你不能用锤子拧开。 --虽然这并不完全正确,但如果你愿意付出足够的努力,你可以创建基础设施来做到这一点(同样,表达式模板类型的方法)
  • 我假设在某些情况下编译器可能能够优化临时对象的创建,或者至少以某种方式减轻它。我很想使用 std::accumulate 因为它的语法,但如果它总是效率低下,我会说它的用处会大大降低。
  • @tach:从编译器的角度来看,问题不是优化之一。您希望它更改operator+ 调用并将它们映射到operator+=,但编译器不知道这种等效性。请注意,虽然 std::string 是标准的一部分,但它是作为用户定义的类型实现的,编译器很可能不像你知道的那么多。
【解决方案3】:

这有点棘手,因为涉及到两个操作, 添加和分配。为了避免复制, 您必须同时修改字符串, 确保分配是空操作。这是第二部分 这很棘手。

我有时会创建一个自定义“累加器”, 大致如下:

class Accu
{
    std::string myCollector;
    enum DummyToSuppressAsgn { dummy };
public:
    Accu( std::string const& startingValue = std::string() )
        : myCollector( startingValue )
    {
    }
    //  Default copy ctor and copy asgn are OK.
    //  On the other hand, we need the following special operators
    Accu& operator=( DummyToSuppressAsgn )
    {
        //  Don't do anything...
        return *this;
    }
    DummyToSuppressAsgn operator+( std::string const& other )
    {
        myCollector += other;
        return dummy;
    }
    //  And to get the final results...
    operator std::string() const
    {
        return myCollector;
    }
};

致电accumulate 时会有几份副本,其中的 返回值,但在实际积累过程中,什么都没有。只是 调用:

std::string results = std::accumulate( foo.begin(), foo.end(), Accu() );

(如果你真的关心性能,你可以添加 Accu 的构造函数的容量参数,以便它可以 在成员字符串上执行reserve。如果我这样做,我会 可能还要手写复制构造函数,以确保 复制对象中的字符串具有所需的容量。)

【讨论】:

    【解决方案4】:

    在没有任何冗余副本的情况下有效地使用std::accumulate 并不明显。
    除了被重新分配并传入和传出 lambda 之外,累积值可能会被实现在内部复制。
    另外,请注意 std::accumulate() itself 采用初始值 by-value,调用一个 copy-ctor,因此忽略在副本源上完成的任何 reserve()s(如某些其他答案)。

    我发现连接字符串的最有效方法如下:

    std::vector<std::string> str_vec{"foo","bar"};
    
    // get reserve size:
    auto sz = std::accumulate(str_vec.cbegin(), str_vec.cend(), std::string::size_type(0), [](int sz, auto const& str) { return sz + str.size() + 1; });
    
    std::string res;
    res.reserve(sz);
    std::accumulate(str_vec.cbegin(), str_vec.cend(),
       std::ref(res), // use a ref wrapper to keep same object with capacity
       [](std::string& a, std::string const& b) -> std::string& // must specify return type because cannot return `std::reference_wrapper<std::string>`.
    {                                                           // can't use `auto&` args for the same reason
       a += b;
       return a;
    });
    

    结果将在res
    此实现没有冗余副本、移动或重新分配。

    【讨论】:

    • @VaughnCato:谢谢 :-)。我实际上调查了同样的问题,并在找到问题前几天发现了这一点。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-05-13
    • 1970-01-01
    • 1970-01-01
    • 2021-06-06
    • 1970-01-01
    • 1970-01-01
    • 2015-10-16
    相关资源
    最近更新 更多