Overview

之前跟Young一起做上一个项目时,遇到和解决了很多小的但是又很常见的编程问题,做完项目想总结一下的时候又发现这是一系列问题,没有太多明确的界限,又好像存在于编程的每一个细节中,我觉得应该统称为编程的基础。
一直说要写,可总觉得无从下笔。现在下决心尝试一下,想到哪里就写到哪里吧~

1. 计算机的结构

现在的计算机依然采用的是冯·诺依曼提出的计算机体系结构。计算机由控制器、运算器、存储器、输入设备、输出设备五部分组成。其中控制器和运算器一起组成了CPU,存储器可以分为内存和硬盘,输入设备就是鼠标键盘麦克风等,输出设备主要是显示器一类的设备。

1.1 内存和硬盘的区别:

写这个的时候我内心是挣扎的,因为可能一眼看起来显得很low,但是这确实是我在接触计算机的过程中遇到的一个困惑,在这里记下来,说不上能解决谁的困惑呢。

简单来说,内存和硬盘都是用来存储东西的,只是由于他们的材质不同,导致他们存储东西的失效和存取效率不同,因此在计算机中的用途也就不同。

内存的主要材质是RAM(Ramdom Access Memory,即随机存取存储器),特点是存储空间小,高速存取,读写时间相等,且与地址无关(即可以随机存取),易挥发性。但是存储信息具有瞬时性,断电之后信息就会完全丢失,这就是为什么台式机忽然断电,你正在编辑的东西如果没保存就全没了,除非你已经保存到了硬盘上。

与RAM容易混淆的是ROM(Read Only Memory,即只读存储器),从它的名字就可以知道,ROM与RAM最大的区别在于ROM是只读的,但是断电之后信息不丢失,因此通常被用来做计算机启动用的BIOS芯片,存取速度相对于RAM来说比较低。
这里说的ROM的只读,并不是说完全不能写,BIOS里的信息不就是写进去的嘛,而且我们经常也会听到刷BOIS,其实就是把BOIS的信息更改,也就是更新ROM中的信息。只是相对于RAM,ROM不适合频繁写而已。

硬盘的特点是存储空间大(现在家用硬盘都是1T+),按址存取,存取速度慢,永久保存数据。在出现固态硬盘前,机械硬盘就是硬盘的代名词,跟RAM可以说是对立的存在。
机械硬盘的工作原理跟CD,唱片机等一样,就是一个盘在转,有一个接触盘的磁头读取接触到的区域的信息。只是机械硬盘是一个立体的结构,里面有很多层类似于盘的面,信息的读取就是靠磁头的移动和磁盘的转动实现。知道了机械硬盘的结构,也就能理解它的特点了,因为磁头要动,盘面要转动,所以机械硬盘的动静很大,发热多,不耐摔(一不小心就把磁头摔坏了)。 速度也很大程度上取决于盘面的旋转速度,盘面旋转速度越快,读取信息自然也就越快,买机械硬盘时,硬盘转速可以说是最重要的指标,5400转/秒(没有更低的了),7200转/秒(主流),12000转/秒(企业级硬盘)。

固态硬盘的存在可以说是内存和机械硬盘的折衷,存储空间没有机械硬盘大,但是可以实现随机读取,性能上相对于机械硬盘有了质的飞越,由于固态硬盘没有了磁盘和磁头这些东西,所以机械硬盘那一系列缺点固态硬盘都没有,一定要说缺点的话,那就是存储空间还不够大,贵。不过从它的缺点来看,它应该是未来的趋势,毕竟有些缺点代表过去,而价格贵空间不够大这些问题,未来多半都能解决,从CPU,硬盘的发展我们已经无数次的看到了这一点。

一句话解释就是,开着计算机,操作系统和其他程序运行时需要的东西都要load到内存中,想要永久保存的东西,都需要从内存中写到硬盘上。

既然所有的程序都是CPU计算,内存只是帮CPU存着结果,那一个程序运行时,CPU和内存以及硬盘又是怎么协作的呢。

1.2 CPU、内存和硬盘

在任何一本计算机组成原理教材中,输入和输出设备都是很简略的,因为真的没有太多需要说的,如果一定要说,我更想解释机械键盘,高清显示屏这些东西对程序员的重要性~

