C语言面向对象风格编程解惑-全局变量性能分析

C语言面向对象风格编程解惑-全局变量性能分析

如果你是CPP老手,但在软件开发过程中要求采用C语言作为主要语言,首先遇到的是各种设计模式不方便应用了,感到非常困扰,然后就是认命之后走向另外一个极端,常常会有过度使用全局变量和goto语句的问题。
CPP既然是C With Class,自然不会排斥面向对象风格编程,大家可以参考
1.《Object-Oriented Programming With ANSI-C》
2. C语言:春节回家过年,我发现只有我没有对象!

本文先尝试分析全局变量的问题。
程序员为什么常常不由自主的使用全局变量呢?
全局变量主要的优点:

  1. 内存地址固定,方便C语言优化器进行极致优化。
  2. 全局可见,任何一个函数或线程都可以读写全局变量,貌似节省变量声明的代码。

全局变量主要的缺点:

  1. 降低代码的可读性,阅读代码是大脑中需要有一个全局词典,并且一旦查字典就相当一次系统IO中断,并且如果有同名变量就是多进程多线程切换。
  2. 生命周期超长,全局变量存放在静态存储区,系统需要为其分配内存,一直到程序结束。
  3. 多个有依赖关系的全局变量的初始化和释放次序需要仔细斟酌。

现在进入本文重点: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命中率是可控的。

总结:

  1. 全局变量因为寻址问题,并不能一定产生性能优化
  2. 相对代码可读性带来的困扰,慎用全局变量
  3. 推荐在C语言中使用面向对象风格编程,类似Linux中fopen,fread系列
posted @ 2024-07-25 11:01  范振勇  阅读(8)  评论(1编辑  收藏  举报