C语言面向对象风格编程解惑-全局变量性能分析
如果你是CPP老手,但在软件开发过程中要求采用C语言作为主要语言,首先遇到的是各种设计模式不方便应用了,感到非常困扰,然后就是认命之后走向另外一个极端,常常会有过度使用全局变量和goto语句的问题。
CPP既然是C With Class,自然不会排斥面向对象风格编程,大家可以参考
1.《Object-Oriented Programming With ANSI-C》
2. C语言:春节回家过年,我发现只有我没有对象!
本文先尝试分析全局变量的问题。
程序员为什么常常不由自主的使用全局变量呢?
全局变量主要的优点:
- 内存地址固定,方便C语言优化器进行极致优化。
- 全局可见,任何一个函数或线程都可以读写全局变量,貌似节省变量声明的代码。
全局变量主要的缺点:
- 降低代码的可读性,阅读代码是大脑中需要有一个全局词典,并且一旦查字典就相当一次系统IO中断,并且如果有同名变量就是多进程多线程切换。
- 生命周期超长,全局变量存放在静态存储区,系统需要为其分配内存,一直到程序结束。
- 多个有依赖关系的全局变量的初始化和释放次序需要仔细斟酌。
现在进入本文重点:C语言全局变量风格会提升性能吗?
尤其是在一个函数中访问多个全局变量的场景中。
直接上代码:
面向对象(结构)风格
struct Student {
int age;
int level;
char name[64] ;
int sex ;
time_t birthday ;
};
int CheckCpp(struct Student *self,int limitAge,int limitLevel,int limitLen,long int *checkCount) {
int res = 0 ;
if (self->age > limitAge) {
res = 1 ;
} else if (self->level < limitLevel){
res = 2 ;
} else if (strlen(self->name) > limitLen) {
res = 3 ;
} else if (self->sex > 1) {
res = 4 ;
} else if (self->birthday > 1000000) {
res = 5 ;
}
// 增加一个计数器防止优化器超级优化
*checkCount = *checkCount + 1 ;
return res ;
}
全局变量风格
int self_age;
int self_level;
char self_name[64] ;
int self_sex ;
time_t self_birthday ;
int CheckC(int limitAge,int limitLevel,int limitLen,long int *checkCount) {
int res = 0 ;
if (self_age > limitAge) {
res = 1 ;
} else if (self_level < limitLevel){
res = 2 ;
} else if (strlen(self_name) > limitLen) {
res = 3 ;
} else if (self_sex > 1) {
res = 4 ;
} else if (self_birthday > 1000000) {
res = 5 ;
}
// 增加一个计数器防止优化器超级优化
*checkCount = *checkCount + 1 ;
return res ;
}
两种代码分别运行计时
typedef unsigned long long TimestampTz ;
unsigned long long GetCurrentTimestamp(void)
{
unsigned long long result = 0;
struct timeval tp;
gettimeofday(&tp, NULL);
result = (unsigned long long) tp.tv_sec ;
return (result * 1000000) + tp.tv_usec;
}
TimestampTz TestCppCount(struct Student *self,long int count){
TimestampTz startMs ,useMs;
int resSum = 0 ;
startMs = GetCurrentTimestamp();
for(long int z = 0 ; z < count ; z ++) {
for( int i = 0 ; i < 20 ; i ++) {
resSum += CheckCpp(self,60 + i,1,100,&myCheckCpp);
}
}
useMs = GetCurrentTimestamp() - startMs;
if (resSum <= 0)
startMs += resSum ;
return useMs ;
}
TimestampTz TestCount(long int count){
TimestampTz startMs ,useMs;
int resSum = 0 ;
startMs = GetCurrentTimestamp();
for(long int z = 0 ; z < count ; z ++) {
for( int i = 0 ; i < 20 ; i ++) {
resSum += CheckC(60,1,100,&myCheckC);
}
}
useMs = GetCurrentTimestamp() - startMs;
if (resSum <= 0)
startMs += resSum ;
return useMs ;
}
count = 100000000 ,相当于都运行2000,000,000次,使用gcc和g++的O1级别优化器分别编译运行,输出结果如下:
[ansible@pg16 testcpp]$ ./testc
c with class : 8603015 us, times: 2000000000
c : 9735154 us, times: 2000000000
[ansible@pg16 testcpp]$ ./testcpp
c with class: 8437410 us, times:2000000000
c : 9928332 us, times:2000000000
[ansible@pg16 testcpp]$ ./testcpp
c with class : 8492479 us, times:2000000000
c : 8739781 us, times:2000000000
几次的结果有一定的偏差,但是级数是稳定的,每次运行都在4纳秒左右,甚至cpp版本还高一点儿,对比生成的汇编代码
可以看到,struct版本采用rdi,rbx 相对寻址,而全部变量风格,则采用rip相对寻址,而全局变量地址在编译时确定,所以访问多个全局变量的Cache命中率是不稳定的,而结构成员变量的地址一般是连续的,所以其多个成员变量寻址时,Cache命中率是可控的。
总结:
- 全局变量因为寻址问题,并不能一定产生性能优化
- 相对代码可读性带来的困扰,慎用全局变量
- 推荐在C语言中使用面向对象风格编程,类似Linux中fopen,fread系列