感觉最近的更新频率略高啊~哈哈~

这次的带来的是一个十分简单便利的C++计时库。

项目地址:https://github.com/miaoerduo/tictoc 欢迎Start和提MR

项目中有详细的说明和Demo,可以很直观的体验到这个库的易用性。

先看一下效果,如果我们正确使用的话,大致会出现类似下面的信息:

demo.cpp @ main [    8,   13]   elapsed:      0.025 s      24.786 ms       24786 us
demo.cpp @ main [ 8, 18] elapsed: 0.049 s 48.709 ms 48709 us
demo.cpp @ main [ 8, 23] elapsed: 0.072 s 72.211 ms 72211 us
demo.cpp @ main [ 8, 24] elapsed: 0.072 s 72.225 ms 72225 us
demo.cpp @ main [ 30, 36] elapsed: 0.022 s 21.747 ms 21747 us
demo.cpp @ main [ 36, 41] elapsed: 0.021 s 21.463 ms 21463 us

可以显示,我们的每个区域的代码(包括行号)的消耗时间。精确到微秒。

起因是这样的,之前有很长时间的工作内容是优化一些特定的函数,保证新旧的SDK的速度的对齐。然后C++虽然有一些工具可以分析运行状态,但通常还是简单的打印时间来的方便 /* Print大法好 */ 。之后,和工程的小伙伴一起Debug的时候,就发现他写了一个头文件,然后用绝对路径的方式去include,而头文件里面就是各种常用的小工具,而最常用到的就是时间的打印。

之后,我专门要到了他的百宝箱,仔细分析了一下,发现计时器模块仍然存在一些问题:

  1. 在Debug的时候,如果加上工具代码,在Release的时候,还得一点点删掉,很麻烦。
  2. 修改时间精度的话,需要修改源码,略麻烦。
  3. 打印的时间戳的信息不完整,看不出来该段时间具体的代码的范围。
  4. 计时器如果在多个文件中都用到,会有各种奇怪的错误,重复定义变量啊,或者找不到变量啥的。
  5. 对更复杂的程序,比如各种库的编译,多个库的链接调用不支持。

上面说的问题,说大不大,说小不小。如果能有个工具能解决上面5个问题,那也是一件十分惬意的事情。所以,也就有了本文和TICTOC这个库。接下来,我们会从上面的5个问题开始,一点一点介绍C++的小技巧。

〇、设计思路

其实计时器的思路很简单,就是定义两个宏TIC和TOC,如果插入TIC,则记录为起始时间,当插入TOC的时候,则计算与上一次TIC之间的时间,并打印出来。

比较麻烦的是,如果我在使用TIC的时候,生成一个变量,那连续使用两次TIC的话,就会出现变量的重复定义。另一个方案就是在全局定义一个时间的变量,但这样会带来另一个问题,就是所有函数都共享这个变量,如果函数内部再运行一次TIC,会覆盖掉这个时间戳,但是其他的TOC的结果不直观。

所以,这里就使用了一个字典,来存放TIC的时间戳。这个字典本身是使用单例模式去生成和维护的。每次TIC的时候都会初始化一次它,但是由于是单例,所以只有第一次会耗时。而字典的键是个字符串,由文件名+函数名联合构成。这样针对每个函数,都会有自己的一个计时器,就不用担心冲突了。之后运行TOC的时候,也会检查当前的文件名和函数名,从而与对应的TIC时间戳相减。是不是听起来很简单!

当然还会碰到很多奇怪的问题,其中最无语的是,当动态库使用这个库,而主程序也使用这个库的时候,所谓的单例模式就失效了,两段程序里面都会有这个字典,然后就冲突了,出现double free的情况。查了半天,才发现是动态库只在静态表导出这个单例,动态连接器默认查询动态表,没找到,从而主程序自己又重复构建了这个实例,导致了存在两个实例。最终用-rdynamic的方式编译就可以解决。但是用这种方式的话,又会显得很麻烦。我采用的解决方法是匿名命名空间,在每个文件中生成自己的单例。细节我们在后面会谈到。

一、Debug or Release?

因为我们不希望在Deliver的时候,再修改代码,所以有没有办法,使用不同的宏来控制我们的程序呢?当然是可以的。C/C++最常用到的预处理语句:#define, #ifdef, #ifndef,#else, #endif。采用下面的方式来进行就可以。

#ifndef TICTOC_HPP
#define TICTOC_HPP #ifdef WITH_TICTOC
// 一些计时器的逻辑单元
// 函数啥的
#else
// 一些假的信息
// 比如宏函数,内容空的,免得编译不过
#endif #endif

首先,这个TICTOC_HPP的宏定义,是为了防止头文件的多次包含。不然在多处include这个头文件的时候,会出现函数重复定义的问题。是一个良好的编程习惯。

