减少复制操作是提高性能关键型应用程序的运行时执行速度的好方法。
如果你被允许挑选一种语言来实现了一个应用,你通常会选一个你知道,这会为你提供捷径。如果你需要一个高速运行,直接可以编译成机器代码的语言,那么C++就是你最好的选择。
在现代应用程序中,内存地址的前后移动,跳转,循环以及(有时非必要的)数据区域的复制,会消耗大量的机器代码。本文将重点介绍C++ move语义,它使你能够避免不必要的复制过程。即使你不是程序员,你仍然可以使用valgring堆分析器massif来分析内存的分配。
代码:右值rvalues和左值lvalues
在C++编程中,你不得不处理右值和左值。在下面的例子中,a在赋值操作符的左侧,所以它是左值。被赋值的5在右侧,所以它是右值。
a = 5;
当编译此行时,左值将被解释为符号地址,此后可以更改。右值是一个纯硬编码值。后续代码无法访问它,因为右值没有地址。如果您可以确定表达式的地址,或者如果编译器允许,则为左值。也就是说,如果一个表达式在行末加了分号后编译通过,则为左值;否则,这是一个右值。
一个左值即可以在左侧也可以在右侧。而右值只能在右侧。
a = 5;b = a;
从C++11开始,move语义和对右值引用的处理的可能性加入了标准。右值引用通过“&&”表示。他们允许把一个左值解释成右值。特别是当创建对象时,使用右值引用可以提升性能,我们会在接下来的示例代码中看到。
默认行为
你有一个类叫做MyObject,它使用另一个类MyType,作为其构造函数的一个参数。
#ifndef CARRIER_H#define CARRIER_H#include #include "MyType.h"template class MyObject{public: /* Default constructor */ MyObject(const T& mytype): m_mytype(mytype){ }; /* Contructor with move */ MyObject(T&& mytype): m_mytype(std::move(mytype)){ }; /* Copy constructor */ MyObject(const MyObject& other): m_mytype(other.m_mytype){ }; /* Move contructor */ MyObject(MyObject&& other): m_mytype(std::move(other.m_mytype)){ } T m_mytype;};#endif
#ifndef MYTYPE_H#define MYTYPE_H#include #include #include #include #include #include #if defined(OPT1) || defined(OPT2) || defined(OPT3) #define PRINT#endiftemplate class MyType{public: typedef T type; T dummy; /* Default constructor */ MyType() = default; /* User constructor */ MyType(const std::vector data){ m_data = new std::vector(data);#ifdef PRINT std::cout << "MyType::MyType() contructor called." << std::endl;#endif } /* Destructor */ ~MyType(){ delete m_data;#ifdef PRINT std::cout << "MyType::~MyType() destructor called." << std::endl;#endif } /* Copy constructor */ MyType(const MyType& other){ if(other.m_data){ m_data = new std::vector(*other.m_data); } else { m_data = NULL; }#ifdef PRINT std::cout << "MyType::MyType(const MyType&) copy constructor called" << std::endl;#endif } /* Move constructor */ MyType(MyType&& other){ m_data = other.m_data; other.m_data = NULL;#ifdef PRINT std::cout << "MyType::MyType(MyType&& other) move constructor called" << std::endl;#endif } /* Copy assignment */ MyType& operator=(const MyType& other){ if(this != &other){ delete m_data; if(other.m_data){ m_data = new std::vector(*other.m_data); } else { m_data = NULL; } } return *this;#ifdef PRINT std::cout << "MyType::MyType& operator=(const MyType& other) copy assignmend called" << std::endl;#endif } /* Move assignment */ MyType& operator=(MyType&& other){ if(this != &other){ delete m_data; m_data = other.m_data; other.m_data = NULL; } return *this;#ifdef PRINT std::cout << "MyType::MyType& operator=(MyType&& other) move assignment called" << std::endl;#endif } void print(){ std::cout << "MyType::m_data contains " << m_data->size() << " elements" << std::endl; }private: std::vector *m_data;};#endif
这是我们的主要测试代码(全的测试代码在最后):
MyType是一个模板类,其构造函数需要一个含有double类型的vector作为参数。MyObject也是一个模板类,把MyType的实例type_1作为参数,带入其构造函数。
使用MyObject,有如下步骤:
- 调用MyType的构造函数,创建一个实例(type_1)
- 调用MyType的拷贝构造函数,拷贝一个type_1
- 调用MyObject的构造函数,拿type_1的拷贝作为参数
- (...... object_1 执行一些操作)
- 调用type_1的析构函数
- 调用object_1的析构函数,这会调用对type_1的拷贝的析构函数。
你会发现,如果MyType的实例type_1只是为了创造MyObject1的实例object_1,那么执行了很多非必要的代码。
我们可以自己试一下:
优化上述示例
接下来,我们按照如下执行:
它不调用MyType复制构造函数,而是调用move构造函数。这对运行时性能有积极影响。如下是我们实际运行的代码:
这里object_2直接通过本应该传给MyType的参数直接创建了实例。
就是这样:仅一行,用MyType的构造函数的参数调用MyObject的构造函数。编译器检测到不需要在内存中保留MyType实例。它没有创建另一个副本,而是将内部指针设置为object_2内MyType的实例,该实例为先前创建的实例。
小心move
使用move语义需要一定程度的谨慎。再次运行程序:
发生了什么?下面是测试代码:
这里我们强制使用了move构造函数去初始化object_3。
当type_3被移入object_3后,你不能再引用type_3。该对象已经被毁灭了并且不能再使用。
测试性能
你也许已经注意到了之前截图中的执行时间。虽然你可能会忽略单条执行时间的显示,但当使用一个无循环则可以让执行时间的显示凸显出来。
- 不用move构造器
- 使用move构造器
可以看到一个平均在200左右,一个在150左右,接近25%的提升。当然这里对于编译器的优化是关闭的。当优化为(-O3)时,会有所差异。
分析内存分配
在linux上分析内存的首选工具是valgrind,或更准确的说是堆分析器massif,接下来如下执行:
输出会生成一个massif.out并且附有进程ID,因此我们可以:
该图显示了随着时间持续增长的内存分配。
总结
无论使用哪种编程语言,减少复制操作(在此示例中都会导致堆内存分配)是提高性能关键型应用程序的运行时执行速度的好方法。
在现代的x86-64 CPU上,堆内存分配是如此之快,以至于您不注意应用程序是否在没有精确测量的情况下进行了优化。 CPU的能力越弱,优化运行时代码就越有意义。例如,在移动设备上,它不仅可以改善响应行为,还可以延长电池寿命。
#include #include #include #include #include #include #include #include "MyType.h"#include "MyObject.h"#define SLEEP_TIME 10void case_1(std::vector &container){ /* Not optimized; invokes copy constructor */ MyType type_1(container); MyObject > object_1(type_1); object_1.m_mytype.print();}void case_2(std::vector &container){ /* Optimized: Invokes move constructor */ MyObject > object_2(container); object_2.m_mytype.print();}void case_3(){ MyType type_3({1.2, 3.4, 5.6}); MyObject > object_3(std::move(type_3)); object_3.m_mytype.print(); /* Dangerous: std::move destroys the object */ type_3.print();}void case_4(std::vector &container){ int i; std::vector > > my_objects; for(i=0; i < 1000 ; i++){ /* Not optimized; invokes copy constructor */ MyType type_1(container); MyObject > object_1(type_1); my_objects.push_back(object_1); }}void case_5(std::vector &container){ int i; std::vector > > my_objects; for(i=0; i < 1000; i++){ /* Optimized: Invokes move constructor */ MyType type_1(container); MyObject > object_1(std::move(type_1)); my_objects.push_back(std::move(object_1)); }}int main(int argc, char* argv[]){ int i; std::vector container; std::cout << "Application started..." << std::endl; std::cout << "Process Id: " << ::getpid() << std::endl; for(i=0; i < 0x7FFF; i++){ double nmbr = (double)(std::rand() + 1) / (double)(1/3); container.push_back(nmbr); } auto start = std::chrono::high_resolution_clock::now();#ifdef OPT1 std::cout << "Compiled with OPT1" << std::endl; case_1(container);#elif OPT2 std::cout << "Compiled with OPT2" << std::endl; case_2(container);#elif OPT3 std::cout << "Compiled with OPT3" << std::endl; case_3();#elif OPT4 std::cout << "Compiled with OPT4" << std::endl; int cnt = 0; std::chrono::duration sum(0); while(true){ auto start = std::chrono::high_resolution_clock::now(); case_4(container); cnt++; auto stop = std::chrono::high_resolution_clock::now(); auto duration = std::chrono::duration_cast<:chrono::milliseconds>(stop - start); sum += duration; std::cout << "Average time: " << std::round(sum.count() / cnt) << "ms - Last execution took: " << duration.count() << "um" << std::endl; }#elif OPT5 std::cout << "Compiled with OPT5" << std::endl; int cnt = 0; std::chrono::duration sum(0); while(true){ auto start = std::chrono::high_resolution_clock::now(); case_5(container); cnt++; auto stop = std::chrono::high_resolution_clock::now(); auto duration = std::chrono::duration_cast<:chrono::milliseconds>(stop - start); sum += duration; std::cout << "Average time: " << std::round(sum.count() / cnt) << "ms - Last execution took: " << duration.count() << "um" << std::endl; }#else std::cout << "Run 'make' with option argument OPT{n}, n={1,2,3} < " << std::endl; std::cout << "E.g. 'make CFLAGS=-DOPT2" << std::endl;#endif auto stop = std::chrono::high_resolution_clock::now(); auto duration = std::chrono::duration_cast<:chrono::microseconds>(stop - start); std::cout << "Execution took " << duration.count() << " us" << std::endl;}
Makefile:
#gcc flags:# -c assemble but do not link# # -g include debug information# -o output# # -s make stripped libray# uncomment the last part in line 9 to compile with debug symbold CFLAGS =-Wall -Werror -std=c++11 -O0CXX = g++all: main.o$(CXX) -o memory_sample main.o $(CFLAGS)main.o: main.cpp MyType.h MyObject.h$(CXX) -c main.cpp $(CFLAGS).PHONY: cleanclean:rm *.orm memory_sample