引言
最近在做性能优化,遇到了一个明显的性能问题,就是在大量数据做深拷贝的时候,程序耗时严重,几乎三分之一的时间都耗在了这里。于是乎仔细看了下此处的代码,发现这些深拷贝完全可以避免。
深拷贝、浅拷贝与移动构造
- 浅拷贝 仅仅复制对象的数据成员的值,如果数据成员是指针,那么它会复制指针的值,而不是指针所指向的内容。这意味着两个对象会共享同一块内存区域。当其中一个对象修改了这块内存,另一个对象也会受到影响。如果没有特别定义拷贝构造函数或赋值操作符,编译器会自动生成默认的浅拷贝。
- 深拷贝 深拷贝则不仅复制对象的数据成员,还会复制指针所指向的内容,即在堆上分配新的内存空间来存储被指针指向的数据。这样,即使两个对象有相同的指针成员,它们也会指向不同的内存区域,修改其中一个对象的指针所指向的数据不会影响另一个对象。深拷贝通常需要显式地实现拷贝构造函数和赋值操作符。
- 移动构造 可以看出深拷贝和浅拷贝一般都发生在拷贝构造和赋值操作符中,虽然浅拷贝比深拷贝开销更小,但是其存在资源多次释放的问题。而移动构造可以避免这个问题,其实现方式通常是将源对象指针指向的资源转移到目标对象中,并将源对象的指针置为nullptr,以避免资源的重复释放。
代码优化
在了解了原理之后,就开始对我们的代码进行优化了。以下是优化前的示例代码:
class SampleData {
public:SampleData() {} // 默认构造函数// 带参构造函数SampleData(int size, int *data) {assert(data != nullptr);_size = size;_data = new int[_size];memcpy(_data, data, sizeof(int) * _size);} SampleData(const SampleData& rhs) {_size = rhs._size;_data = new int[_size];memcpy(_data, rhs._data, sizeof(int) * _size);}SampleData& operator=(const SampleData& rhs) {_size = rhs._size;delete[] _data;_data = new int[_size];memcpy(_data, rhs._data, sizeof(int) * _size);return *this;}~SampleData() {_size = 0;delete[] _data;_data = nullptr;}void processData() {// do something}private:int _size{0};int* _data{nullptr};
};void func() {std::vector<SampleData> data_arr;int n = 100000;constexpr int size = 1000;int input_data[size] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};for (int i = 0; i < n; ++i) {SampleData data(size, input_data);data_arr.push_back(data); // 产生了深拷贝}
}int main()
{func();return 0;
}
优化之后,新增移动构造函数如下:
SampleData(SampleData&& rhs) {_size = rhs._size;_data = rhs._data;rhs._size = 0;rhs._data = nullptr;
}
func函数更改如下:
void func() {std::vector<SampleData> data_arr;int n = 100000;constexpr int size = 1000;int input_data[size] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};for (int i = 0; i < n; ++i) {SampleData data(size, input_data);data_arr.push_back(std::move(data)); // 移动构造}
}
总结
- 在做性能优化的时候,遇到数据量较大的场景,减少不必要的数据拷贝,是可以极大得提高程序性能的。
- 移动语义可以帮助我们实现在浅拷贝的同时,又能避免资源重复释放的问题。但是使用移动语义也要注意,被移动的对象在移动完成之后,不能再被使用,否则可能出现未知错误。