WITH_TICTOC这个宏才是用来控制我们的Debug/Release的关键。在Debug的时候,编译加入一个宏定义,用g++直接编译的话,就是编译的时候加上-DWITH_TICTOC。用CMakeList的话,就是另一套了,自己查一下吧。在Release的时候,去掉这个宏定义就行,这样编译走的就是#else的分之,里面可以不写代码(我这里还是写了几行,定义了一些宏,但是宏的操作是空的)。

总之,灵活的使用宏定义,就可以让我们的编译器按照我们的想法去工作!

二、多种精度

问题二就比较简单了,既然每设置一种精度,都要修改一下代码,不如一次性的将所有的精度都打印出来了!这部分似乎没有什么好说的,就简单的说一下,我这里用到的计时的函数吧。

#include<sys/time.h>
/*
struct timeval {
time_t tv_sec; // seconds
suseconds_t tv_usec; // microseconds
};
*/
struct timeval get_tick() {
struct timeval time;
gettimeofday(&time, NULL);
return time;
}

timeval是一个表示时间的结构体,可以精确到微秒级别,完全够我们使用了。

三、打印完整的信息

首先,对于一个计时器,为了方便调试,我们希望知道什么信息呢?这里列出来我比较关心的:

  1. 这个时间戳所在的位置,包括:文件名,函数名
  2. 时间戳是哪一段代码产生的,即:起始和结束的代码行号
  3. 具体的时间(按不同精度显示)

对于3,上文已经介绍了。那么如何获取文件名、函数名以及行号呢?

其实C++中(C语言中也有的)早就给我们定义好了一些宏。这里就简单的列一下常用的几个,大家感兴趣也可以自己去查询:

  1. __FILE__ : 宏所在的文件名
  2. __FUNCTION__ : 宏所在的函数名
  3. __LINE__ : 当前行号
  4. __DATE__, __TIME__ : 最后一次编译的时间
  5. __TIMESTAMP__ : 文件最后的修改时间

所以,我们这里主要用到三个:__FILE__, __FUNCTION__, __LINE__ 。

四、Working Everywhere

上面的问题4和5,放在一起介绍。

针对问题4,是我们在多个文件同时使用了计时器,如果通过全局变量的方式去存储时间戳,那么每个文件都会有自己的时间戳,从而导致冲突(当然,把时间戳改成static的可能可以解决)。而且,同一个文件中,如果出现函数调用,也有修改这个全局的时间戳,导致打印时间很不友好。

这里使用字典来存放时间戳,给每个文件都创建自己的时间戳,从而解决了这个问题。在〇章中,也有介绍。

那么问题5就很复杂了,多个动态库同时使用时,会崩溃。首先,为了让字典在程序中,只存在一份,我这里使用了单例模式。如果把所有的文件都编译在一起,是完全OK的。问题就出在,如果动态库使用了这个工具,而主程序也使用该工具,且又链接了动态库,那么程序中就会出现多个字典,在程序退出析构的时候,就会出现多次free的情况(很奇怪吧,明明是两个实例,居然两次析构函数都调用同一个实例)。之前也说了,用-rdynamic的方式编译会很麻烦,而且我们不可能给整个大项目的每个部分都加这个编译选项吧。我们的工具库要足够的独立!

按照之前的分析,我们其实只需要给每个函数都分配自己的一个键就可以了,其实完全没必要只有一个Global的字典,只需要给每个文件都生成自己的字典不就OK了吗。但是,怎么去实现呢?

常见的方法有两个:

  1. static 变量,static 关键字有一个功能,是保证这个变量只在该文件中使用。不会导出。
  2. 匿名命名空间,也叫匿名名字空间,这里采用的就是这个方案。

namespace {
void print() {
std::cout << "hello world" << std::endl;
}
}

上面就是最简单的匿名命名空间,如果我们在代码中这么定义,其等价于:

namespace thisisaspecificnamespace {
void print() {
std::cout << "hello world" << std::endl;
}
} using namespace thisisaspecificnamespace;

里面的这个大长串是啥意思?

其实thisisaspecificnamespace这个名字是我瞎写的,对于编译器,他会给这个匿名命名空间生成一个独一无二的名字,保证一定不重复,然后在改文件中,using它。所以自然就只有这个文件本身能够调用里面的函数了。

我们的工具是一个纯头文件,所有的库想依赖该文件,都会直接include它,而include操作其实就是简单的copy文件的内容,所以这段代码就会进入每个文件自身中,成为其源码的一部分。如此,只要我们把单例维护的代码放在匿名命名空间中,就可以保证其在每个文件中有且只有一个。就不用担心不同的库之间的冲突了。

五、补充

最后,我编写的这个库,并没有花费太多的时间,不过编程的过程中,确实还是感受到一点快乐的。不知不觉,现在写代码的时候,更喜欢以一种工具或是框架的角度去审核自己的作品。相比于追求编程的速度,慢慢蜕变成追求更优雅的设计,更简洁和实用的功能以及尽可能好的兼容性。

