Ⅰ C/C++最底层是怎么实现的
许多同学可能在学习C++的时候,都会感到一定的困惑,继承到底是怎样分配空间的,多态到底是如何完成的,许许多多的问题,必须挖掘到C++底层处理机制,才能搞明白。有许多C程序员也并不认同C++,他们认为C++庞大又迟缓,其更重要的原因是,他们认为“C++是在你的背后做事情”。的确,C++编译器背着程序员做了太多的事情,所以让很多不了解其底层机制的人感到困惑。想成为一个优秀的程序员,那么这样的困惑就不应该存在,只有了解了底层实现模型,才能写出效率较高的代码,自信心也比较高。
我们先从一个简单但有趣的例子谈起。有如下的4个类:
class X {};
class Y : public virtual X {};
class Z : public virtual X {};
class A : public Y, public Z {};
上面的4个类中,没有任何一个类里含明显的数据,之间只是表示了继承关系,那么如果我们用sizeof 来测量它们的大小,将会得到什么结果呢?
你可能会认为,既然没有任何数据和方法,大小当然为0,而结果肯定会出乎你的意料,即使是class X 的大小也不为0。
在不同的编译器上,将会得到不同的结果,而在我们现在最常用的VC++编译器上,将得到如下的结果:
sizeof X 的结果是 1 。
sizeof Y 的结果是 4 。
sizeof Z 的结果是 4 。
sizeof A 的结果是 8 。
惊讶吗?那么为什么会得到这样的结果?让我们一个一个来分析。
对于一个空的class,事实上并不是空的,它有一个隐晦的1 byte ,那是被编译器安插进去的一个char 。这使得这个class 的两个对象得以在内存中配置独一无二的地址,这就是为什么 sizeof X 的结果是 1。
那么Y和Z呢,怎么会占用 4 byte ?其实它们的大小受到三个因素的影响:
1. 语言本身所造成的额外负担:当语言支持多态时,就会导致一些额外负担。在派生类中,这个额外负担表现在一个指针上,它是用来指向一个被称作“虚函数列表”的表格。而在VC++编译器上,指针的大小正好是 4 byte 。
2. 编译器对于特殊情况所提供的优化处理:在class Y 和 class Z 中,也将带上它们因为继承class X 而带来的 1 byte ,传统上它被放在派生类的固定部分的尾端。而某些编译器(正如我们现在所讨论的VC++编译器)会对空的基类提供特殊的处理,在这个策略下,一个空的基类被视为派生类对象最开头的一部分,也就是说它并没有花费任何额外空间(因为既然有了成员,就不需要原本为了空类而安插一个char了),这样也就节省了这 1 byte 的空间。事实上,如果某个编译器没有提供这种优化处理,你将发现class Y 和 class Z 的大小将是8 byte ,而不仅仅是5 byte 了,原因正如下面第3点所讲。
3. “对齐”(Alignment)机制:在大多数机器上,群聚的结构体大小都会受到alignment的限制,使它们能够更有效率地在内存中被存取。Alignment 就是将数值调整到某数的整数倍。在32位计算机上,通常alignment 为 4 byte(32位),以使总线达到最大的“吞吐量”,在这种情况下,如上面所说,如果 class Y 和class Z 的大小为 5 byte ,那么它们必须填补 3 byte ,最终得到的结果将是 8 byte 。是不是开始感谢VC++编译器幸好有这样的优化机制,使得我们节约了不少内存空间。
最后,我们再来看看 class A ,它的大小为什么是 8 byte ?显而易见,它继承了class Y 和class Z ,那么它的大小直接就把 class Y 和class Z 的大小加起来就够了。真有这么简单吗?实际上这只是一个巧合而已,这是因为之前编译器的优化机制掩盖这里的一些事实。对于没有优化的 class Y 和class Z 来说,他们的大小都是8 byte ,那么继承了它们两个的 class A 将是多大呢?16 byte?如果你有这样的编译器试一下的话,你会发现答案是12 byte 。怎么会是12 byte 呢?记住,一个虚拟继承的基类只会在派生类中存在一份实体,不管它在 class 继承体系中出现了多少次!class A的大小由下面几部分决定:
l 被大家共享的唯一一个 class X的实体,大小为1 byte。
l 基类class Y 的大小,减去因虚拟继承的基类class X而配置的大小,也就是4 byte 。基类class Z的算法相同,它们加起来就是8 byte 。
l class A自己的大小,0 byte 。
l class A 的alignment的大小(如果有的话)。前述三项的总和是9 byte ,那么调整到4 byte的整数倍,也就是12 byte 。
我们前面讨论的VC++编译器得出的结果之所以是8 byte ,是因为 class X 实体的那1 byte被拿掉了,于是额外的3 byte也同样不必了,因此就直接把class Y 和class Z的大小加起来,得到8 byte 。
这个例子看懂了吗?是不是对C++的底层机制开始感兴趣了?那么我们再来举一个同样有趣的例子。
有这样一个类:
class A {
private:
int a;
char b;
char c;
char d;
};
它的大小是多少呢?
如果你有记得我之前提到的alignment机制的话,你应该会猜到它的大小是8 byte 。的确如此,int a占用4 byte ,char b , char c 和char d各占1 byte ,加起来是7 byte ,再加上alignment额外带来的1 byte ,总共是8 byte 。
瞧,就是这么简单,那么现在我们把里面的成员变量换换位置,如下:
class A {
private:
char d;
int a;
char b;
char c;
};
我们将char d拿到第一个位子,放在int a之前。那么现在你能告诉我class A的大小是多少呢?你肯定不会再猜8 byte了,因为你会觉得这与上面似乎有些不同,但你不能肯定到底是多大。不敢确定的时候就去试试吧,原来是12 byte ,这又是怎么回事呢?同样的类,只是改变了成员变量的位子,怎么就会多出4 byte的存储空间?其实这一切又是由变量的存储规则造成的。对于一个类来说,它里面的成员变量(这里单指非静态的成员变量)是按声明的顺序存储在内存空间中的。在第一种的情况中,它们紧紧的排列在一起,除了由于alignment所浪费的1 byte空间外,它们几乎用了最小的存储空间;而在第二种情况中,它们则不是排列得那么紧密了,错误就在于char d ,它一个人就占用了4 byte 。为什么它会占用4 byte呢,其实责任也不全在它,后面的int a也有不可推卸的责任。Int 型数据在VC++编译器中正好是占用4 byte的,等于一个alignment量,而这4 byte一定是密不可分的。当char d占用了1 byte后,其后空出了3 byte(对于一个alignment量来说),而一个int型数据不能被拆成3 byte +1byte来存储,那样编译器将无法识别,因此int a只有向后推到下一个alignment的开始,这样char d就独占了4 byte ,中间有3 byte浪费掉了。而后面的char b和char c依旧紧密排列,最后又由于alignment调整2 byte ,整个类的大小就变为了12 byte 。
看了这个例子,是不是该反省以前随意定义成员变量了?如果你要定义一个含3个int型数据和4个char型数据的类,本来最优化的方法只需要16 byte ,而你却随意的定义成如下的样子:
class F{
private:
char c1;
int i1;
char c2;
int i2;
char c3;
int i3;
char c4;
};
看看结果是什么,这个类竟然要占据28 byte的空间,比刚才整整大了12 byte!
再来看看继承的时候,成员变量是怎样存放的。我们将第2个例子中的class A 改成三层的继承模式,或许我们在做项目中,真的会遇到这样的情况。
class A1{
private:
int a;
char b;
};
class A2: public A1{
private:
char c;
};
class A3:public A2{
private:
char d;
};
现在我们来预测一下class A3 的大小,是8 byte吗?不,结果竟是16 byte ,竟然整整多了1倍。这是为什么呢?按照成员变量的排列顺序,int a,char b,char c,char d应该紧密的排列在一起,8 byte没错。但事实并非如此,这些都是因为继承而造成的。知道“在继承关系中,基类子对象在派生类中会保持原样性”吗?或许这样专业的一句话,你并不能明白是什么意思,那么听我下面的分析。在为派生类分配内存空间的时候,都是先为基类分配一块内存空间,而所谓的“原样性”是指基类原本在内存空间中是什么样子,那么它在派生类里分配的时候就是什么样子。拿这个例子来说,class A1占据了8 byte的空间,其中int a占4 byte ,char b占1 byte ,因alignment而填补3 byte 。对于class A1来说,占据8 byte空间没什么好抱怨的,但是class A2呢?轻率的程序员会认为,class A2只在class A1的基础上增加了唯一一个char c ,那么它应该会和char b绑在一起,占用原本用来填补空间的1 byte ,于是class A2的大小是8 byte,其中2 byte用于填补空间。然而事实上,char c是被放在填补空间所用的3 byte之后,因为在class A2中分配的class A1应该完全保持原样,于是class A2的大小变成12 byte ,而不是8 byte了,其中有6 byte浪费在填补空间上。相同的道理使得class A3 的大小是16 byte ,其中9 byte用于填补空间。
那么也许你会问,既然“原样性”会造成这样多的空间浪费,那么编译器为什么还要这样做呢?其实这样做是有它的必要的。我们考虑下面这种情况:
A1* pA1=new A1();
A1* pA2=new A2();
*pA1=*pA2;
我们定义了两个A1型指针,一个指向A1对象,一个指向A2对象。现在我们执行一个默认的复制操作(复制一个个的成员变量),那么这样一个操作应该是把pA2所指的对象的A1那部分完全复制到pA1所指的对象里。假设编译器不遵循“原样性”,而是将派生类的成员和基类的成员捆绑在一起存放,去填补空间,那么这样的操作变会产生问题了。A1和A2都占8 byte ,pA2会将其所指的8 byte空间里的内容全部复制给pA1所指的对象,那么pA1所指的对象本来只有2个数据,3 byte的填补空间,而复制后却变成了3个数据,2 byte的填补空间了,对于char c ,我们并不想把它复制过来的。这样完全破坏了原语意,而这样引起的bug几乎是无法察觉的。
Ⅱ 计算机底层算法是什么
计算机最底层的原理是2进制的,只有1(通电)或0(断电),计算机通过大量的与门、非门、或门、异或门、异非门来计算的。
Ⅲ 如何入手学习android 底层开发
android 底层开发学习:
一、基于Android的CPU+GPU的异构编程开发,目前主要有以下几种平台:
1. OpenCL
在桌面系统和大规模并行计算领域被普遍使用的一种底层API。最近一段时间,主流的芯片厂商的旗舰或准旗舰芯片都开始支持OpenCL1.1或者1.2标准,包括高通,三星, 联发科,Rockchip等厂商的芯片,都可以找到OpenCL的支持。
2. CUDA
目前只有NVIDIA自己出的基于Tegra K1芯片的设备(NVIDIA Shield)支持CUDA,所以支持的面比较窄。
3. RenderScript (RS)
Google力推的异构编程,宗旨是由平台帮你选择运行的处理器,也就是说你是不知道你的程序跑在CPU还是GPU上的,这是由系统的驱动来决定的。想法是美好的,可现实是开发者并不买RS的帐, 大家觉得RS的性能不可控,灵活性太差,其文档之缺乏也被人诟病;此外,芯片厂商对于RS的优化都还普遍处于比较低阶的水平,这些都导致了RS在实际应用中很少被用到。
二、由于OpenCL的普及程度,以下部分只针对OpenCL展开。
目前支持OpenCL的设备和芯片
1. 支持OpenCL的GPU
高通几乎全系的GPU, 包括但不限于以下GPU (Adreno 305, 320, 330, 405, 420, 430, 530 ...)
ARM Mali的6系和7系GPU, 比如T628, T760
2014年以来较新的Imagination PowerVR GPU,比如G6430
2. 支持OpenCL的芯片。以下是一个很粗略地列举了主要的支持OpenCL的芯片。
高通8064, 8974(骁龙800,801), 8084(骁龙805), 8994(骁龙810)等
三星 Exynos猎户座 5420, 5433 (内置ARM Mali GPU)
联发科 MT6752 (内置ARM Mali T760 GPU)
瑞芯微 RK3288 (内置ARM Mali GPU)
3. 支持OpenCL的手机和平板。 这个就数不胜数了,下面只随手给出几个例子以供参考。(注意:Google Nexus系列的手机或平板,虽然硬件上支持OpenCL,但因为删掉了OpenCL的驱动程序,所以基本都不支持OpenCL;值得注意的是,据国外blog上报道,可以将相应的OpenCL驱动推送回设备以重新开启OpenCL的支持, 详见maxlv.net 的页面)
三星 Galaxy S4, S5, S6, Note 3, Note 4
LG G2, G3, G4
HTC One M7, M8, M9
小米使用高通芯片的手机和平板
魅族M1 Note
台积电P90HD
等等等等。。。。
如果不确定手头的设备是否支持OpenCL, 可以使用OpenCL-Z Android进行检测,这款软件可以显示详细的OpenCL的设备信息,同时运行micro-benchmark检测设备的计算能力。
三、需要掌握的知识:
1. 简单的GPU基本知识
2. OpenCL并行程序设计
3. Android NDK知识
4. Android JNI接口的编写
5. 简单的Android程序开发知识
四、开发的步骤(这里只是步骤的精简版本,只阐述操作,不进行解释):
1. 编写OpenCL的C/C++程序实现GPU的核心计算代码
2. 用Android NDK编译之前写的C/C++代码。这一阶段可以在纯C/C++环境下工作,可以编写main函数测试实现的功能,用NDK将代码编译为可执行的代码(BUILD_EXECUTABLE), 然后用ADB将可执行程序推送到设备上运行。运行可执行程序要求设备具有root权限,如果没有root权限,可以通过Native Program Launcher (AndroidNativeLauncher · GitHub,可能需要翻墙) 这一工具在设备上执行二进制代码。
3. 上一阶段测试结束,功能基本正常。开始编写JNI接口。
4. 开始编写Android应用程序,使用JNI封装native函数。编译C/C++代码成动态链接库。
5. 在Android程序里,以静态方式加载上一步编译的动态链接库。
6. 在需要的地方(比如点击按钮事件),调用相应的native函数,即可实现相应的功能。