【问题标题】:Difference between string += s1 and string = string + s1string += s1 和 string = string + s1 之间的区别
【发布时间】:2019-11-29 14:55:27
【问题描述】:

当我使用fans = fans + s[i] 时,我的一个程序超过了时间限制,而当我使用fans += s[i] 时,它正在被接受...为什么会发生这种情况? 为了解释更多,fans是一个字符串,s也是一个字符串,所以在迭代字符串时,si只需要s的一些字符,所以我正在创建一个新的字符串fans.现在有两种方法可以将字符添加到我的新字符串中粉丝。问题在下面提到

fans = fans + s[i]; // gives Time limit exceeded 
fans += s[i];       // runs successfully

【问题讨论】:

  • 一个做临时的,另一个不做。 move-assignment 可以让前者更有效率,但不能比后者更好。
  • 请提取并提供minimal reproducible example,如果没有它,您的问题将是题外话。作为新用户,也可以使用tour 并阅读How to Ask
  • 所以fans 是一个std::string 而s[i] 是一个字符?还是另一个包含单个字符的字符串?
  • @Naman 你读过 Ulrich Eckhardt 的评论吗?至少你应该澄清fanss[i] 是什么类型,但实际上如果你edited 你的问题并添加一个我们可以复制和编译的完整程序(minimal reproducible example)会更好。只需检查您获得的链接。这不会花很长时间,它会显着改善你的问题。谢谢!
  • 您不应该粘贴您正在处理的程序的代码。您应该从该代码中提取minimal reproducible example 并发布。应该可以在不更改的情况下获取并编译它。它不应包含证明问题所不需要的任何内容。你的问题仍然缺乏,所以它仍然是题外话。

标签: c++ performance stdstring compound-assignment


【解决方案1】:

对于内置类型a += ba = a + b 完全相同(除了 a 只计算一次),但对于类,这些运算符被重载并调用不同的函数.
在您的示例中,fans = fans + s[i] 创建了一个临时字符串,并将其分配(移动)给fans,但fans += s[i] 不会创建该临时字符串,因此它可能会更快。

【讨论】:

  • "+=" 如果字符串大小已经与预分配的缓冲区一样大,也可能导致重定位。如果后来证明这是一个问题(当然应该测量),并且如果可以提前估计大小,则可以使用 string::reserve() 显式增加缓冲区大小。
  • @IMil 实际上,取决于实现如何处理分配,这两种方法很可能没有区别,不是吗?对于一个简单的实现,我希望这两种方法几乎完全相同(1 次分配的时间)。
  • @MCΔT 没错。因此,向 OP 建议如果性能是一个问题,那么应该了解标准容器的工作原理以及它们在速度和内存方面的作用和不保证是有意义的。在紧密循环中修改 std::string 肯定是浪费,但在不了解上下文的情况下很难建议替换。
【解决方案2】:

对于基本类型,a = a + ba += b 含义相同。

对于任意类类型,a = a + ba += b 是不相关的;他们查找不同的运算符,这些运算符可以做任意事情。它们实际上不相关是代码异味,这是设计问题的标志。

a = a + b 大致变成operator=( a, operator+( a, b ) );实际的查找规则要复杂一些(涉及成员运算符和非成员运算符,以及= 没有非成员运算符等事实),但这是它的核心。

a += b 变成 operator+=( a, b ) 在类似的意义上。

现在,用+= 来实现+ 是一种常见的模式;如果你这样做,你会得到:

a = a + b

变成

a = ((auto)(a) += b);

其中(auto) 是新的/“创建参数的临时副本”功能。

基本上a+=b可以直接复用a的内容,而a = a + b不能;在评估a+b 的那一刻,它不知道a 将很快被覆盖。

一些库使用一种称为“表达式模板”的技术来处理这个问题; a+b 不是一个值,而是表达式a+b 的编译时描述,当分配给a 时,它实际上用于用数据填充a。使用表达式模板,消除了a+=ba=a+b 了解更多的基本问题。

现在,对于std::string,具体来说,a+b创建了一个临时字符串对象,然后a=(a+b)将其移动到a中(它可以重用临时字符串对象的缓冲区或a的缓冲区,标准对此事保持沉默)。

a+=b 必须重用a 缓冲区中的任何多余容量。所以如果你a.reserve(1<<30)(10亿),a+=b不能分配更多。

