C语言本身的性能几乎是所有语言中最高的,当然,这要除去机器语言(二进制?)和汇编语言。使用C语言开发,本身已经可以获得极高的程序执行效率,但是,不同程序执行得到相同的结果,同一种语言不同方式的编码效率也不尽相同。使用C语言开发可能开发的效率并不如一些本身封装好很多库的语言开发效率高,例如java,python,但是C语言同样可以将一些常用数据结构封装成常用接口,以此来实现相关的库函数功能,当然这是自定义的,例如,我们可以自定义hash创建,这类似于C++语言中的map,使用经典红黑树,二叉树存储数据结构,我们同样可以实现与C++ STL 库一致的功能。在一套代码中封装C语言的“库”能极大地提升开发的效率,例如创建单链表,创建双向链表,创建hash表,所有这些常用的数据结构,都可以封装成通用接口 。
言归正传,我们来简单谈一下C语言在咖啡过程中,基于语言本身能实现的优化。出去算法之外,编码本身也是 影响效能的一部分。
在通信公司做开发,从代码中能学习到不少东西,特别是每次项目验收时代码鉴定,好的鉴定结果能让你受益匪浅。
1.指针扩展不要超过1级
struct TA
{
int a;
int b;
int c;
};
struct TB
{
struct TA *pA;
};
void TEST_1(TB *p)
{
p->pA->a = 1;
p->pA->b = 2;
p->pA->c = 2;
//p->pA //在汇编指令中被执行3次
}
void TEST_2(TB *p)
{
TA *pA = p->pA;
pA->a = 1;
pA->b = 2;
pA->c = 2;
}
上面的代码中看似区别不大,但实际上TEST_1中的指针扩展语句在汇编指令中被执行3次,在TEST_2中,之执行1次。
2.结构体复制入参问题
对于结构体,即便是不会修改结构中得到内容,如果允许,最好将函数入参设置为指针,我们都知道,函数入参时会在内存中为每个参数保存一个副本,而对于大型结构体,拷贝这样一个副本,是不值得的。指针也会进行保存副本,但是指针的大小在同一操作系统下是固定的,无论何种数据类型的指针的字节数都是4(32位系统)或者8(64位系统)。
3.结构体成员赋值问题“=”号与memcpy
我们知道,相同结构体之间是允许直接赋值的。当数据结构大小小于128字节的时候,用“=”号赋值比memcpy快具体原因轻看下面的链接: 等号赋值与memcpy的效率问题。
4.减少不必要 的强制类型转换
每一次强制类型转换,编译器都会将其编译为汇编指令,但是在本身不需要转换的地方使用,就会导致汇编指令冗余
例如:
(size_t) unsigned int //(32位系统)
(size_t) long unsigned int //(64位系统)
5.设计类型一致的比较操作数条件判断
基于第4 点,强制类型转换会生成汇编指令,因此在设计的时候,比较操作数最好设计成相同数据类型
6.短字符串/字符数组的初始化
对于字符数组初始化,用memset函数初始化看似专业,实际上对于字符串,我们只需要将其第一个成员赋值为'\0'即可,这样可以减少不必要的赋值操作。
void test()
{
char str[32];
str[0] = '\0';
}
对应的,我们对字符串判断是否为空字符串,也只需要判断第一个字符是否为0或者'\0',而不需要使用函数strlen
void test(char str[] /* *pc */)
{
if (0 == str[0])
//或者
if('\0' == str[0])
}
8.进程间或者线程间通信
对于进程之间或者线程之间的通信方式,如果只是通知事件,可以用eventfd,函数原型:
#include <sys/eventfd>
int eventfd(int initval, int falgs);
9. 用好二进制
在乘以2或者2的整数次幂或除以2或者2 的整数次幂的时候尽量用位运算符来代替
乘法和除法运算很占CPU性能。
例如2 * 32
void test()
{
int x = 2;
x *= 32; //(1) = 64
x << 5 // 0000 0010 (2) 左移5位 -> 0100 0000 (64)
}
10.尽量减少使用除法与算
可以适当的将除法运算转换为乘法:
void test()
{
if (a == b/c)
//replace by
if (a * c == b)
}
11.使用+= ,-= ,*= , /= 等复合运算,以加一为例,效率由高到低分别是
i++ > i+= 1 > i = i + 1
12.使用经典小巧的的的库函数
多数情况下效率肯定比我们自己写的高。
例如 :swap, max, min, sort, qsort, atoi ...
13.使用inline ,const 或者&修饰符
注意&不是取地址,而是引用。
inline让函数内联,建议编译器将函数体代码赋值粘贴带函数调用处,在函数体短小,函数调用比较频繁的时候, 能有效避免因为函数调用带来的额内存开销(因为每一次调用函数系统都会生成许多额外的变量)
const 的作用就是限定一个变量不被修改,可以使用const修饰一个常量。
&的作用类似于C语言的指针,这在C++中十分常用,并且比指针高效。
14.多使用迭代器iterator
C++中经常用到容器,而必不可少的就是迭代器,迭代器可以配合for循环使用,要掌握容器,迭代器就是精髓,就如同想要掌握C语言,就必须掌握指针。
15.vector容器 —— 最常用的容器
16.内存拷贝的优化
我们先来看一个结构:
typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
#ifndef __uint32_t_defined
typedef unsigned int uint32_t;
# define __uint32_t_defined
#endif
struct in6_addr
{
union
{
uint8_t __u6_addr8[16]; // 128 bit
#if defined __USE_MISC || defined __USE_GNU
uint16_t __u6_addr16[8]; // 64 bit
uint32_t __u6_addr32[4]; // 32 bit
#endif
} __in6_u;
#define s6_addr __in6_u.__u6_addr8
#if defined __USE_MISC || defined __USE_GNU
# define s6_addr16 __in6_u.__u6_addr16
# define s6_addr32 __in6_u.__u6_addr32
#endif
};
这是C语言中IPV6地址的结构,联合体,占用128位。
我们再看看两种拷贝方式:
void Ipv6Output(in6_addr stAddr, A *pst)
{
pst->stAddr = stAddr;
}
上述例子入参为结构而不是指针,此时这个结构临时存储一份在堆栈,然后从堆栈再拷贝到另一结构,这个过程在汇编指令中有非对齐操作,关于字节对齐,不在这里赘述。
优化:
void FastCopy(const in6_addr *pstSrc, in6_addr *pstDes)
{
const uint64_t *puiSrcAddr = (uint64_t *)pstSrc;
uint64_t *puiDesAddr = (uint64_t *)pstDes;
puiDesAddr[0] = puiSrcAddr[0];
puiDesAddr[1] = puiSrcAddr[1];
}
/* 上述优化是基于64位操作系统的字节对齐,如果是32位,需要用32位指针拷贝4次 */
如代码片段所示,字节对齐在通信中也是效率的一部分。