復制數據的快速方法std::copy
C++復制數據各種方法大家都會,很多時候我們都會用到std::copy這個STL函數,這個效率確實很不錯,比我們一個一個元素復制或者用迭代器復制都來的要快很多。
比如,我寫了一段下面的代碼,復制100000000數據量,std::copy的性能要比前兩個性能要好。
const int size = 100000000; int *k = new int[size]; int *p = new int[size]; //const int size = 5F5E100h; DWORD t1, t2; t1 = GetTickCount(); for (int i = 0; i != size; i++) p[i] = k[i]; t2 = GetTickCount(); cout << t2 - t1 << "ms" << std::endl; t1 = GetTickCount(); int *pStart = k, *pEnd = k + size, *pDest = p; for (; pStart != pEnd; pDest++, pStart++) *pDest = *pStart; t2 = GetTickCount(); cout << t2 - t1 << "ms" << std::endl; t1 = GetTickCount(); std::copy(k, k + size, p); t2 = GetTickCount(); cout << t2 - t1 << "ms" << std::endl;
在我的機子上表現如下:

很多時候我們知道用是可以這么用,可是為什么std::copy的效率要比我們這其他兩種方法的效率要好呢?為了找到真正的原因,我們必須做機器級分析了,我們不妨跟蹤一下前兩個方法的匯編(VS編譯器,x86)
下標取值的方法(A方法):
for (int i = 0; i != size; i++) 00F0A8B1 mov dword ptr [ebp-54h],0 00F0A8B8 jmp main+0A3h (0F0A8C3h) 00F0A8BA mov eax,dword ptr [ebp-54h] 00F0A8BD add eax,1 00F0A8C0 mov dword ptr [ebp-54h],eax 00F0A8C3 cmp dword ptr [ebp-54h],5F5E100h 00F0A8CA je main+0C0h (0F0A8E0h) p[i] = k[i]; 00F0A8CC mov eax,dword ptr [ebp-54h] 00F0A8CF mov ecx,dword ptr [p] 00F0A8D2 mov edx,dword ptr [ebp-54h] 00F0A8D5 mov esi,dword ptr [k] 00F0A8D8 mov edx,dword ptr [esi+edx*4] 00F0A8DB mov dword ptr [ecx+eax*4],edx 00F0A8DE jmp main+9Ah (0F0A8BAh)
迭代器方法(B方法):
int *pStart = k, *pEnd = k + size, *pDest = p; 00F0A944 mov eax,dword ptr [k] 00F0A947 mov dword ptr [pStart],eax 00F0A94A mov eax,dword ptr [k] 00F0A94D add eax,17D78400h 00F0A952 mov dword ptr [pEnd],eax 00F0A955 mov eax,dword ptr [p] 00F0A958 mov dword ptr [pDest],eax for (; pStart != pEnd; pDest++, pStart++) 00F0A95B jmp main+14Fh (0F0A96Fh) 00F0A95D mov eax,dword ptr [pDest] 00F0A960 add eax,4 00F0A963 mov dword ptr [pDest],eax 00F0A966 mov ecx,dword ptr [pStart] 00F0A969 add ecx,4 00F0A96C mov dword ptr [pStart],ecx 00F0A96F mov eax,dword ptr [pStart] 00F0A972 cmp eax,dword ptr [pEnd] 00F0A975 je main+163h (0F0A983h) *pDest = *pStart; 00F0A977 mov eax,dword ptr [pDest] 00F0A97A mov ecx,dword ptr [pStart] 00F0A97D mov edx,dword ptr [ecx] 00F0A97F mov dword ptr [eax],edx 00F0A981 jmp main+13Dh (0F0A95Dh)
這兩段匯編都有一個共同的特性就是都會有這么一種操作:
A在10-15行中,每次都取[ebp-54h]這個位置的值(也就是i),然后每次都取p和k的指針,然后再取i的值,然后以i的值(eax和edx)定位到數組相應位置[esi + eax*4]和[ecx + edx*4],然后再把[ecx + edx*4]放到[esi + eax*4]中。B在11到24行中,也是差不多的用法,只是他把下標位置改成了指針指向的位置。
分析到這里我們可以發現,這兩個方法是在太累贅了,比如A,這么簡單的賦值居然要訪問存儲器5次,大大降低了運行效率。
那么為什么std::copy會那么快呢?我們先來跟蹤一下std::copy的源代碼:
template<class _InIt, class _OutIt> inline _OutIt _Copy_memmove(_InIt _First, _InIt _Last, _OutIt _Dest) { // implement copy-like function as memmove const char * const _First_ch = reinterpret_cast<const char *>(_First); const char * const _Last_ch = reinterpret_cast<const char *>(_Last); char * const _Dest_ch = reinterpret_cast<char *>(_Dest); const size_t _Count = _Last_ch - _First_ch; _CSTD memmove(_Dest_ch, _First_ch, _Count); return (reinterpret_cast<_OutIt>(_Dest_ch + _Count)); } template<class _InIt, class _OutIt> inline _OutIt _Copy_unchecked1(_InIt _First, _InIt _Last, _OutIt _Dest, _General_ptr_iterator_tag) { // copy [_First, _Last) to [_Dest, ...), arbitrary iterators for (; _First != _Last; ++_Dest, (void)++_First) *_Dest = *_First; return (_Dest); } template<class _InIt, class _OutIt> inline _OutIt _Copy_unchecked1(_InIt _First, _InIt _Last, _OutIt _Dest, _Trivially_copyable_ptr_iterator_tag) { // copy [_First, _Last) to [_Dest, ...), pointers to trivially copyable return (_Copy_memmove(_First, _Last, _Dest)); } template<class _InIt, class _OutIt> inline _OutIt _Copy_unchecked(_InIt _First, _InIt _Last, _OutIt _Dest) { // copy [_First, _Last) to [_Dest, ...) // note: _Copy_unchecked is called directly elsewhere in the STL return (_Copy_unchecked1(_First, _Last, _Dest, _Ptr_copy_cat(_First, _Dest))); } template<class _InIt, class _OutIt> inline _OutIt _Copy_no_deprecate1(_InIt _First, _InIt _Last, _OutIt _Dest, input_iterator_tag, _Any_tag) { // copy [_First, _Last) to [_Dest, ...), arbitrary iterators return (_Rechecked(_Dest, _Copy_unchecked(_First, _Last, _Unchecked_idl0(_Dest)))); } template<class _InIt, class _OutIt> inline _OutIt _Copy_no_deprecate1(_InIt _First, _InIt _Last, _OutIt _Dest, random_access_iterator_tag, random_access_iterator_tag) { // copy [_First, _Last) to [_Dest, ...), random-access iterators _CHECK_RANIT_RANGE(_First, _Last, _Dest); return (_Rechecked(_Dest, _Copy_unchecked(_First, _Last, _Unchecked(_Dest)))); } template<class _InIt, class _OutIt> inline _OutIt _Copy_no_deprecate(_InIt _First, _InIt _Last, _OutIt _Dest) { // copy [_First, _Last) to [_Dest, ...), no _SCL_INSECURE_DEPRECATE_FN warnings _DEBUG_RANGE_PTR(_First, _Last, _Dest); return (_Copy_no_deprecate1(_Unchecked(_First), _Unchecked(_Last), _Dest, _Iter_cat_t<_InIt>(), _Iter_cat_t<_OutIt>())); } template<class _InIt, class _OutIt> inline _OutIt copy(_InIt _First, _InIt _Last, _OutIt _Dest) { // copy [_First, _Last) to [_Dest, ...) _DEPRECATE_UNCHECKED(copy, _Dest); return (_Copy_no_deprecate(_First, _Last, _Dest)); }
我們發現,copy最后要么執行的是_Copy_unchecked1,要么執行的是_Copy_memmove,那究竟執行的是誰呢?我們來看中間函數_Copy_no_deprecate的返回值:
return (_Copy_no_deprecate1(_Unchecked(_First), _Unchecked(_Last), _Dest, _Iter_cat_t<_InIt>(), _Iter_cat_t<_OutIt>()));
這里運用的是C++ 的traits技術,_Iter_cat_t<_InIt>其實是一個模板的別名:
template<class _Iter> using _Iter_cat_t = typename iterator_traits<_Iter>::iterator_category;
iterator_traits可以用來顯示一個STL里面廣泛運用的用來判別迭代器的屬性的東西,它一共有5個屬性,其中iterator_category就是說明了這個迭代器是以下哪五種迭代器之一:
input_iterator_tag //輸入迭代器,單向一次一步移動,讀取一次
output_iterator_tag //輸出迭代器,單向一次一步移動,塗寫一次
forward_iterator_tag //向前迭代器,單向一次一步移動,多次讀寫,繼承自輸入迭代器
bidirectional_iterator_tag //雙向迭代器,雙向一次一步移動,多次讀寫,繼承自向前迭代器
random_access_iterator_tag //隨機迭代器,任意位置多次讀寫,繼承自雙向迭代器
而在我們的例子里,由於我們是int *類型,所以這個東西的iterator_category是random_access_iterator_tag,所以我們會跳到_Copy_unchecked上,然后執行_Ptr_copy_cat
template<class _Source, class _Dest> inline _General_ptr_iterator_tag _Ptr_copy_cat(const _Source&, const _Dest&) { // return pointer copy optimization category for arbitrary iterators return {}; } template<class _Source, class _Dest> inline conditional_t<is_trivially_assignable<_Dest&, _Source&>::value, typename _Ptr_cat_helper<remove_const_t<_Source>, _Dest>::type, _General_ptr_iterator_tag> _Ptr_copy_cat(_Source * const&, _Dest * const&) { // return pointer copy optimization category for pointers return {}; }
因為我們的_Source和_Dest類型都是指針類型(而不是常量引用),所以會匹配第二個重載版本,然后經過conditional_t的轉換,最后會轉換成_Trivially_copyable_ptr_iterator_tag(那個轉換太長了,大家可以去STL一個一個翻),然后調用_Copy_memmove,然后_Copy_memmove我們一眼就發現了一個很熟悉的東西:
_CSTD memmove(_Dest_ch, _First_ch, _Count);
memcpy與memmove其實差不多,目的都是將N個字節的源內存地址的內容拷貝到目標內存地址中,但是,當源內存和目標內存存在重疊時,memcpy會出現錯誤,而memmove能正確地實施拷貝,但這也增加了一點點開銷。memmove與memcpy不同的處理措施:
當源內存的首地址等於目標內存的首地址時,不進行任何拷貝
當源內存的首地址大於目標內存的首地址時,實行正向拷貝
當源內存的首地址小於目標內存的首地址時,實行反向拷貝
這下我們就明白了,當我們對動態數組調用std::copy的時候,實際上就是調用的memmove的C標准庫,用memmove可以加快復制過程。
memmove機器級實現方式
實際上我們其實可以在http://www.gnu.org/prep/ftp找到其實現代碼,但是由於C標准庫的代碼真的雜亂無章,閱讀難度實在是太高,我們能不能有另一種方法去感知memmove的實現方式呢?
首先我們有一個直覺就是,作為一個C標准庫,在memmove內部,一定是有用了內聯匯編的方式實現,如果直接用C/C++代碼去實現,我們很難生成高質量的代碼,網上有很多所謂的memmove的實現,其實都只是在C/C++層面上對功能進行了模擬而已,效率肯定是沒有匯編高的。
現在我們的問題就是怎么實現匯編級的memmove,一看到這里我們就可以立馬反映過來這不就是x86匯編的內容嗎?在x86匯編中,我們要實現內存的復制,最常見的指令就是movsb,movsw,movsd(分別移動字節,字,雙字)
這三個指令每一次執行都會將源地址到目的地址的數據的復制
目標地址由di決定(對於movsb,movsw是di,movsd是edi),每執行一次,根據DF的值+1(DF == 0)或者-1(DF ==1)
源地址由si決定(對於movsb,movsw是si,movsd是esi),每執行一次,根據DF的值+1(DF == 0)或者-1(DF ==1)
這三個指令還要配合rep來用,rep是重復指令,當ecx>0時它會一直執行被請求重復的指令。
我們可以在VS上進行內聯匯編(x86下,x64還要配置太復雜了)
__asm { mov esi, dword ptr[k]; mov edi, dword ptr[p]; mov ecx, 5F5E100h; rep movsd; };
好吧,其實上面是memcpy。如果要實現memmove,還需要多進行一些判斷,就像memmove要求的那樣
事實上,我們只要單步調試就可以看到memmove執行的代碼了,在VS里面看,的確是進行了匯編優化(注意VS編譯器用的memmove的並不是在memmove.c定義的C的版本,而是在memcpy.asm的匯編版本),在我們的例子中,匯編代碼如下:
ifdef MEM_MOVE _MEM_ equ <memmove> else ; MEM_MOVE
_MEM_ equ <memcpy> endif ; MEM_MOVE
% public _MEM_ _MEM_ proc \ dst:ptr byte, \ src:ptr byte, \ count:IWORD ; destination pointer
; source pointer
; number of bytes to copy
OPTION PROLOGUE:NONE, EPILOGUE:NONE push edi ; save edi
push esi ; save esi
; size param/4 prolog byte #reg saved
.FPO ( 0, 3 , $-_MEM_ , 2, 0, 0 ) mov esi,[esp + 010h] ; esi = source
mov ecx,[esp + 014h] ; ecx = number of bytes to move
mov edi,[esp + 0Ch] ; edi = dest
; ; Check for overlapping buffers: ; If (dst <= src) Or (dst >= src + Count) Then ; Do normal (Upwards) Copy ; Else ; Do Downwards Copy to avoid propagation ;
mov eax,ecx ; eax = byte count
mov edx,ecx ; edx = byte count
add eax,esi ; eax = point past source end
cmp edi,esi ; dst <= src ?
jbe short CopyUp ; no overlap: copy toward higher addresses
cmp edi,eax ; dst < (src + count) ?
jb CopyDown ; overlap: copy toward lower addresses
; ; Buffers do not overlap, copy toward higher addresses.
CopyUp:
cmp ecx, 020h jb CopyUpDwordMov ; size smaller than 32 bytes, use dwords
cmp ecx, 080h jae CopyUpLargeMov ; if greater than or equal to 128 bytes, use Enhanced fast Strings
bt __isa_enabled, __ISA_AVAILABLE_SSE2 jc XmmCopySmallTest jmp Dword_align CopyUpLargeMov:
bt __favor, __FAVOR_ENFSTRG ; check if Enhanced Fast Strings is supported
jnc CopyUpSSE2Check ; if not, check for SSE2 support
rep movsb
mov eax,[esp + 0Ch] ; return original destination pointer
pop esi pop edi M_EXIT
因為我們的例子中沒有重疊的內存區,而且大小也比128bytes要大,自然就進入了CopyUpLargeMov過程,我們可以很清楚地發現rep movsb了,memmove實現過程就是我們所想的那樣。實際上memmove匯編版本還有其他大量的優化,有興趣的朋友可以點進去memcpy.asm去看一看。
這樣感覺很不錯,用movsd指令以后我們可以很直觀地發現我們已經減少了很多無謂的寄存器賦值操作(movsd指令還有被CPU進行加速的)我們接下來試下效果:

效果很不錯,已經可以達到memmove的C標准庫效果了。
Reference :