modern c++的写法思考之减少copy

随着c++的发展,在性能和易用性上得到越来越多的更新,也发现自己越来越不会写c++了,特别是modern c++版本也越来越多,当前常用的就有c++11,c++14,c++17三个。最近收集并琢磨了一些写法,罗列于此。

首先我们定义一个结体体,如下:

struct Student
{
    string name;
    int age;
    bool sex;
};

创建对象,然后存储于各种容器中,是我们常见的操作,在以往的c++写法里,不太计较少许的拷贝损耗,就通常这么写了:

Student s;
s.name = "zhang san";
s.age = 18;
s.sex = true;
vector<Student> vs;
vs.push_back(s);

当然现代一点的写法,也可以简写成这样:

vs.push_back({"zhang san", 18, true});

但实际并没有减少拷贝,以及多余构造和析构的问题。显示这里有string从const char*的构造,这个name进行了一次拷贝,创建Student临时对象时又进行了一次拷贝,push_back时,在容器中又创建了一个对象,同时再次进行整个结构体的拷贝,然后是临时Student对象的析构,接着是每个成员变量的析构。当然啦,这里的结构体并不大,name也不是很长的字符串,所以反复拷贝的开销也并不会太大。假设name是比较长的字符串,我们就需要来减少这个开销了,其实就是减少拷贝。

传统方式解决这个问题,容易理解,但有有些繁琐,可以这么写:

vs.push_back(Student());
auto &s = vs.back();
s.name = "zhang san";
s.age = 18;
s.sex = true;

现代的写法可以这样:

vs.push_back(move(Student{"zhang san", 18, true}));

通过move可以将对象的内容(支持move constructor的成员变量,简单理解为swap()操作)高效地转移给另一个对象,而不会出现字符串拷贝。

但是上面的写法里临时变量的构造和析构还是没有少,于是还可以这么写:

struct Student
{
    Student(string &_name, int _age, bool _sex)
        : name(move(_name)), age(_age), sex(_sex)
    {}

    string name;
    int age;
    bool sex;
};

vs.emplace_back("zhang san", 18, true);

这里就不会有Student的临时对象产生了,自然也不会有析构。emplace_back()大体意思是就地构造。emplace_back可以完全替代push_back,某些情况下前者性能更好,其他情况下,它也不会差。

了解vector低层原理的同学都知道,当不停地push_back/emplace_back时,capacity会以两倍的方式不断扩容,同时将原来的数据拷贝到新的内存里,这么说来,如果有大的string成员,耗时的拷贝不可避免了,然而现代c++则很好的帮我们处理了这些,看如下例子:

vs.emplace_back("hello world hello world");
printf("%p\n", vs[0].data());
vs.emplace_back("hello world hello world");
printf("%p\n", vs[0].data());
vs.emplace_back("hello world hello world");
printf("%p\n", vs[0].data());
vs.emplace_back("hello world hello world");
printf("%p\n", vs[0].data());
vs.emplace_back("hello world hello world");
printf("%p\n", vs[0].data());
vs.emplace_back("hello world hello world");
printf("%p\n", vs[0].data());

运行结果会发现,输出的指针都是一样的,说明尽管对象发生了拷贝,但是对象的字符串成员并没有拷贝,就跟move效果一样。

当然这里需要注意一个问题,就是小字符串优化(small string optimization),上面我把hello world写了两遍,就是避免小字符串优化。长度小于16的字符串会直接放在栈上,大于等于16的才放在堆上,而只有放在堆上的数据,move才有实际的意义。通常指针大小,也很容易判断数据是在堆上还是在栈上,栈指针要比堆指针大得多,如图:

小字符串优化示例:

string a = "123456789012345";
string b = "1234567890123456";
static int c = 100;
printf("%p\n", a.data());  // 0x7ffe11e3a780
printf("%p\n", b.data());  // 0x1429c20
printf("%p\n", &c);        // 0x6020b0
printf("%p\n", main);      // 0x400c86

新版本的c++又提供了shrink_to_fit()函数,以方便减少内存消耗,这显然也会引起数据拷贝操作,将数据拷贝到一个恰好大小的新的内存里。

当然更高效的情况是,我们提前知道有多少元素要存,使用reserve()提交开辟好空间。

现代c++在赋值的右值为临时变量时,已经为我们优先使用move来优化效率了。比如:

vector<string> create_names() {
    vector<string> vs;
    vs.emplace_back("hello world hello world");
    vs.emplace_back("hello world hello world");
    vs.emplace_back("hello world hello world");
    printf("%p\n", vs.data());
    return vs;
}

int main()
{
    auto vs = create_names();
    printf("%p\n", vs.data());
    return 0;
}

总体来说,指针安全和执行效率,两者很难兼得。

发表于2018-10-11 20:13   修改于2018-10-11 20:13   评论:0   阅读:92  



回到顶部

首页 | 关于我 | 关于本站 | 站内留言 | rss
python logo   django logo