高级语言的欺骗
1.内存
a.内存不是无限的
所有的高级语言,从C/C++到JAVA到go,到rust,Ruby等等都和操作系统一起编织了一个美好的谎言,任意一台计算机的内存大小都是无限的,只要程序申请就能使用。
但是实际的世界是残酷的,内存条大小是16G就不会变成32G,硬盘大小是1T就不会变成2T,操作系统用了虚拟技术和请求调页技术对内存大小进行扩充,而上层的高级语言也选择相信这一美好的谎言。
b.对内存各部分的读取速度是不一致的
高级语言同样隐藏了另外一个事实,从不同的位置读取数据的花销是不一致的
如图,一个代码可能从整个存储体系的任意位置读取指令和操作数,但是在存储结构中,越往下的数据存取速度越慢。很多地方会有这样一句话,叫只能从内存中获取数据用于程序运行,这句话对也不对,现代计算机操作系统会将部分数据向上移动到速度更快的cache中,也会在内存不足时将部分页面调入外存中,当需要时再将页面调入内存(即从整体数据流转上看其实是从外存中读取指令和操作数),甚至有些语言为了操作速度块,部分数据直接保留在寄存器中,一直到运算完毕再写回内存,因此实际上每条指令或者数据的获取速度是完全不一致的。
c.内存导致的性能瓶颈
高并发服务器是一个很好的例子,每一个TCP的socket都需要在内存中划定一片缓冲区,但是由于内存空间是有限的,所以当有超量的TCP连接被创建的时候,内存空间被严重占用,大量的内存页面被调入外存中,当其他程序需要运行时需要做大量的页面置换,导致一条指令的取指令时间严重延长,导致内存变为程序运行的性能瓶颈。
冯诺依曼瓶颈
2.CPU与编译器
a.CPU的核心是有限的
cpu的核心是有限数,如Inter core I9 13900K内核数 24 Performance-core(性能核)数 8 Efficient-core(能效核)数 16,即在多线程的时候哪怕是开无限个线程的性能永远也高不过24线程(纯计算,无IO),而且还会导致运算的性能下降。
b.CPU的指令执行是重叠的和乱序的
现代的CPU普遍使用指令流水,即多条指令同时在运行,每条指令执行不同的阶段,即一条指令的平均执行时间会远小于完整执行完一条指令的时间,同时从高级语言的视角来看,每一条指令是顺序执行的,但是为了维持指令流水的运行,有的时候编译器和CPU会做出一些很抽象的优化,目的就是为了能够维持指令流水的运转。
同时循环控制指令会打断整个指令流水
看下面一个例子
int j=1;
while(int i=i;i<10000000;i++)
{
j++;
}
int j=100000001;
即直接做编译期计算,将复杂度全部留在编译期
当然在没那么激进的优化的情况下会被展开
这是java的JIT的常见处理方式,减少指令跳转数量,减少总的数据指令数。
public class MIAN
{
public static test()
{
int j=1;
while(int i=1;i<1000000;i+=8)
{
j+=1;
j+=1;
j+=1;
j+=1;
j+=1;
j+=1;
j+=1;
j+=1;
}
}
}
当然我们用手写可以写出更符合指令流水的代码(用C++实现一下)
int j1=1;
int j2=j1+1;
int j3=j1+2;
int j4=j1+3;
int j5=j1+4;
int j6=j1+5;
int j7=j1+6;
int j8=j1+7;
for(int i=1;i<100000000;i+=8)
{
j1+=8;
j2+=8;
j3+=8;
j4+=8;
j5+=8;
j6+=8;
j7+=8;
j8+=8;
}
int j=j1+j2+j3+j4+j5+j6+j7;
这个代码很好的利用了指令流水线,由于之前的代码每一个语句都在等待上一个语句的结果,因此指令流水实际上是受阻的,这个代码将数据展开,互相之间不干扰,可以的运转指令流水(当然这个代码只是做一个示例,在实际工程中一般不会这么写,也不应当在非极端情况下吹毛求疵)
3.语句,编译器和操作系统
a.每条指令的开销实际上是不同的
1.语言的封装性
在越高级的语言中这个趋势越明显,我们看下面几个语句,从C到C++到Java
C语言
struct Int
{
int i;
};
int main()
{
Int data{1};
}
C++
struct Int
{
int i;
Int(int _i):i(_i){};
};
int main()
{
Int data{1};
}
由于Java没有结构体,只能用类
public class Int
{
int i;
Int(int _i)
{
i=_1;
}
}
public class MAIN
{
static void main(String arge[])
{
Int data=new Int(1);
}
}
很明显三者的花销是不一致的,C语言直接在栈上构建数据,直接赋值,C++在栈上构造且要掉用构造函数,Java不仅要调用构造函数还需要内存分派函数。
2.操作系统调用需要付出的代价远高于一般函数
对操作系统调用的代价远高于一般函数,首先需要将整个核心切入内核态,所有的寄存器全部替换,在内核态中执行后再切回用户态,复杂度相当于做一次完整的内核级线程切换。
这也就是为什么在性能优化的时候都建议使用内存池和线程池,即将所有的复杂度留在启动时,用启动时的复杂度与内存空间来换取运行时的高性能。