mktime为什么这么慢

/******************************************************************************************
*              版权声明
*   本文为本人原创,本人拥有此文的版权。鉴于本人持续受益于开源软件社区,
* 本人声明:任何个人及团体均可不受限制的转载和复制本文,无论是否用于盈利
* 之目的,但不得修改文章内容,并必须在转载及复制时同时保留本版权声明,否
*   则为侵权行为,本人保留追究相应主体法律责任之权利。
*               speng2005@gmail.com
*                  2015-11
******************************************************************************************/


       最近写了个C++小程序,没想到栽在mktime函数上,以前很少用过这个函数。

       事情是这样的,在Linux上(debain,kernel 2.6.26)写一个C++小程序,实现将txt文件中的数据提取后简单处理并插入mysql数据库。逻辑很简单,程序也不大,写完以后跑600万行的txt数据文件,感觉比较慢,于是开始使用一个较小的数据文件跑优化工具来分析代码瓶颈。先后使用了OProfile,gprof和google-perftools这三个工具,最后发现还是google-perftools最好用。经过分析找到程序热点并不在之前所认为的mysql接口函数上,而是在一个将"1990-12-19"格式的字符串转换为日历时间即time_t类型的函数上,代码如下:

time_t getTimeOfDate(const string & dateStr, string * timeStr = NULL)
{
        stringstream & mySS = getMyss();  //可以以'-'为分隔符的stringstream
        mySS.clear();
        mySS.str(dateStr);
        int year,month,day;
        mySS>>year>>month>>day;
        if( month > 12 || day > 31 ) return (time_t)-1;
        struct tm stm;
        stm.tm_year = year - 1900;
        stm.tm_mon = month - 1;
        stm.tm_mday = day;
        stm.tm_hour = 0;
        stm.tm_min = 0;
        stm.tm_sec = 0;
        time_t t = mktime(&stm);
        if(timeStr)
        {
                mySS.clear();
                mySS.str("");
                mySS<<t;
                *timeStr = mySS.str();
        }
        return t;

}

       如上代码跑完测试数据文件需要284秒,其中程序总运行时间的83.3%花在getTimeOfDate函数上,而在该函数内竟然主要时间消耗在mktime函数调用上,在该函数上消耗了程序总运行时间的82.3%,这真是出乎意料!此时google-perftools的调用图如下:

        上图中__GI_mktime函数以及其下层函数调用树即是我用的Linux平台上对标准c库中mktime函数的具体实现,可以说还是相当复杂的。根据调用图分析后又查了些资料,知道原因在于mktime函数调用中要处理时区和夏令时问题。仔细看看上面getTimeOfDate函数代码中,发现犯了个低级错误,未对struct tm stm结构的所有成员进行内存初始化,从而导致后面调用的mktime函数中将自动尝试设置正确的时区信息并调整夏令时,而这操作在本人使用的linux系统上意味着要扫描系统时区配置文件并分析(通过strace命令跟踪也证实程序运行过程中频繁地对/etc/localtime文件进行stat64系统api调用),这就是程序运行缓慢的主要原因。于是对getTimeOfDate函数代码进行改进,如下:

time_t getTimeOfDate(const string & dateStr, string * timeStr = NULL)
{
        stringstream & mySS = getMyss();  //可以以'-'为分隔符的stringstream
        mySS.clear();
        mySS.str(dateStr);
        int year,month,day;
        mySS>>year>>month>>day;
        if( month > 12 || day > 31 ) return (time_t)-1;
        struct tm stm[2];
        int * pStm = (int*)&stm[0];
        fill(pStm, pStm + sizeof(tm)/sizeof(int) + 1, 0);
        stm[0].tm_year = year - 1900;
        stm[0].tm_mon = month - 1;
        stm[0].tm_mday = day;
        time_t t = mktime(&stm[0]);
        if(timeStr)
        {
                mySS.clear();
                mySS.str("");
                mySS<<t;
                *timeStr = mySS.str();
        }
        return t;
}

       用同样的测试环境跑优化了getTimeOfDate函数的程序来处理同样多的数据文件,跑完仅需要43秒,性能提高了5倍!此时google-perftools分析的调用图如下:

       从图中可以看到getTimeOfDate函数仅占用了程序总运行时间的26.3%,而其中mktime函数调用仅占用了程序总运行时间的12.6%,程序性能已经得到优化。

       后来看到网上资料说如果设置了TZ环境变量可以加快mktime的执行速度,于是又测了一下,在启动我的测试程序之前先设置TZ环境变量:export TZ=UTC0,然后跑完同样数据文件花了40秒,性能略有提高,表面看效果不明显。但如果分析google-perftools产生的调用图,还是可以看到明显区别的,如下:

       从图中可以看到getTimeOfDate函数仅占用了程序总运行时间的17.6%,而其中mktime函数调用仅占用了程序总运行时间的1.4%,关于对mktime标准c函数的调用已经得到很大的优化。

       综上来看,如果你的程序要频繁调用mktime函数,一定要在调用之前对struct tm的所有成员尤其是时区进行合理初始化。查了一下struct tm的完整定义,发现不同平台和开发环境的定义还不太相同,也没工夫去查标准c的定义,暂以下面这个参考吧:

