更少更优的汇编指令
所有的程序最后都要以二进制指令的形式在硬件上运行(包括Java,C#这些运行在虚拟机上的语言,只不过虚拟机隐藏了这些细节),也就是说谁有最优的汇编指令谁就在同等情况下有更快的速度,更优有两个方面,第一指令数量更少,第二指令的效率更高(如寄存器直接自增的速度会明显快于取值自增写回)。当然汇编的好坏和编译器有关,但是本身高级代码的好坏也决定了最后优化出来的汇编指令的上限。
尽可能熟悉语言特性与实现
熟悉语言每一个操作的代价
值传递
作为一门多范式语言,C++在这方面是最复杂的,同时具有引用传递,指针传递,值传递,完美转发多种模式,但是在不同情况下这些操作的代价是完全不同的,值传递的代价最大,引用传递的代价最小,但是引用传递有的时候又会出现空引用。但是我们上面的所有讨论都指出一个问题,即尽量不要用值传递,除非万不得已。接下去的两个例子也给出了关于传值的一些优化建议。
//值传递
template<typename dataType>
dataType function(dataType data)
{
return data;
}
//引用传值
template<typename dataType>
dataType& function2(dataType& data)
{
return data;
}
//完美转发
template<typename dataType>
dataType&& function2()
{
dataType data;
return data;
}
//一个导致悬空引用的示例
template<typename dataType>
dataType& function3()
{
dataType data;
return data;
}
对于Java/C#来说这方面的问题比较小,但是我们也可以来聊一下,有的时候也会导致性能的损失。java/C#(safe)的值主要有两种形式,一种在堆上一种在栈上,堆上的值由gc进行管理,栈上的值受到生命周期的影响,自动展开解退。但是性能的瓶颈恰好出现在这里,如果从c/c++的视角来看,在堆上的数据或者说Java/C#的引用本质上就是会自动解引用的指针。Java除了八大数据类型之外都是以指针的形式间接寻址的(由指针地址获取指针本身<栈>再由指针存储的地址访问具体数据<堆>)实际上性能也就在额外的访存中被浪费看下面一个例子,我将几个类写在一个文件中
class data
{
int i;
int j;
data(int i_,int j_) //为了和Java兼容我就不用初始化器了
{
i=i_;
j=j_;
}
};
class Main
{
void fun(data a)
{
for(int s=0;s<100000;s++)
{
a.i+=1;
}
}
static void Main(string[] args)
{
data a=new data(1,2);
fun(a)
}
};
实际上我们会发现,在这里每访问一次数据,但需要访存多次,如果将i变为一个局部变量,实际上不会改变封装性(还是在main类中可见)但是从栈上直接获取,明显由更好的性能,在超量的对数据的访问过程中,这实际上有可能成为性能瓶颈。
是否需要那么多高级特性
每一个特性都是需要代价的,我们在编写我们自己的程序的时候是否需要那么多特性是值得商榷的,这句话换到Java中就是我们是否真的需要框架,框架是可能的范式的集合,但是据我所知很多制造业公司的业务根本不需要那么多范式,手写一个简单的模型加上redis中间件最后加上一个后端数据库就够了,高并发,分布式统统不存在。使用一个框架就需要为这个框架付出相应的代价,但是在访问量不大的情况下框架反而是累赘。
下面有几个例子,很多都是实例
1.我们姑且称这个程序员为小A,小A是科班出身的,有很棒的计算机基础,但是研究C++特性走火入魔恨不得在程序中把所有知道的C++特性全部写上去,在应用代码中模板满天飞,当然最后出来的代码的性能处于快慢大小二相性,C++中大量的特性用于加速,相应的也有大量的特性会拖慢速度,同时模板会导致C++代码的大小急剧膨胀。
2.小B是培训班出身的Java程序员,言必称框架,请求这位先生写一个物联网路由服务器(客户定制,链接大型设备最多不超过30而且是持续性链接,数据频率5HZ/台),最快的办法就是手搓一个reactor模型,每台设备单独开一个线程,线程池和redis都不要,加上Linux提供的epoll,2G的服务器保证跑的飞起,这位先生还在想spring到底要怎么配置。
也就是说很多时候我们需要评估我们使用的每一个特性,这些特性我们需要付出的代价是什么,对于这个项目是否是划算的。
从特性角度进行优化
协程——更大限度的并发
目前从语言级支持协程的包括go,C#,C++等,当然这些语言可以分为两类,分别是C#,C++的无栈协程和go的有栈协程,但是协程的目的殊途同归,为了能有消耗更小的并发。但是协程从来就不是万能的,协程的切换也是需要付出代价,对协程的使用必须谨慎,否则反而会成为程序运行的瓶颈。
编译期计算
这主要是针对C++的,C++提供了关键字用于实现编译期计算,同时C++模板本身就是图灵完备的(比较走火入魔),也就是说很多有实际具体数据的计算任务完全可以使用模板元在编译期间完成,尤其是C++现在在探索浮点类型也可以作为模板元参数(更走火入魔了)。我们来看一个在编译期计算的例子
#include<iostream>
template<int N>
struct test
{
constexpr static int value=N*test<N-1>::value;
};
template<> //模板特化
struct test<0>
{
constexpr static int value =1; //当n特化为0的时候value=1
};
int main()
{
std::cout<<test<10>::value<<std::endl;
}
当然没事别这么用,同事揍你我不负责。
尽可能用全局变量和静态变量
在多泛式语言和面向过程语言如C,go,C++,rust,python中尽可能使用全局变量,局部变量的周期性决定了局部变量必须要不停的在内存中构建释放,这也是对性能的一种消耗。
在面向对象语言中如java,c#中尽可能使用静态变量,在这两种语言中,创建一个短生命周期的对象消耗会远大于上述的几种语言,同时大量的创建放弃也会导致内存的碎片化,进而引起虚拟机对内存的重整或者全局gc,进一步拖慢运行速度。
减少使用反射等运行时特性
Java的反射是在运行是进行额外操作获取对应的类型,虽然方便,但是需外付出额外的开销,c++中的虚函数也是同理,需要额外的空间进行存储,也需要额外的空间和指令用于实现虚函数,如果想要实现性能更优异的代码,能够不使用这些特性尽量不使用。
unsafe不是洪水猛兽
在C++的扩展语言或者进化语言如C#,rust中都有unsafe选项,即代码不受gc控制,或者不做安全性检查,但是这不意味着unsafe是洪水猛兽,到目前为止还是必须承认C++是目前功能最齐全,控制最精细,运行速度最快的语言,虽然有那么点走火入魔。所有对安全所作出的努力都有代价,包括gc,包括rust的检查点,都会拖慢代码运行,在性能高度敏感的地方使用unsafe甚至是使用内联汇编是一个正确的选择,而作为一个程序员也需要具有一定的内存管控能力,毕竟也不是C++,通篇内存自己控制。