面向对象之后增加的开销

Posted by zhangxiaojian on October 31, 2014

这半个月作业爆表,很多作业无甚意思,不做不行,做了浪费时间。于是尽量将作业写的有点内容,不至于应付差事,也不至于浪费时间。本文就是作业之一,要求写程序对比面向对象语言在增加了面向对象的特性之后,在时间上和空间上有什么变化。正好借此整理Inside the C++ object  model中的一些内容。

前言:

面向对象设计思想对于软件工程的意义非同一般,从结构化程序设计到面向对象,使得大型软件设计,开发,解耦,维护等等变得更加可操作。从语言层面上来讲,面向对象的设计方法仅仅是一个指导思想,并不一定要求拥有面向对象特性的语言才能实现。我想纯c的代码,也完全能够写出c++面向对象的效果。就是需要额外做很多工作。显然这些工作难度很大,并且是可重复利用的。因此不如加入到语言的特性之中,交给编译器去解决复杂的,重复的工作。但是计算机中总是存在着平衡,功能的增加就意味着性能的降低。c++编译器为了支持面向对象语言特性,会默默的为使用者“做很多事”。或多或少,都会增加一些时间或是空间上的开销。

Part1:空间开销

举一个例子,假设我们现在需要写程序模拟数学中的点,线,面的坐标。点在坐标轴上只有一个值就可以确定,线和面分别由两个点和三个点确定。当然还需要一些对它们操作的函数,完成一些计算任务。先以结构化设计方法实现:

#include<iostream>

struct Point
{
    float x;
};

struct Point2D
{
    float x;
    float y;
};

struct Point3D
{
    float x;
    float y;
    float z;
};

//some operating functions here.....

int main()
{
    std::cout<<"sizeof(Point): "<<sizeof(Point)<<std::endl;
    std::cout<<"sizeof(Point2D): "<<sizeof(Point2D)<<std::endl;
    std::cout<<"sizeof(Point3D): "<<sizeof(Point3D)<<std::endl;
}

上述程序执行结果:

inside1

在结构化程序设计中,函数和数据是分离开的,因此函数不会影响struct大的空间大小。在此暂时省略。从上面的程序中可以发现,三个结构中重复的定义了x,y。而且线本来就是由连个点组成的。面向对象的设计方式就可以解决上述冗余的定义实现重用。不如先画一个类图:

inside2

其中point中y,z和mult函数都是virtual,为了留给子类去实现,因为virtual函数对类的空间大小是有影响的,所以这里直接加上方法。代码实现如下:

#include<iostream>

class Point{
    public:
        virtual ~Point(){}
        virtual Point& mult(float) = 0;
        float x() const{ return _x; }
        virtual float y() const { return 0; }
        virtual float z() const { return 0; }
    protected:
        float _x;
        Point( float x = 0.0 ):_x(x){}
};

class Point2D : public Point{
    public:
        Point2D( float x = 0.0,float y = 0.0 ):Point(x),_y(y){}
        ~Point2D(){}
        Point2D& mult(float){ return *this;}
        float y() const { return _y; }
    protected:
        float _y;
}; 

class Point3D:public Point2D{
    public:
        Point3D(float x = 0.0,float y = 0.0,float z = 0.0):Point2D(x,y),_z(z){}
        ~Point3D(){}
        Point3D& mult(float){ return *this;}
        float z(){return _z;}
    protected:
        float _z;
};

int main()
{
    std::cout<<"sizeof(Point): "<<sizeof(Point)<<std::endl;
    std::cout<<"sizeof(Point2D): "<<sizeof(Point2D)<<std::endl;
    std::cout<<"sizeof(Point3D): "<<sizeof(Point3D)<<std::endl;
}

上述程序执行结果:

inside3

显然,内含相同大小数据成员的struct和class确拥有不同大小的空间。需要说明的是,class空间较大并不是因为函数声明在类中,对象的函数也是定义在”别处”,使用的时候去寻址然后调用。实际上class类空间大于struct,是因为class为了支持多态性,产生了一个名叫虚函数指针的东西。它位于类空间的头部或尾部(根据编译器而定)。因此,增加了一个指针类型大小的空间,在32位机器上是4字节。

如果以为这就是增加多态性质后增加的空间大小,那就太年轻了。class中的虚函数指针指向一个在编译期间产生的表空间,名叫虚函数表。表中存的是虚函数的函数指针地址。如下图在vs2010中显示的Point2D树状结构图:

inside4

其中_vfptr就是虚函数指针,而下面的数组就是它指向的虚函数表。对应Poiont2D中四个虚函数的地址。所以增加多态性质之后,相对于结构化程序,额外的存储空间还要加上

                      虚函数个数 * sizeof(int*)+1

其中+1,是虚函数表额外存放的typeinfo信息。在vs2010中存在虚函数表的末尾。

C++而言,支持多重继承,那么情况就更要复杂一些,空间开销也要比单继承更大。但原理也都差不多类似。并且许多纯面向对象的语言,都摒弃了多继承的特性。这里就不做深究了。

Part2:时间开销

时间开销则复杂的多,涉及的方面很广。例如类中data member是用编译期间产生的offset来访问,当涉及到多重继承还要增加多余的代码来根据offset移动this指针;当函数是virtual,根据Part1中的结构,需要先访问虚函数表,再定位到类型相关的函数;不同于结构化函数传参式的方式,面向对象的函数通过this指针来确定可以访问的member data,等等。

这里对比单一继承的虚函数与结构化调用的开销,首先是结构化的代码,在Part1代码基础上添加一个方法:

void calculate(Point3D* pd)
{
    float temp;
    for(int i = 0; i < 100000000; ++i)
    {
        temp = pd->x * pd->y * pd->z;
    }
}

//在mian函数中添加调用

Point3D pd;
pd.x = 1.01;
pd.y = 2.02;
pd.z = 3.03;

int start = clock();
calculate(&pd);
printf("%.3lf
        n",double(clock()-start)/CLOCKS_PER_SEC);

最终耗时:0.220 s

接下来是面向对象的方法,面向对象的核心在于运行时期根据实际的类型,调用对应的函数。为了公平起见,还是将calculate当作普通函数,与上述相同。代码稍作修改如下:

void calculate(Point3D* pd)
{
    float temp;
    for(int i = 0; i < 100000000; ++i)
    {
        temp = pd->x() * pd->y() * pd->z();
    }
}

//在mian函数中添加调用
Point3D *pd = new Point3D(1.01,2.02,3.03);

int start = clock();

calculate(pd);

printf("%.3lf
        n",double(clock()-start)/CLOCKS_PER_SEC);

最终耗时:7.617 s

两者相差将近36倍之多。

总结:

说明了面向对象特性会在时间,空间上有所增加,上述例子选取的主要是单继承下增加了多态性之后的开销。多继承在各个方面的开销都会更大。其余很多方面的开销也许不如增加虚函数之后明显,但也确实存在。相反,面向对象在软件设计方面带来的好处在大型软件开发中很明显,软件模式,设计模式等等的出现,都是基于面向对象模型提出的软件设计方法。因此,随着硬件越来越强大,开销相对于大型软件开发,维护来说,也许是可以忍受的。

本来是要求写成实验报告的形式,什么实验目的,实验步骤,bulalala。。。还是写成了博客的形式,让老师凑合着看吧。已经尽力了~