struct tm {
  int tm_sec; /* 秒–取值区间为[0,59] */
  int tm_min; /* 分 - 取值区间为[0,59] */
  int tm_hour; /* 时 - 取值区间为[0,23] */
  int tm_mday; /* 一个月中的日期 - 取值区间为[1,31] */
  int tm_mon; /* 月份(从一月开始,0代表一月) - 取值区间为[0,11] */
  int tm_year; /* 年份,其值从1900开始 */
  int tm_wday; /* 星期–取值区间为[0,6],其中0代表星期天,1代表星期一,以此类推 */
  int tm_yday; /* 从每年的1月1日开始的天数–取值区间为[0,365],其中0代表1月1日,1代表1月2日,以此类推 */
  int tm_isdst; /* 夏令时标识符,实行夏令时的时候,tm_isdst为正。不实行夏令时的进候,tm_isdst为0;不了解情况时,tm_isdst()为负。*/
  long int tm_gmtoff; /*指定了日期变更线东面时区中UTC东部时区正秒数或UTC西部时区的负秒数*/
  const char *tm_zone; /*当前时区的名字(与环境变量TZ有关)*/

};

在不同环境中struct tm结构定义的差别主要在tm_isdst,tm_gmtoff,tm_zone这三个成员上,有的系统没定义这三个成员,有的系统定义了一部分,有的系统定义了但名称不一样。不管怎么样,在使用mktime前你应该根据系统情况对这三个成员进行初始化,最简单的方法就是全部置0。



### mktime函数的作用及使用方法 `mktime` 是 C++ 标准库 `<ctime>` 中的一个重要函数,主要用于将 `struct tm` 类型的时间结构体转换为表示自 1970-01-01 00:00:00 UTC 起经过的秒数(即 Unix 时间戳)类型的 `time_t` 值[^1]。 #### 函数原型 以下是 `mktime` 的标准函数声明: ```cpp #include <ctime> time_t mktime(struct tm* timeptr); ``` 参数说明: - **`timeptr`**: 指向一个 `struct tm` 对象的指针。此对象包含了待转换的具体时间信息。 返回值: - 成功时返回对应的 `time_t` 值。 - 如果无法计算有效的时间,则返回 `-1` 表示错误。 --- #### `struct tm` 结构体定义 `struct tm` 定义了一个存储分解时间的数据结构,具体成员及其含义如下[^2]: ```cpp struct tm { int tm_sec; /* 秒 – 取值区间为[0,59] */ int tm_min; /* 分钟 - 取值区间为[0,59] */ int tm_hour; /* 小时 - 取值区间为[0,23] */ int tm_mday; /* 日 - 取值区间为[1,31] */ int tm_mon; /* 月 (从一月开始计数,0代表一月) - 取值区间为[0,11] */ int tm_year; /* 年份,其值等于实际年份减去1900 */ int tm_wday; /* 星期几 - 取值区间为[0,6],其中0代表星期天 */ int tm_yday; /* 当前年度中的第几天 - 取值区间为[0,365] */ int tm_isdst; /* 是否处于夏令时期间标志位 */ }; ``` 注意:`tm_year` 字段是以 1900 为基础的偏移量,因此如果要设置当前年份为 2023,则应将其设为 `2023 - 1900 = 123`。 --- #### 使用示例 以下是一个完整的例子展示如何使用 `mktime` 来获取指定日期对应的时间戳: ```cpp #include <iostream> #include <ctime> int main() { struct tm timeinfo; // 设置目标时间为 2023 年 10 月 1 日 12:30:45 timeinfo.tm_year = 2023 - 1900; // 年份相对于 1900 的差值 timeinfo.tm_mon = 9; // 十月,取值范围 [0,11] timeinfo.tm_mday = 1; // 一日 timeinfo.tm_hour = 12; // 下午十二点 timeinfo.tm_min = 30; // 三十分钟 timeinfo.tm_sec = 45; // 四十五秒 // 自动调整并转换为目标时间戳 time_t timestamp = mktime(&timeinfo); if (timestamp != -1) { std::cout << "Unix Timestamp: " << timestamp << std::endl; // 打印调整后的 tm 结构体内容 char buffer[80]; strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &timeinfo); std::cout << "Adjusted Time: " << buffer << std::endl; } else { std::cerr << "Error converting the specified date." << std::endl; } return 0; } ``` 运行结果可能类似于: ``` Unix Timestamp: 1696137045 Adjusted Time: 2023-10-01 12:30:45 ``` --- #### 关键特性 1. **自动校正功能** 在调用 `mktime` 后,传入的 `struct tm` 参数会被修改以反映标准化的时间值。例如,即使输入超出正常范围的数值(如 `tm_hour=25`),它也会被适当地规范化到合法范围内。 2. **处理闰年逻辑** 内部实现会考虑闰年的特殊情况来确保准确性。比如,在判断某一年是否为闰年时采用特定规则而非简单算法[^3]。 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值