在继续处理,data_struct 模块前,我们需要一些工具。做什么用?开发用。客户需要吗?不需要。是什么?是代码。还好没开课,估计这三问三答,人都跑的差不多了。余下的是睡觉,聊天 的。但先等等。为什么说需要一些工具,而且是代码,而且客户不需要的。这是因为我们在linux下开发。我们没有IDE。哈,可能有人现在就开始装 ecplise了。别急,有了这些工具,可以解决很多问题,且不通过传统的debug方案。你用eclipse还是要debug啊。
这里谈两个工具,其一,是计数器,其二是内存检测器。别想的太复杂,其实就是一些C代码。嵌入到你的设计代码中,有利于你后续的开发工作。同时,通过这两个工具的设计介绍,给新手一些开发的思维模式借鉴。
现在,首先,我们解决计数器的问题。使用create_module.sh。这玩意不是来练手脚本设计的,不然我们跑题了。如下
./create_moudule.sh cpu_clk
创建一个模块。确认模块OK,如下方式。
cd cpu_clk
make
bin/test_cpu_clk_main
如果正常,则表示该创建的基本文件都OK了。
现在我们在src/cpu_clk.c里增加如下内容,仅跟着
1 |
static int cpu_clk_flag =0;//添加如下内容 |
2 |
#define GET_CPU_CLK(re) do { \ |
3 |
unsigned int __a,__d; \ |
4 |
__asm__ __volatile__( "rdtsc" : "=a" (__a), "=d" (__d)); \ |
5 |
(re) = ((unsigned long)__a) | (((unsigned long)__d)<<32); \ |
在src/cpu_clk.c里增加如下内容。
1 |
unsigned long long cpu_clk_get( void ){ |
在inc/cpu_clk.h的 ////ins_func_declare下面增加如下内容。
1 |
unsigned long long cpu_clk_get( void ); |
没错,这只是C文件里的函数申明。给与C编译器检测函数调用入口参数规范用的。你不写,也一样通过。但有时会有麻烦。
在src/test_cpu_clk_main.c里,增加如下函数,在 cpu_clk_init();后面。
1 |
printf ( "the cpu clk is %lld\n" ,cpu_clk_get()); |
保存所有文件。在cpu_clk/目录下,运行
看看是否有什么结果出来。
你可以再执行一次。此时数值不一样。OK现在来解释一下上面的操作。
#define GET_CPU_CLK(re) 这是个宏,意思是将出现GET_CPU_CLK(x)的地方,进行替换。替换内容就是这个宏后面东西。由此上面的函数替换成如下。
1 |
unsigned long long cpu_clk_get( void ){ |
3 |
do { unsigned int __a,__d; __asm__ __volatile__( "rdtsc" : "=a" (__a), "=d" (__d)); (re) = ((unsigned long )__a) | (((unsigned long )__d)<<32); } while (0); |
这可不是我解释的,你可以在当前目录下执行如下操作。
gcc -Iinc src/cpu_clk.c -E -o tt.c
此时,当前目录下会有个tt.c的文件。 -E的意思是做预处理,对类似#ifdef ,#define 进行对应展开。当然预处理会去除掉不必要的内容。例如这段内容,在 tt.c里的,
3 |
unsigned long long cpu_clk_get( void ); |
这里的意思是,将cpu_clk.h的内容COPY过来,当然空行和注释都取出了。所以新人别在问我#include "X"是做什么的了?就是把X给插到这。
现在说下这个#define里的 do{}while(0)有什么意义,注意,实际C代码里的while(0)后面没有;好。
do {...} while(0) 实际上是执行到while (0)后,不再循环。因此整个功能等同于如下
{
...
}
这个够爽吧,处理没有接口参数外,整一个内联小函数啊。而为什么我们不直接定义为
#define A(b) { ... }
这是因为,这样处理,会产生代码奇异,不是歧异。例如
#define my_swap(a,b) {int t = a; a = b; b = a; }
如果你想这样,
if (c > 0)
my_swap(a,b);
else
my_swap(a,b);
编译器可不鸟你了说你else有问题。而如果加上了do {} while(0) 此时 my_swap(a,b);则为do {}while(0); 此时会当作一条语句,符合if 的要求。当然这样写还有其他好处,不过这个好处是最大的。
__asm__ __volatile__是什么?给谁看的。是GCC特有的东西,给编译器看的。__asm__表示下面是段 内嵌汇编,而且是AT&T风格,这里不多说AT&T风格和intel风格的区别。不过建议使用前者。因为这个风格在其他嵌入式场合下,大 差不差。__volatile__ 和C的volatile一样,是用于告诉编译器,这个是易变的东东,所以你别优化,你也别挪位置。
这里引申说一下,编译器自动优化,有时会把后面的代码放到前面操作,根本原因是每个逻辑代码片的执行,需要数据准备,数据计算,数据写出等动作。但CPU 往往在计算时,也可以传递数据,这个计算结果没出来时,也可以做别的计算,所以优化时,会改变你的代码的执行顺序,如果不想让他被改变,则可以使用 __asm__ __volatile__ 的汇编,加上个 NOP,就是什么都不做的空指令。编译器看到这个,就大头了,哦这个不能动,因为他随时需要处理,我不能打乱逻辑顺序。上述这个加NOP空指令的做法,绝 大多数情况下,不会被使用,只有在写driver或者CPU跑的太快的时候,会使用到。我确实碰到过CPU因为工艺问题,实际跑的和说明不一直的情况,就 是说,数据没被稳定的传递到指定寄存器时,就已经被使用了,由此导致数据出错,我的口头禅就是”优化过了“。说这么极端的例子是想让新手知道 volatile是有什么用。同时,该用时别忘了,不需要用时,别乱加。
关于汇编指令,我就不多说了。是调用了INTEL的特殊指令 rdtsc。这个指令用于从一个特殊寄存器里读一个64位的数据,上面这么猥琐的写法是目前我没找到在GCC下,实现64位的汇编内联方式。也即下面的方式无法读出64位,只能读出低32位。
1 |
#define GET_CPU_CLK(re) __asm__ __volatile__("rdtsc" : "=A" (re)) |
那么这个寄存器是做什么用的。就是你的INTEL的CPU在上电后,每个时钟频率它累加一次。没错。我的笔记本电脑测试,睡一秒,他的差值约在 2.39*10^9,也就是近2.4G。这个东西有什么用?太有用了。你想啊,你连续两次读取这个值,再乘以1000除以一秒钟的差值,就是两次之间的毫 秒数(基于CPU不睿频),如果乘以1000000,则是微秒数。夸张点说。如果你的电脑是3GHz,则理论上你可以测量的精度到 1/3纳秒。比系统提供的时间测试函数毫秒级的管用多了。
而printf中间 %lld是用于打印64位整型的。当然你也可以 %llx,这个是按照16进制输出。通常我喜欢16进制,但考虑新人,我就不折磨你们了。
那么为什么我就只写了一个函数,并且立刻打印?这里就要教育新人了,老手之所以不出错,就是因为人老了,需要拐杖,走半步,扶一下。你年轻,一个跟头就 10万8千里,远近不谈,你跳来跳去的,和目标毛关系没有啊,最终发现自己都不知道在哪了。因此,对于C代码设计,特别是不进行debug模式的正向设 计,一定要注意一个设计思想,每个目标能分解的,一定要分解。任何不可分解的目标,一定要有反馈测试。如果我们连续两次执行程序,得出的值是递减的,怎么 办?假设你不知道INTEL的这个CPU是递增还是递减,难道这个情况不摸一下,你就敢写 clk计数差的公式了吗?
好。现在我们要写计数差了。先计算1秒需要多少个clk。
在src/cpu_clk.c的 cpu_clk_init函数内,在 //todo ...后写如下内容。
2 |
unsigned long long t1,t2; |
6 |
second_count = t2 - t1; |
7 |
printf ( "one second need %lld clock\n" ,second_count); |
sleep函数是系统函数,单位是秒,需要#include 头文件<unistd.h> 外加存在printf,需要头文件 <stdio.h>所以在#include "cpu_clk.h" 下增加
这里有个小习惯,库的头文件放在你的头文件下面。没什么为什么。等你发现有的时候这样写有价值就知道了。我要举例估计都是你一年内遇不到的情况。
由于我们希望有个每秒的基准CLK数据,同时不希望外部C文件的函数看到,如同类里的私有成员变量一样,则我们在 cpu_clk_flag下面定义
static unsigned long long second_count = 0;
需要明确,static 对于变量定义时,他的含义叫做局部。而不是静态。能访问到该变量的,只有当前C文件在申明该函数之后的所有位置。之前的也访问不了它。
如果有人说这个含义是静态变量,那是指在一个函数内声明一个变量,前面加上了static,此时这个变量可以被访问到的,仍然是该函数内的代码,毕竟有个 {}嘛,但是由于这样申明的变量和函数外申明的变量有个共同的特点,就是编译器给他确定了一个具体的存储空间(具体位置是连接器分配的,至少有个号了,这 年头买车牌不是也要摇号嘛),由于存在具体空间,所以每次调用函数时,函数内的这个变量的值都会是曾经修改过的值,相对函数其他的变量(会自动在堆栈或寄 存器里临时作为存储空间,如同你租房,产权不是你的,所以每次新搬进去,不是都留有上户人家的痕迹吗?),状态不变,所以此时解释为静态变量。想不绕,很 简单,无论哪声明,这个变量都有号了,当然函数外无论怎么声明,也都有号,除非强制声明全局寄存器变量,另谈。而函数外面的变量默认是全局变量,即便其他 C文件没有看到诸如extern int a;只要他知道有a这个变量,编译器一样可以通过,最多来个warning。由此导致static对于变量声明而言,在函数内外有不同的表现,其实是看和 谁在做对比而已。
OK。走半步,扶一下。保存,make ,bin/test_cpu_clk_main.c
我的桌面电脑显示
one second need 3311011729 clock。
第二次运行结果为
one second need 3311031083 clock。
哈,至少和我的CPU,i5-2500 正常为3.3GHz没有差异。不过这里要注意了。你们会发现低位有差异。显然,linux是多任务操作系统,但不是实时操作系统。上述时间差异,是由于操作系统每次sleep之间的时间并不能严格保证。
不过希望大家注意一点,这并不影响实际计时。因为如果一个小函数,只是在微妙级的,比如只用了5us。如果是我的CPU,相当于已经运行了16500个 cycle。对于小函数已经很多了。那么5us是个什么概念,一般进程之间切换的时间也在微秒级,假设就是2us吧,那么如果进程每个2us就要切换,那 意味着系统有一半的时间用来进进出出。这系统利用率也太低了。所以通常一个进程被分配的时间是毫秒级的。由此,如果你的微秒级的函数想被别的进程打断,概 率在1/1000。因此,这样的异常不算什么。多测几次,去掉比较大的值就OK了,而且如果你的函数跑的内容比较少,假设50us就跑完,大约16万个 cycle,想时间突然变大,也是很难的。
这里要引申说一句。如果在诸如linux这种非实时操作系统下,强迫程序每次运行时间相等这事没意义,有意义的是诸如DSP或者实时要求强的场合。那你纠 结汇编代码,从C的优化编译300个cycle,到你手工50个cycle,是有意义的。而且可以保证每次均是50个cycle,如果不是,那这CPU也 太不稳定了,这几乎是绝不可能的事情。说这些废话,是想强调,1秒测不准没关系。因为他是用于简单换算计算时间的。而同时,测不准不是机器出毛病了。而是 非实时操作系统自身的问题。
好我们下面继续改代码。在定义second_count=0后增加 last_count= 0;这个是为了做计数。我们修改cpu_clk_get如下
1 |
unsigned long long cpu_clk_get( int mode){ |
2 |
unsigned long long now_count,re; |
3 |
const long MODE_CLK[2] = {1000,1000000}; |
4 |
GET_CPU_CLK(now_count); |
5 |
re = now_count - last_count; //((now_count - last_count) * MODE_CLK[(mode != 0)]) / second_count; |
6 |
last_count = now_count; |
在inc/cpu_clk.h的//ins_def下增加如下代码。
在src/test_cpu_clk_main.c内的main 函数上面增加如下代码
4 |
for (i = 0 ;i < 1000 ; i++ ){ |
在main 函数中修改如下代码
01 |
int main( int argc, char *argv[]){ |
02 |
unsigned long long mill,micro; |
04 |
printf ( "hello test_cpu_clk_main now run...\n" ); |
06 |
cpu_clk_get(MILL_SEC); |
07 |
for (i = 0 ; i < 10000 ; i++){ |
10 |
mill = cpu_clk_get(MILL_SEC); |
11 |
printf ( "the time is %lld (ms)" ,mill); |
12 |
cpu_clk_get(MILL_SEC); |
13 |
for (i = 0 ; i < 10000 ; i++){ |
16 |
micro = cpu_clk_get(MICRO_SEC); |
17 |
printf ( "the time is %lld (us)" ,micro); |
19 |
printf ( "hello test_cpu_clk_main now exit...\n" ); |
调整 inc/cpu_clk.h里的cpu_clk_get的声明。
保存,make ,bin/test_cpu_clk_main
呵呵。很小很小的值。为什么呢?
我们打开一下 Makefile,看一下
$(MODULE_TB):$(MODULE_TS) $(MODULE_BIN) 下面的一行,此处使用了-O2优化。由于这个test函数确实没做什么。所以被优化掉了。我们将此处-O2去掉。
由于模块还没有稳定,所以我们暂时对模块的编译,也就是clk_cpu.c的编译仍然不用优化,使用普通方式。(这也是为什么模版自动生成是,这里没有放 O2的原因),此时记得要make clean.因为Makefile本身的更新并不没有依赖对应。所以make会认为没有什么可以做的。此时再make , bin/test_cpu_clk_main
怎么样,看到数据没有?好大,哈。不过这是clock变化的数量。至少目前测试,我们发现确实有时钟变化差异。切记切记,每一步都测试,别怕别人笑话,每步测试至少不用debug也知道问题出哪了。比如我们发现O2优化的太猛了。
现在修正src/cpu_clk.c的cpu_clk_get函数如下
1 |
unsigned long long cpu_clk_get( int mode){ |
2 |
unsigned long long now_count,re; |
3 |
const long MODE_CLK[2] = {1000,1000000}; |
4 |
GET_CPU_CLK(now_count); |
5 |
re = ((now_count - last_count) * MODE_CLK[(mode != 0)]) / second_count; |
6 |
last_count = now_count; |
进行测试,以后我就不废话了,测试就是make ,和执行。此时我的电脑显示, 29ms,和20901us。这非常正常。因为第一次运行test函数,这个代码片并没有被加载到CACHE中,而后面再次调用则直接从cache里调 用。如果一个小函数,每次测试,都是第二次比第一次时间要长,那一定出鬼了,如果你了解计算CACHE的价值后。
前面说到O2态猛了,猛在什么地方呢?猜都能猜的出来,首先test函数它不敢省略掉。但for循环却可以改成 a+= 1000;。那么我们在O2是,能否不让这个 for 循环被优化掉呢?哈,前面不是刚说过volatile吗?你在 int a;前面加上volatile关键词,告诉编译器,这个a你可别乱动,指不定别的线程会随时调用读取或修改,所以你老实的给我for循环。大家可以试一 下。有人会说,野鬼你够变态,不求最快,但求最慢。不过我希望新手们知道,你能让代码慢下来,也就有机会让代码快上去,所以不要怕。原理清楚,你就不在是 工具的奴隶。什么是工具,C语言,C的编译器就是工具。
这里说下
1 |
re = ((now_count - last_count) * MODE_CLK[(mode != 0)]) / second_count; |
我们已知 X,和Y,Y表示一秒多少clk,X是当前clk。那么我们要知道X对应多少毫秒,自然是 X/ Y *1000ms/s。但是毕竟是整数啊,所以乘法放前,除法放后。但这种做法,你需要知道位宽是否溢出。你可以算一下,如果你的CPU是 4.5GHz,64位可以描述多长时间,由此知道我们很难溢出。
MODE_CLK是个数组,为什么放在函数里,原因如下,一个基础原因,一个目的原因。
基础原因是,它够小,否则免谈下面的事情。
目标原因,我希望代码够快,由于是函数内的变量,因此会和函数同时读上来,只要函数不被别的代码冲洗掉(从CACHE内),它就不会。但这也要看CPU的架构,编译器也不是老大,CPU的架构才是最终的决策者,否则还是在堆栈里或常量表里。
为什么是MODE_CLK[(mode != 0)],这是因为就两个选项,为了防止外面的人传个mode = 3,此时会数组访问出界,取了个莫名其妙的值。而此时mode只有0,或1.
因此需要新手注意两点。
1、任何判断,无论什么情况,记得加(),我就是记忆力不好,所以C语言的优先级我一直搞不清楚,无论是计算还是逻辑处理,但是()总没错吧。
2、如果是确定的模式数组的下标,通常模式的数组数量尽可能取2的幂次方。例如你有3种选择,用4,9种选择用16.多出来点空间没问题。你可以让多出来的空间存放最大或最小值,这有什么优势呢?如下。
假设,你是9个类型,分别有9个值,1,2,。。。9,则你的数组如下声明
int mode_array[16]= {1,2,3,4,5,6,7,8,9,9,9,9,9,9,9};
OK。对于外部传入的mode,则你可以简单如下修正到安全范围,
mode &= 0xf;
那么假设对mode_array的取值就可以写成
mode_array[(mode & 0xf)]
这样做的好处是,没有了
if ((mode < 0) || (mode > 15))
这种双判断。保证了mode在有效空间内。当然数据未必有效。不过这种错误只会让数据出错,不会让数据崩溃。因为 mode_array[10]= mode_array[8],所以,只会数据计算不正确而已。
但新手们要注意, mode < 0 ,mode > 15并不耽误时间。这个只是个CPU的计算而已。是if耽误时间。跳转会冲掉指令流水线的。因此简单的判断诸如
re = (re > 0) ? re: 0-re;会令编译器很舒服的选择带条件符号的指令,就是如果判断符号不成立,该指令就不执行,准确说是该指令执行的结果不放入目的寄存器(目标变量,这总能理解把,就是等号左边,我要口吐白沫了。。。。。)
不过话又回来,上面这代码写的比较矫情,因为存在个除法,除法动辄10个cycle以上。10个cycle啊,有些不复杂的DSP(计算能力不强大的)2 个cycle内,平均可以做到128位的数据传入传出,同时4个byte移位完成,4个byte * byte的乘法,外加2个指针计算,再加8个byte的加法,还不谈流水线。你说一个除法占10个以上cycle过分不过分。
但是保持良好的C代码书写习惯很重要,尽可能的让代码写的简单明了,编译器虽然不是人,但也是人写的,你简单了,他就快捷了。
上面的代码好不好?不好。至少有个问题。就那我的CPU ,CORE2 I5-2500,INTEL的资料明确的说明,可以睿频到3.7GHz。也就是频率是可以变的。那么这样基础的1秒的clk就不准确了。因此我们通常是根 据clk来计算可能更方便。而且希望求出某个函数,在整个计算中的总耗时,每次都换算到时间上,有除法,会导致精度不高。因此我们还需要另一个函数,真正 的cpu_clk_get,现在的只是cpu_time_by_clk_get而已。同时还存在个问题。总是cpu_clk_get,万一代码里,不对 称,那算出来的不就是错的吗?为了防止这个人为失误,我们还是需要修改一下现在的代码。
目标如下:
1、存在 get_clk_start ,get_clk_end两个操作。
2、存在一个转换时间的操作。
3、get_clk_start, get_clk_end操作频繁,转换时间函数操作次数很少。
由此我们如下设计,将src/cpu_clk.c的代码,修改如下
01 |
void cpu_clk_init( void ){ |
03 |
//log("module inited..",X); |
10 |
void cpu_clk_start( void ){ |
11 |
GET_CPU_CLK(last_count); |
13 |
unsigned long long cpu_clk_end( void ){ |
14 |
unsigned long long re; |
16 |
return re - last_count; |
18 |
unsigned long long cpu_clk_2_time(unsigned long long clk, int mode){ |
19 |
unsigned long long now_count,re; |
20 |
const long MODE_CLK[2] = {1000,1000000}; |
22 |
unsigned long long t1,t2; |
26 |
second_count = t2 - t1; |
27 |
//printf("one second need %lld clock\n",second_count); |
29 |
re = (clk * MODE_CLK[(mode != 0)]) / second_count; |
30 |
last_count = now_count; |
对应调整好inc/cpu_clk.h,如果不调整,这里有64位的整型,当test_cpu_clk_main C文件发现找不到函数声明时,通常会默认使用INT型替换。那就麻烦了。
修改src/test_cpu_clk_main.c函数如下
01 |
int main( int argc, char *argv[]){ |
02 |
unsigned long long mill,micro; |
04 |
printf ( "hello test_cpu_clk_main now run...\n" ); |
08 |
for (i = 0 ; i < 10000 ; i++){ |
16 |
for (i = 0 ; i < 10000 ; i++){ |
19 |
micro = cpu_clk_end(); |
21 |
mill = cpu_clk_2_time(mill,MILL_SEC); |
22 |
micro = cpu_clk_2_time(micro,MICRO_SEC); |
23 |
printf ( "the time is %lld (ms)\n" ,mill); |
24 |
printf ( "the time is %lld (us)\n" ,micro); |
26 |
printf ( "hello test_cpu_clk_main now exit...\n" ); |
此时,必须有start和end两个函数。同时,最后计算时间,而且,会sleep两次。其实这是没有关系的。你也可以把sleep(1);独立出一个函 数。但不要放到init里了。为什么呢?因为能拖后计算的,就拖后计算,减少代码计算过程中的变量关联度,是一个简化代码的好方法。什么意思呢?举例吧, 两个和尚有水吃,有两种方案,一则,轮着,今天你明天我。二则,一起扛着。通常前者比后者要快,因为自己扛水,就一个人,庙里的老和尚说了,山下的女人是 老虎,那还不一个劲的跑回庙里,但两个和尚扛,一会你累了,一会他要去便便了,反正只要一个人有事情,这个事情就得耽误。程序计算也是,如果数据干净整 洁,至少在一个逻辑代码片里,少掉的数据,可以少掉很多数据和数据的关联成本。如果把sleep(1)放到init里,会让后续使用者无限遐想,动不动就 尝试算把时间。还不如明确告诉它,最后再说。而且你要算一次我就sleep(1),就是让你少给我处理clk到time的转换时间(我野鬼看着除法就不 爽!)。这里对sleep(1)每次计算都折腾一次,确实是我抬杠了,但是减少计算流程中,数据的关联,是优化算法的一个基本准则,这个是要记住的。
这个时候做的好不好了?还是不好。为什么呢?我反复说过cache,在我眼里,cache就像自己家,而内存充其量是小区。甚至只能算个城市,那有人说了,硬盘算什么,我琢磨了一下,觉得至少算火星吧。
由于我们绝大都说测试时间是发生在对某个函数的时间测试上,因此,通常cpu_clk_start ,cpu_clk_end是成对在一个函数里的。如果在函数的入口和出口,表示对这个函数的测试,如果是调用某个函数的前后,则表示对这个子函数的测试。 因此我们为什么不能把这个写到测试代码里呢?非要调用cpu_clk的模块,万一这个模块的代码和当前代码在cache里有冲突呢?目前cache基本采 用一组多line的方式。究竟是什么,不懂的你就不需要知道,只要知道,低位地址相同的代码,会在CACHE里打架,怎么个打架法,就是轮到谁了,就把别 人踢出去。也就是说存在,一个父函数调用子函数时,子函数会把父函数的代码踢出CACHE,等返回时,父函数又踢子函数,来回踢,如果父函数是在个for 循环里调用子函数的话。基于这种考虑,我们现在需要define了。
define 啥,就是把上面的cpu_clk_start cpu_clk_end用宏的方式,嵌入到实际调用的代码里。由此,现在得在inc/cpu_clk.h做文章。OK。修正inc/cpu_clk.h如下代码。
02 |
#define GET_CPU_CLK(re) do { \ |
03 |
unsigned int __a,__d; \ |
04 |
__asm__ __volatile__( "rdtsc" : "=a" (__a), "=d" (__d)); \ |
05 |
(re) = ((unsigned long )__a) | (((unsigned long )__d)<<32); \ |
08 |
#define CPU_CLK_START() GET_CPU_CLK(s_last_count) |
09 |
#define CPU_CLK_END(re) do{GET_CPU_CLK(re); re -= s_last_count; }while(0) |
10 |
static unsigned long long s_last_count = 0; |
同时将src/cpu_clk.c的#define GET_CPU_CLK给删除。
修改src/test_cpu_clk_main.c里的cpu_clk_start成大写,X = cpu_clk_end();为 CPU_CLK_END(X);如下
02 |
for (i = 0 ; i < 10000 ; i++){ |
10 |
for (i = 0 ; i < 10000 ; i++){ |
15 |
mill = cpu_clk_2_time(mill,MILL_SEC); |
16 |
micro = cpu_clk_2_time(micro,MICRO_SEC); |
然后测试。嘿嘿,怎么样?如果还不放心,或者疑问inc/cpu_clk.h里面s_last_count为什么也是static,你可以使用 gcc -E 对test_cpu_clk_main.c进行观测。看看都怎么会事。只要记住,C语言,是对独立的C文件进行编译的,同时,函数外变量不加static 则默认extern,其他C文件的函数是可以使用访问的,你就一切明了怎么总说重定义错误。
但这里又有问题了。啥问题?如果我不想要这写测试代码怎么办?慢慢注释?
别忘了,C语言的#define是个强大的文本替换工具。现在我们对inc/cpu_clk.h进行扩充如下,并且修正cpu_clk_2_time为一个宏。
03 |
#define CPU_CLK_START() GET_CPU_CLK(s_last_count) |
04 |
#define CPU_CLK_END(re) do{GET_CPU_CLK(re); (re) =(re) - s_last_count; }while(0) |
05 |
#define CPU_CLK_2_TIME(re,mode) do{\ |
06 |
unsigned long long now_count,second_count;\ |
07 |
const long MODE_CLK[2] = {1000,1000000};\ |
09 |
unsigned long long t1,t2;\ |
13 |
second_count = t2 - t1;\ |
15 |
re = (re * MODE_CLK[((mode) != 0)]) / second_count;\ |
17 |
static unsigned long long s_last_count = 0; |
19 |
#define CPU_CLK_START() do{}while(0) |
20 |
#define CPU_CLK_END(re) do{}while(0) |
21 |
#define CPU_CLK_2_TIME(re,mode) do{}while(0) |
如果我们注视掉USED_CPU_CLK,则所有涉及CPU_CLK的地方,都被定义为一个没有意义的do{}while(0),为什么这样做,我前面已经说过了。
而且你会发现CPU_CLK_2_TIME就是cpu_clk2_time的照抄。不过要注意,此处#define 存在一个参数叫做re因此,需要将原先函数中局部变量的声明删除掉。
现在src/test_cpu_clk_main.c也调整如下
01 |
int main( int argc, char *argv[]){ |
02 |
unsigned long long mill,micro; |
04 |
printf ( "hello test_cpu_clk_main now run...\n" ); |
08 |
for (i = 0 ; i < 10000 ; i++){ |
16 |
for (i = 0 ; i < 10000 ; i++){ |
20 |
CPU_CLK_2_TIME(mill,MILL_SEC); |
21 |
CPU_CLK_2_TIME(micro,MICRO_SEC); |
22 |
printf ( "the time is %lld (ms)\n" ,mill); |
23 |
printf ( "the time is %lld (us)\n" ,micro); |
25 |
printf ( "hello test_cpu_clk_main now exit...\n" ); |
测试,应该一切正常了。
现在好不好?还不好。为什么。如果我希望一个C文件里有测试,另一个C文件的测试关闭怎么办呢?恩。确实如此,所以#define USED_CPU_CLK通常不是在.h里定义的。你将inc/cpu_clk.h里的这个定义注释掉。随后在src /test_cpu_clk_main.c里修改成如下的内容。
然后测试,测试后,再把src/test_cpu_clk_main.c这个C文件里的 #define USED_CPU_CLK再注释掉,再测试。
如果你现在还搞不懂#include是怎么运作的,我就只能说“我真的败了,你去看“巧虎”吧。”
OK。到此,可以把inc/cpu_clk.h直接作为一个测试模块使用了。对,没搞错,一个头文件,如果被#include也可以作为一个测试模块使 用。只要它足够短小。同时希望新手注意,改优化的地方优化,不需要优化的地方,字多不坏事,如CPU_CLK_2_TIME这个宏。我可没时间折腾这个。
简单总结一下,其实利用CPU的CLOCK CYCLE计数寄存器做高精度记时器,是个及其简单的事情,大家也看到了,最后的头文件里面没有什么内容。但是即便如此,新手切记,
能分解的任务,分解处理,步步测试。每一步都是针对前一步的正确状态进行迁移,修改,增添。
同时,#define ,#include后面除了特殊情况,我就多说了。但你要记得do{}while(0)哦。至少这么写,找工作时,别人会误意味你水平还不错。
记得volatile什么意思。
记得static什么意思。
记得gcc -E的用途。