C++0x性能提升

10

C++0x的一个改进是unique_ptr智能指针,它能够帮助编写更高效的C++代码(遗憾的是,它无法像memmove()那样进行移动操作:该提案未被纳入草案)。

在即将到来的标准中还有哪些性能改进呢? 以以下代码为例:

vector<char *> v(10,"astring");
string concat = accumulate(v.begin(),v.end(), string(""));

这段代码将连接包含在向量v中的所有字符串。这个简洁的代码存在问题,accumulate()会复制数据,并且不使用引用。而string()每次调用加号运算符时都会重新分配内存。因此,与经过优化的类似C代码相比,该代码性能较差。

C++0x提供了解决这个问题以及其他问题的工具吗?


1
这里有关于该主题的其他答案:https://dev59.com/E3RB5IYBdhLWcg3wZWfi#637737 - Johannes Schaub - litb
4个回答

14

是的,C++通过一种叫做“移动语义”的东西来解决这个问题。

基本上,它允许一个对象获取另一个对象的内部表示,如果该对象是一个临时对象。例如,相对于通过复制构造函数复制字符串中的每个字节,你通常只需要允许目标字符串获取源字符串的内部表示即可。仅当源是r-value时才允许这样做。

这是通过引入“移动构造函数”来完成的。它是一种构造函数,其中你知道src对象是一个临时对象并将要消失。因此,允许目标获取src对象的内部表示。

对于“移动赋值运算符”,也是同样的道理。

为了区分复制构造函数和移动构造函数,语言引入了“右值引用”。类将其移动构造函数定义为采用右值引用,它只能绑定到右值(临时对象)。所以我的类会定义类似以下代码:

 class CMyString
 {
 private:
     char* rawStr;
 public:

     // move constructor bound to rvalues
     CMyString(CMyString&& srcStr) 
     {
         rawStr = srcStr.rawStr
         srcStr.rawStr = NULL;             
     }

     // move assignment operator 
     CMyString& operator=(CMyString&& srcStr) 
     {
         if(rawStr != srcStr.rawStr) // protect against self assignment
         {
             delete[] rawStr;
             rawStr = srcStr.rawStr
             srcStr.rawStr = NULL;
         }
         return *this;
     }

     ~CMyString()
     {
         delete [] rawStr;
     }
 }

这里有一篇非常好而且详细的文章,介绍了移动语义和可以实现它的语法。


3
这使得像对集合的排序和旋转这样的排列操作更快。我猜最常见的例子是对字符串集合进行排序。 - Brian
1
这也是一个非常好的rvalue引用解释:https://www.boostpro.com/trac/wiki/BoostCon09/RValue101 - Johannes Schaub - litb
将所有容器和算法扩展(或转换)为移动语义似乎是一项艰巨的任务,我想知道进展如何,是否能在不牺牲效率的情况下保持与实际STL的兼容性。 - anon
@litb:该链接要求输入用户名和密码 :( - sstock

7

一项性能提升将是广义常量表达式,由关键字constexpr引入。

constexpr int returnSomething() {return 40;}

int avalue[returnSomething() + 2]; 

这不是合法的C++代码,因为returnSomething()+2不是常量表达式。

但是通过使用constexpr关键字,C++0x可以告诉编译器该表达式是一个编译时常量。


1
更多的关键字???我们已经有了volatile、unsigned、restrict、static、const、Register……我漏掉了什么吗? - Trevor Boyd Smith
1
@TrevorBoydSmith:C++有大约60个关键字,你错过了很多。 - Mooing Duck

1
vector<string> v(10, "foo");
string concat = accumulate(v.begin(), v.end(), string(""));

这个例子在任何C++标准中都是糟糕的编程。它相当于这个:
string tmp;
tmp = tmp + "foo"; //copy tmp, append "foo", then copy the result back into tmp
tmp = tmp + "foo"; //copy tmp, append "foo", then copy the result back into tmp
tmp = tmp + "foo"; //copy tmp, append "foo", then copy the result back into tmp
tmp = tmp + "foo"; //copy tmp, append "foo", then copy the result back into tmp
tmp = tmp + "foo"; //copy tmp, append "foo", then copy the result back into tmp
tmp = tmp + "foo"; //copy tmp, append "foo", then copy the result back into tmp
tmp = tmp + "foo"; //copy tmp, append "foo", then copy the result back into tmp
tmp = tmp + "foo"; //copy tmp, append "foo", then copy the result back into tmp
tmp = tmp + "foo"; //copy tmp, append "foo", then copy the result back into tmp
tmp = tmp + "foo"; //copy tmp, append "foo", then copy the result back into tmp

C++11的移动语义只会处理“将结果复制回tmp”的部分。从tmp中进行的初始复制仍然是复制。这是一个经典的Schlemiel画家算法,甚至比在C中使用strcat的例子更糟糕。
如果accumulate只使用+=而不是+=,那么它就可以避免所有这些复制。
但是,C++11确实为我们提供了一种更好的方法,同时保持简洁,使用范围for
string concat;
for (const string &s : v) { concat += s; }

编辑:我认为标准库供应商可以选择使用move操作符将操作数移到加号上来实现accumulate,因此tmp = tmp + "foo"将变成tmp = move(tmp) + "foo",这基本上解决了这个问题。我不确定这样的实现是否严格符合规范。在C++11模式下,GCC、MSVC和LLVM都没有这样做。而且由于accumulate是在<numeric>中定义的,人们可能会认为它只适用于数字类型。

编辑2:从C++20开始,accumulate已被重新定义为使用move,就像我之前编辑中的建议一样。我仍然认为这是对一个只设计用于算术类型的算法的可疑滥用。


1

抱歉 - 你不能断言 string concat = accumulate(v.begin(),v.end(), string("")); 必须 重新分配内存。当然,一个简单的实现会这样做。但编译器完全可以在这里做正确的事情。

这已经是C++98的情况了,而C++0x继续允许智能和愚蠢的实现。也就是说,移动语义将使智能实现更简单。


以下是来自gcc 4.1.1的accumulate()函数体:for (; __first != __last; ++__first) __init = __init + *__first; return __init; 你说得对,实现可以通过提供特化来进行优化。对于字符串而言,部分优化是可能的,特化应该:1)将字符串作为引用或指针--这很困难,因为函数签名是由标准规定的;2)使用append()或+=--这是可能的。但最终我认为,语言中适当的新工具将允许解决问题,而无需如此特定的特化。 - anon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接