一个程序在运行时(不考虑输入和输出),实际上就是CPU、内存和硬盘的协作过程。 计算机要执行一个程序,需要先把这个程序,程序运行需要的数据等从硬盘导入到内存中,运行结束之后,产生的数据还需要重新写入硬盘永久保存。因此,内存可以看做是CPU和硬盘之间的混缓冲池,CPU是不会直接和硬盘交换数据的,因为硬盘读取的速度太慢了。

但是CPU和内存之间的交换依然存在问题,因为内存这些年的发展也就是变大变大变大,在读写速度上没有什么改进,毕竟材质摆在那里。而CPU的发展日新月异,不只主频越来越高,CPU核数也越来越多,计算能力越来越强,单位时间可以计算的数据量也就越来越大。CPU实际在做运算时,是需要把数据从内存load到CPU中的寄存器中的,随着CPU性能的提升,单位时间在寄存器和内存之间传输的数据量也越来越大,一边算一边取肯定不行,因为CPU就要闲着等数据了。所以CPU在寄存器和内存之间也设置了一个缓冲,那就是CPU缓存。这个缓存是在CPU中的,用来预取内存中的数据,供CPU使用,这样CPU在使用时就不用从内存中获取数据了,而是先从CPU缓存中获取数据。

CPU缓存对CPU性能的影响是至关重要的,CPU读取数据的方式变成了:先去缓存中找,没有命中,再去内存中取,如果缓存中有,就可以直接取到放到寄存器中。如果CPU缓存频繁的不命中,就变成了每次CPU读取数据,都多做了一次无用功,因此CPU缓存的命中率十分重要。不过现在这部分已经相当成熟了,CPU的缓存命中率可以达到95%以上,甚至更高(99%)。

CPU缓存的设计很直观,最早的时候只有一级缓存,CPU在缓存中找不到想要的数据就得去内存了。而且为了保证缓存命中时的速度,一级缓存通常都很小(不然弄个足够大的,程序需要的数据都预取过来,肯定命中啊,但是命中之后取数据的速率会降低),因此命中率不是很高,再后来就有了2级缓存,比一级缓存大,但是存取速度比一级缓存慢一点点,但是肯定比内存存取快得多,这样CPU在一级缓存中找不到的时候,会再去二级缓存找,还找不到的话才会去内存中找。二级可能还不够用,后来CPU又有了三级缓存。当然,也不是越多越好,通常三级缓存就足够了,既保证了命中率,又不至于影响存取速度。
下面是一个主流的Intel CPU的部分配置:

Intel 酷睿i7 4790K:
CPU主频    4GHz
核心数量    四核心 
线程数    八线程
制作工艺    22纳米
三级缓存    8MB 

可以看到,三级缓存也不过8M,跟内存的大小相比,简直可以忽略不计。

跟存取有关的另一个问题就是程序的局部性原理

程序的局部性原理是指程序总是趋向于使用最近使用过的数据和指令,也就是说程序执行时所访问的存储器地址分布不是随机的,而是相对地簇集,这种簇集包括指令和数据两部分。
程序局部性包括程序的时间局部性和程序的空间局部性。
    1. 程序的时间局部性: 是指程序即将用到的信息可能就是目前正在使用的信息。
    2. 程序的空间局部性: 是指程序即将用到的信息可能与目前正在使用的信息在空间上相邻或者临近。

这种局部性很大程度上是由循环语句造成的,在编程中,循环的比例极其高,而循环使得程序具有很好的局部性:程序指令反复执行,数据存储很有规律性。良好的编程规范会极大地提升缓存的使用效率,而缓存能够拥有高命中率的基础也正是程序的局部性原理。

这也是GOTO语句有害性的主要依据,GOTO语句使得程序的执行变得很难提前预测,增加了缓存预测的难度,因此GOTO语句在很多语言中都成了过去。

这里不再展开讲编程对CPU缓存以及内存的影响,有兴趣的话可以自己搜索~

1.3 内存的使用

不同操作系统中对于内存的使用是完全不同的:

  • Windows是按需使用,你打开QQ,操作系统就会把QQ程序和需要的数据等载入内存,你关了QQ,Windows就会把QQ使用的内存释放掉,所以Windows的内存使用量就是实际的使用量,如果你的内存使用比很高,说明你的内存应该升级了。
  • Mac采用不同的内存使用策略,那就是你打开一个程序之后,就算你已经关了这个程序,这个程序实际上依然在内存中,这样当你下次再打开的时候,这个程序响应时间会非常短,就好像你本来就开着一样。实际上它确实一直就开着,常驻内存,只是你不知道而已。
    当然,当开的程序足够多时,内存总是会被占满,这时候Mac系统会用一个内存压缩技术把已经不用的程序压缩,给新程序腾出空间。
    所以,买Mac的话内存配置高一点总是好的,不会浪费。

