作为正式接触汇编的开篇,本文将研究二维数组的遍历问题。在图像处理中,通常需要遍历图像像素(即二维数组)。下面给出三个版本的遍历函数,并研究他们的汇编代码(VC2010编译器,x86版,Release模式)。
(1)在两层循环内每次根据行列索引计算元素位置。
(2)为了避免在内存循环内的乘法计算,可在每次外层循环中计算好行起始地址,内层循环每次执行++操作。
(3)将外层循环的乘法操作也去掉,在循环外部先计算好数组的起始地址,内层循环每次执行++操作即可。
测试程序实现对图像的反相操作(即B=255-A)。我们的直观感觉时他们的访问效率应该逐步提升的,本人之前也是一直用第三种方法来遍历图像像素的。但本次测试发现,效率根本没有提升。究其原因,是编译器做了优化。下面分别给出三个函数以及他们对应的汇编代码(VS中调试—>窗口-->反汇编可查看),并对汇编代码做了注释。
(1)版本1
inline void InvimageV1(uchar *A ,uchar *B,int Width,int Height) { for (int Y=0;Y<Height;Y++) { for (int X=0;X<Width;X++) { B[Y*Width+X]=255-A[Y*Width+X]; } } }
汇编:
003013EB mov eax,dword ptr [esp+44h] //加载高度到寄存器eax 003013EF mov edi,dword ptr [esp+48h] //加载宽度到寄存器edi 003013F3 mov dword ptr [esp+24h],ecx //将目的数组地址存放到[esp+24h]内存处 003013F7 mov ecx,dword ptr [esp+84h] //将源数组地址加载到寄存器ecx中 003013FE mov dword ptr [esp+1Ch],edi //将宽度保存到地址[esp+1ch]内存处 00301402 mov dword ptr [esp+20h],ecx //将源数组地址保存到[esp+20h]内存处 00301406 cmp eax,ebx //测试宽度是否小于等于0(ebx中值为0) 00301408 jle main+21Bh (30143Bh) //若是跳转到30143Bh处 0030140A mov dword ptr [esp+18h],edi //将宽度数保存在地址[esp+18h]内存处 0030140E mov dword ptr [esp+14h],eax //将高度保存在地址[esp+14h]内存处 00301412 cmp edi,ebx //测试宽度是否小于0(ebx中值为0) 00301414 jle main+211h (301431h) //是跳转到301431h处 00301416 mov esi,dword ptr [esp+24h] //将源数组地址加载到寄存器esi 0030141A sub esi,dword ptr [esp+20h] //源地址-目的地址,偏移量保存在寄存器esi 0030141E mov eax,ecx //目的数组起始地址加载到寄存器eax 00301420 or dl,0FFh //将寄存器dl置为255 00301423 sub dl,byte ptr [esi+eax] //255减去源数组中的值(源数组元素地址=目的地址+偏移量),差保存在寄存器dl 00301426 inc eax //eax值加1,对应LinePS++ 00301427 dec edi //edi值减1,控制内层循环次数 00301428 mov byte ptr [eax-1],dl //将差值保存在目的数组对应元素中 0030142B jne main+200h (301420h) //若edi值非0,继续内层循环 0030142D mov edi,dword ptr [esp+1Ch] //将宽度加载到寄存器edi 00301431 add ecx,dword ptr [esp+18h] //源数组起始地址加上宽度值,即转向下一行,对应Y*Width+X,每次递增Width,避免乘法 00301435 dec dword ptr [esp+14h] //将高度值减1,控制外层循环 00301439 jne main+1F2h (301412h) //若高度值非零,继续外层循环
其中黄色覆盖区域为外层循环,绿色覆盖区域为内层循环。
(2)版本2
inline void InvimageV2(uchar *A ,uchar *B,int Width,int Height) { uchar *LinePS,*LinePD; for (int Y=0;Y<Height;Y++) { LinePS=A+Y*Width; LinePD=B+Y*Width; for (int X=0;X<Width;X++) { LinePD[X]=255-LinePS[X]; } } }
汇编:
00FA13E1 mov edx,dword ptr [esp+3Ch] //将高度加载待寄存器edx 00FA13E5 mov eax,dword ptr [esp+40h] //将宽度加载到寄存器eax 00FA13E9 mov edi,eax //将宽度加载到寄存器edi 00FA13EB cmp edx,ebx //测试高度是否小于等于0 00FA13ED jle main+215h (0FA1435h) //若是,转到0FA1435h 00FA13EF mov esi,dword ptr [esp+44h] //将源数组地址加载到寄存器esi 00FA13F3 mov ecx,dword ptr [esp+7Ch] //将目的数组地址加载到寄存器ecx 00FA13F7 mov dword ptr [esp+18h],eax //将宽度保存到[esp+18h]内存处 00FA13FB mov dword ptr [esp+14h],esi //将源数组地址保存到[esp+14h]内存处 00FA13FF mov dword ptr [esp+10h],edx //将宽度保存在[esp+10h]内存处 00FA1403 cmp edi,ebx //测试宽度是否小于等于0 00FA1405 jle main+203h (0FA1423h) //若是,转到0FA1423h 00FA1407 mov eax,ecx //将目的数组起始地址加载到寄存器eax 00FA1409 sub esi,ecx //源地址-目的地址,偏移量保存在寄存器esi 00FA140B jmp main+1F0h (0FA1410h) //跳转至0FA1410h 00FA140D lea ecx,[ecx] //无意义指令? 00FA1410 or dl,0FFh //将寄存器的dl置255 00FA1413 sub dl,byte ptr [esi+eax] //255减原数组元素(源数组元素地址=目的数组元素地址+偏移量) 00FA1416 inc eax //eax值加1,对应X++ 00FA1417 dec edi //edi值减1,控制内层循环次数 00FA1418 mov byte ptr [eax-1],dl //将差值保存到对应的目的数组元素中 00FA141B jne main+1F0h (0FA1410h) //若edi大于0,继续内层循环 00FA141D mov eax,dword ptr [esp+18h] //将宽度加载到寄存器eax 00FA1421 mov edi,eax //将宽度加载到寄存器edi 00FA1423 mov esi,dword ptr [esp+14h] //将源数组起始地址加载到寄存器esi 00FA1427 add esi,eax //esi(源地址)值加上宽度,即转向下一行,对应LinePS=A+Y*Width,每次递增Width,避免乘法 00FA1429 add ecx,eax //ecx(目的地址)值加上宽度,即转向下一行,对应LinePD=B+Y*Width,每次递增Width,避免乘法 00FA142B dec dword ptr [esp+10h] //将高度值减1,控制外层循环 00FA142F mov dword ptr [esp+14h],esi //将源数组地址保存到[esp+14h]内存处 00FA1433 jne main+1E3h (0FA1403h) //若高度值非零,继续外层循环
其中黄色覆盖区域为外层循环,绿色覆盖区域为内层循环。
(3)版本3
inline void InvimageV3(uchar *A ,uchar *B,int Width,int Height) { uchar *LinePS=A,*LinePD=B; for (int Y=0;Y<Height;Y++) { for (int X=0;X<Width;X++) { LinePD[0]=255-LinePS[0]; LinePS++; LinePD++; } } }
汇编:
000213DB mov ecx,dword ptr [esp+34h] //加载高度到寄存器ecx 000213DF mov edx,dword ptr [esp+38h] //加载宽度到寄存器edx 000213E3 mov eax,dword ptr [esp+3Ch] //加载源数组地址到寄存器eax 000213E7 mov esi,dword ptr [esp+74h] //加载目的数组地址到寄存器esi 000213EB cmp ecx,ebx //测试宽度是否小于等于0 000213ED jle main+1F2h (21412h) //若是跳转到21412h处 000213EF mov dword ptr [esp+14h],ecx //将高度保存到地址[esp+14h]的内存处 000213F3 cmp edx,ebx //测试高度是否小于等于0 000213F5 jle main+1ECh (2140Ch) //若是跳转到2140Ch处 000213F7 mov edi,edx //将宽度1024加载到寄存器dei ,对应内层循环的循环次数 000213F9 lea esp,[esp] //无意义的操作?相当于mov esp,esp 00021400 or cl,0FFh //将寄存器cl置为255 00021403 sub cl,byte ptr [eax] //255减源数组元素,结果存放在寄存器cl中 00021405 inc eax //eax中值加1,对应LinePS++ 00021406 mov byte ptr [esi],cl //将差值存放在目的数组对应元素中 00021408 inc esi //esi中值加1,对应LinePD++ 00021409 dec edi //edi中值减1,控制内层循环次数 0002140A jne main+1E0h (21400h) //当edi值非零,继续内层循环 0002140C dec dword ptr [esp+14h] //将内存地址[esp+14](存放高度)值减1,控制外层循环次数 00021410 jne main+1D3h (213F3h) //当此值仍非零时,继续外层循环
其中黄色覆盖区域为外层循环,绿色覆盖区域为内层循环。
可以看出三个版本的内层循环操作上并没有什么差异,Release模式下编译器已经将循环内计算地址中的乘法计算优化成加法,而我们第三个版本的目的正是去掉乘法计算,因此三者执行效率上并没有多大差异。
下面给出完整的测试代码:
#include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include <iostream> using namespace cv; using namespace std; inline void InvimageV1(uchar *A ,uchar *B,int Width,int Height) { for (int Y=0;Y<Height;Y++) { for (int X=0;X<Width;X++) { B[Y*Width+X]=255-A[Y*Width+X]; } } } inline void InvimageV2(uchar *A ,uchar *B,int Width,int Height) { uchar *LinePS,*LinePD; for (int Y=0;Y<Height;Y++) { LinePS=A+Y*Width; LinePD=B+Y*Width; for (int X=0;X<Width;X++) { LinePD[X]=255-LinePS[X]; } } } inline void InvimageV3(uchar *A ,uchar *B,int Width,int Height) { uchar *LinePS=A,*LinePD=B; for (int Y=0;Y<Height;Y++) { for (int X=0;X<Width;X++) { LinePD[0]=255-LinePS[0]; LinePS++; LinePD++; } } } int main() { Mat src,dst; int nWidth,nHeight,iterNum=1000; uchar *pSrc,*pDst; int64 t1,t2; src=imread("1.jpg",0); dst = src.clone(); nWidth=src.cols; nHeight=src.rows; pSrc=src.data; pDst=dst.data; imshow("原始图",src); t1=getTickCount(); for (int i=0;i<iterNum;i++) { InvimageV1(pSrc ,pDst,nWidth,nHeight); } t2=getTickCount(); cout<<"InvimageV1:"<<(t2 - t1)*1000./getTickFrequency()<<"ms"<<endl; imshow("InvimageV1",dst); t1=getTickCount(); for (int i=0;i<iterNum;i++) { InvimageV2(pSrc ,pDst,nWidth,nHeight); } t2=getTickCount(); cout<<"InvimageV2:"<<(t2 - t1)*1000./getTickFrequency()<<"ms"<<endl; imshow("InvimageV2",dst); t1=getTickCount(); for (int i=0;i<iterNum;i++) { InvimageV3(pSrc,pDst,nWidth,nHeight); } t2=getTickCount(); cout<<"InvimageV3:"<<(t2 - t1)*1000./getTickFrequency()<<"ms"<<endl; imshow("InvimageV3",dst); waitKey(0); }