这里,小喵与你共同进步!

TICTOC: Header Only C++ Timer的更多相关文章

  1. IP分片重组的分析和常见碎片攻击 v0.2

    IP分片重组的分析和常见碎片攻击 v0.2http://www.nsfocus.net/index.php?act=magazine&do=view&mid=584 作者:yawl ( ...

  2. Python使用requests模块下载图片

    MySQL中事先保存好爬取到的图片链接地址. 然后使用多线程把图片下载到本地. # coding: utf-8 import MySQLdb import requests import os imp ...

  3. Nginx Parsing HTTP Package、header/post/files/args Sourcecode Analysis

    catalog . Nginx源码结构 . HTTP Request Header解析流程 . HTTP Request Body解析流程 1. Nginx源码结构 . core:Nginx的核心源代 ...

  4. ASM丢失disk header导致ORA-15032、ORA-15040、ORA-15042 Diskgroup无法mount

    SQL> select * from v$version; BANNER --------------------------– Oracle Database 11g Enterprise E ...

  5. 利用缓存、Timer间隔时间发送微信的实例,很有用的例子

    //Class WechatOfferExcutor 此类为微信触发类,属于上层调用类,其中有用到用静态变量缓存offer信息,Task异步执行发送方法等 using Newtonsoft.Json. ...

  6. Node.js timer的优化故事

    前几天nodejs发布了新版本4.0,其中涉及到一个更新比较多的模块,那就是下面要介绍的timer模块. timers: Improved timer performance from porting ...

  7. WPF下可编辑Header的Tab控件实现

    介绍 有这样一个需求,当用户双击Tab控件Header区域时, 希望可以直接编辑.对于WPF控件,提供一个ControlTemplate在加上一些Trigger就可以实现.效果如下: 代码 首先,我们 ...

  8. POSIX Timer

    SYNOPSIS #include <signal.h> /* only timer_create need this header */ #include <time.h> ...

  9. 从NSTimer的失效性谈起(二):关于GCD Timer和libdispatch

    一.GCD Timer的创建和安放 尽管GCD Timer并不依赖于NSRunLoop,可是有没有可能在某种情况下,GCD Timer也失效了?就好比一開始我们也不知道NSTimer相应着一个runl ...

随机推荐

  1. [Visual Studio] [Config] [Transformation] [SlowCheetah] 在非Web工程中使用Transformation

    1. 为VS安装SlowCheetah插件 https://marketplace.visualstudio.com/items?itemName=VisualStudioProductTeam.Sl ...

  2. INNODB insert buffer 简单分析

    在mysql5.1 之前称为Insert Buffer, 优化2级非唯一索引上插入操作的读IO, 在5.5之后改名为Change Buffer, 功能也扩展为2级非唯一索引上的插入.删除.更新.pur ...

  3. 转:C#综合揭秘——细说进程、应用程序域与上下文之间的关系

    引言 本文主要是介绍进程(Process).应用程序域(AppDomain)..NET上下文(Context)的概念与操作.虽然在一般的开发当中这三者并不常用,但熟悉三者的关系,深入了解其作用,对提高 ...

  4. 用LinkedList

      >用LinkedList模拟栈集合MyStack >MyStack测试类   用LinkedList模拟栈集合MyStack import java.util.LinkedList; ...

  5. TiDB数据库 mydumper命令导出数据报错:(mydumper:1908): CRITICAL **: Couldn't acquire global lock, snapshots will not be consistent: Access denied for user 'super'@'%' (using password: YES)

    今天想使用Tidb官方提供的mydumper来备份AWS上的RDS集群中mysql数据库的某个表,发现报错了 [tidb@:xxx /usr/local/tidb-tools]$ -t -F -B x ...

  6. priority_queue的优先级变化(结构体的写法)

    priority_queue的优先级变化(结构体的写法) 在头文件中加上#include <queue> 即可使用stl中的库函数priority_queue,优先队列默认的是从大到小的优 ...

  7. mac brew安装mysql

    mac不自带mysql,这里需要重新安装,方法依然很简单 brew install mysql unset TMPDIR mysql_install_db --verbose --user=`whoa ...

  8. 服务器上u盘装机centos7.2

    说明: 截止目前CentOS 7.x最新版本为CentOS 7.2.1511,下面介绍CentOS 7.2.1511的具体安装配置过程 服务器相关设置如下: 操作系统:CentOS 7.2.1511 ...

  9. NOIP模拟赛-2018.11.6

    NOIP模拟赛 今天想着反正高一高二都要考试,那么干脆跟着高二考吧,因为高二的比赛更有技术含量(我自己带的键盘放在这里). 今天考了一套英文题?发现阅读理解还是有一些困难的. T1:有$n$个点,$m ...

  10. HTML5调用百度地图API获取当前位置并直接导航目的地的方法

    <!DOCTYPE html> <html lang="zh-cmn-Hans">     <meta charset="UTF-8&quo ...