实际上,无论什么系统,计算机配置多大内存,内存总有占满的时候,那么当内存不够用的时候,操作系统就需要将一部分程序的内存腾出来给新的程序用,这部分内存储存的信息会被写到硬盘上暂存,这块硬盘上的区域有一个专有的名字,叫做交换区(Swap Area)

在linux下,使用free -h命令就可以看到下面的信息:

             total       used       free     shared    buffers     cached
Mem:          992M       909M        82M        14M       133M       216M
-/+ buffers/cache:       560M       432M
Swap:           0B         0B         0B

其中的Swap就是交换区的大小,一般来说Swap used是0,说明内存充足,如果used很大,说明内存不够用,频繁有程序被换出内存。

如果内存不够用了,需要将别的程序占用的内存放到交换区,那么操作系统应该怎么操作呢?
我们举一个极端点的例子,假设程序可用内存为500M,程序A需要使用300M,程序B需要使用250M,那么如果A在运行时,B也运行,系统应该怎么办。

直观的想法就是把A换出去,把B放进来。这样做会有两个问题:一是不管是A在运行,还是B在内存中运行,实际上都是有很多空闲内存未使用的;二是现在的系统都是分式运行的,每个程序都分配时间片,如果A和B同时运行,那么程序会A和B交替运行,这样A和B在内存中换来换去,效率极低,显然是不合理的。

解决的方法就是把内存划分成小份,每个程序拥有很多份内存,就算需要换出去,也只需要换出一部分就可以了,这个思路被称为分页,操作系统会将内存划分成一样大小的小份,每一份被称为一个页面,分页对用户程序是透明的,是一种物理结构的内存划分。这种内存管理方式也叫分页存储管理。但是分页系统也会有别的问题,假设页面大小是4KB,一个程序占用内存10K,那么就需要分配给这个程序3个页面,第3个页面就有2K的空间是没有使用的,也就是有2K的空间是被浪费的,这些被浪费的空间被称为内存碎片,因为碎片是在页面里面的,所以分页产生的内存碎片也叫内碎片

可以看到,页面既不能太大,也不能太小。太小的话一个程序分到的页面会特别多,不方便管理,页表也会很大;太大的话,最后一个页面利用率低,可能产生很多内存碎片,浪费内存空间。

有了页表管理之后,就实现了对内存的细粒度管理,给程序分配内存空间时也不需要连续的内存空间了(因为页表会索引分配的页)。另外,当内存满了之后,将内存交换出去的单位也是页面,使得交换内存的粒度也很小。

当然,还有段式内存管理,现代操作系统大多采用的也都是段页结合,这一就不再详细说明,可以参考 http://blog.csdn.net/wangrunmin/article/details/7967293

1.4 32位系统 or 64位系统

32位和64位的系统都需要配合CPU使用,如果你的CPU是32位的,那么装64位作用也不大。但是现在的CPU一般都支持64位系统。
两者的区别主要有两点,一是寻址能力,一是计算能力。

寻址能力:

  • 32位系统可以寻址的空间是$2^{32}$,即4G的空间,如果你使用了32位的系统,那么最多只可以用4G的内存,更多的内存装上也是无法识别的。
  • 64位系统可以寻址的空间是$2^{64}$,即16TB,当然这只是理论上而已。以Windows 7为例,家庭普通版能支持8GB内存,家庭高级版能支持16GB内存,而64位的Windows7专业版、企业版和旗舰版最高可支持192GB内存。

计算能力:
64位平台上的运行性能要远超过32位平台。原因在与CPU通用寄存器的数据位宽,64位平台是64位,而32位平台是32位,运算时理论上性能会相应提升1倍。实际上,在64位Windows7下运行32位的应用软件并不会让你感觉到性能的飞跃,只有64位的应用软件才能最大化发挥64位平台的优势。但显而易见,目前64位的应用程序在种类的数量上都要远低于32位平台。