【讨论】:

  • 您能否链接到有关此新(auto) 功能的进一步讨论?
  • @m.m 不是讨论,而是一篇论文:open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0849r1.html
  • 谢谢。可能应该允许static_cast<auto>(x) 保持一致,即T(x) 表示static_cast<T>(x),反之亦然
  • @M.M T(x) 不代表static_cast<T>(x),反之亦然。有时确实如此。我认为这里的冗长是一种罪过。 C++ 标准在事物的初始版本中往往过于冗长,我们应该与这种趋势作斗争。
【解决方案3】:

如果您使用fans=fans+s[i],则字符串将在每个循环传递中被复制。新元素将添加到字符串的副本中,结果将重新分配给变量fans。在此之后,必须删除旧字符串,因为它不再被引用。这需要很长时间。

如果您使用增强赋值fans+=s[i],则不会在每个循环传递中复制字符串,并且不需要删除引用变量,因为这里没有引用变量。这样可以节省很多时间。

希望你现在能明白了!!

【讨论】:

  • 在现代 C++ 中,不需要“复制”字符串 - 可以应用移动语义。
  • @Alnitak,我相信移动语义可以使string = string + s1 优于没有移动语义,但不如string += s1。我相信前者将始终拥有strings1 的所有字节的至少一个分配和一个完整副本,而后者(string += s1)仅复制s1 的字节并且不会任何额外的分配(只要结果可以适合string 的已分配容量)。请参阅我的相关帖子 [stackoverflow.com/q/57151379/1245420],如果我错了,请纠正我!
  • @phonetagger 我指的是由= 运算符引起的额外副本,没有可用的移动语义。使用移动语义,可以将 string + s1 的结果重新分配给 string 而无需复制开销。但是,是的,首先会创建一个临时文件来制作string + s1,这也涉及复制操作。
  • fans = fans + s[i]中,operator+的结果必须实现一个临时对象;可以移出到fans,但这仍然需要fans的原始内容的完整副本与原始内容同时存在,直到发生这种移动然后原始内容被丢弃
【解决方案4】:

std::string 有成员 operator +operator +=。前者通常通过中间临时的方式与后者一起实现。看起来像这样(如果你想确切地知道你的实现源代码,请检查你的实现源代码):

/// note reference return type
std::string& operator +=(char c) 
{
    this->append(c);
    return *this;
}

// note value return type
std::string operator +(char c) const
{
    std::string tmp = *this;
    tmp += c; // or just tmp.append(c) directly
    return tmp;
}

tmp 的设置很昂贵。使用 move-assignment 语义可以(并且通常)使整个功能更好地到达调用方的最终目的地,但是临时的费用仍然存在。做几次,你不会注意到差异。做上千次、上百万次等等,这可能意味着一个不同的世界

【讨论】:

  • 在其他方面实现string+anythingstring+=anything 看起来总是效率低下(一次分配和释放太多)。你确定有实现吗?
  • @Deduplicator += 通常是两者中效率更高的一个(尽管可能存在异常值)。如上所述通过调用+= 来实现+ 是相对常见的,这不会产生任何重大开销。两个运算符通常都通过 const 引用接受他们的 RHS 参数(如果它是不平凡的),所以没有额外的参数复制,只是简单的函数调用开销——如果编译器内联它,即使这样也经常被消除。同样,NRVO 或其朋友之一意味着operator+ 只创建一个临时的,而不是两个。
  • @Miral 让我们来看看,好吗? += 使用 +,字符串将被重新分配,使用任何可用的额外容量是不可能的。 + 使用 +=,字符串将为 lhs 的副本分配,然后必须重新分配以容纳 rhs。对于加法不是串联的类型,通常不会有这个问题,但这是关于具有容量的字符串,特别是std::string
  • 正如我所说,通常你不会通过调用+ 来实现+=,因为那完全是低效的。对于+ 调用+=,是的,您是正确的,上面的确切实现没有达到它应有的效率,因为它可能 分配两次(但如果附加的 rhs 适合lhs的容量,那么这不会发生)。为了提高效率,实践中通常会使用不同的实现,它在调用+=(或道德等价物)之前保留附加的大小。不过,这超出了此答案的范围。
猜你喜欢
  • 2013-10-20
  • 1970-01-01
  • 2013-05-27
  • 1970-01-01
  • 1970-01-01
  • 2013-06-25
  • 1970-01-01
  • 1970-01-01
  • 2017-08-24
相关资源
最近更新 更多