汇编学习:二维数组遍历

作为正式接触汇编的开篇,本文将研究二维数组的遍历问题。在图像处理中,通常需要遍历图像像素(即二维数组)。下面给出三个版本的遍历函数,并研究他们的汇编代码(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);
}
View Code


posted @ 2016-10-07 23:49  xiaoluo91  阅读(2017)  评论(0编辑  收藏  举报