概述
获取时间戳
GetTickCount
Windows平台, 可通过GetTickCount和GetTickCount64获取时间戳。它们底层实现是一样的, 返回值的位宽不同。GetTickCount返回uint32_t
,最大值2^32,单位毫秒, 系统运行49.71天必定发生绕回的现象,程序处理不好很容易出问题,不建议使用。 GetTickCount64返回的是uint64_t
,5.8亿年左右才绕回,更加直观安全。
QueryPerformanceCounter (QPC)
Windows平台,可通过QueryPerformanceCounter (QPC)相关API, 来获取高精度时间戳。
c++11中的std::chrono::high_resolution_clock
以及std::chrono::steady_clock
就是通过QPC API来实现的。
high_resolution_clock
当前与steady_clock
的实现一样, 调用了_Query_perf_frequency()
以及_Query_perf_counter()
函数。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hq3g8fnx-1640677558215)(https://note.youdao.com/yws/res/e/WEBRESOURCEd29b5055865e10a132d245303ef627ce)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FpLhrQ2n-1640677558219)(https://note.youdao.com/yws/res/c/WEBRESOURCEfa77319c142400ec04de03be9331965c)]
而_Query_perf_frequency()
的底层则是调用的QueryPerformanceCounter。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5UKAARnt-1640677558220)(https://note.youdao.com/yws/res/6/WEBRESOURCEcb3623be9fdcf71b036431fb1575e6a6)]
API定义
BOOL QueryPerformanceFrequency(
[out] LARGE_INTEGER *lpFrequency
);
BOOL QueryPerformanceCounter(
[out] LARGE_INTEGER *lpPerformanceCount
);
官方例子:
LARGE_INTEGER StartingTime, EndingTime, ElapsedMicroseconds;
LARGE_INTEGER Frequency;
QueryPerformanceFrequency(&Frequency);
QueryPerformanceCounter(&StartingTime);
// Activity to be timed
QueryPerformanceCounter(&EndingTime);
ElapsedMicroseconds.QuadPart = EndingTime.QuadPart - StartingTime.QuadPart;
//
// We now have the elapsed number of ticks, along with the
// number of ticks-per-second. We use these values
// to convert to the number of elapsed microseconds.
// To guard against loss-of-precision, we convert
// to microseconds *before* dividing by ticks-per-second.
//
ElapsedMicroseconds.QuadPart *= 1000000;
ElapsedMicroseconds.QuadPart /= Frequency.QuadPart;
获取精确时间戳的封装类
class AccurateClock
{
public:
// frequence指定1秒分成多少份, 默认1000表示单位ms。
AccurateClock(__int64 frequence = 1000) :_frequence_(frequence)
{
QueryPerformanceFrequency(&_system_freq_);
QueryPerformanceCounter(&_base_tick_);
}
~AccurateClock() {}
__int64 getTick() const
{
LARGE_INTEGER current_tick;
QueryPerformanceCounter(¤t_tick);
return (_frequence_ * (current_tick.QuadPart - _base_tick_.QuadPart) / _system_freq_.QuadPart);
}
private:
LARGE_INTEGER _base_tick_;
LARGE_INTEGER _system_freq_;
__int64 _frequence_;
};
参考资料 https://docs.microsoft.com/en-us/windows/win32/sysinfo/acquiring-high-resolution-time-stamps
Sleep函数
Sleep函数表示线程休眠, 不占用CPU, 单位ms。 但是精度有限,我的测试机是10年前的机器,第一代i7, 操作系统是win10
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2060YZsH-1640677558221)(https://note.youdao.com/yws/res/8/WEBRESOURCE54c4e197837d15f156974ff93202a148)]
调用Sleep(1000)误差比较小, 但调用Sleep(1)时误差将会变得很大。看以下测试。
AccurateClock clock;
while (true) {
ULONGLONG t1 = GetTickCount64();
__int64 c1 = clock.getTick();
Sleep(1000);
ULONGLONG t2 = GetTickCount64();
__int64 c2 = clock.getTick();
printf("Sleep(1000) cost %lld , %lld msn", t2 - t1, c2 - c1);
}
结果如下,相对1000ms来看, 有时候偏差15-16ms还算可以接受。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dswA8O0n-1640677558222)(https://note.youdao.com/yws/res/0/WEBRESOURCE2c738d499ac7967b22348ffb125f9bb0)]
当我们把Sleep(1000)改成1000个Sleep(1)时
AccurateClock clock;
while (true) {
ULONGLONG t1 = GetTickCount64();
__int64 c1 = clock.getTick();
for (int i = 0; i < 1000; i++) {
Sleep(1);
}
ULONGLONG t2 = GetTickCount64();
__int64 c2 = clock.getTick();
printf("Sleep(1000) cost %lld , %lld msn", t2 - t1, c2 - c1);
}
结果令人惊讶,如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B1kM8bi9-1640677558223)(https://note.youdao.com/yws/res/f/WEBRESOURCEdfb842ef92deb2cf19bafbb9b9c8e2df)]
平均每个Sleep(1)实际耗费了15.xx ms。
增加Sleep精度的API。
MMRESULT timeBeginPeriod(
UINT uPeriod // 需要的最小时间精度,单位ms
);
- 参考资料 https://docs.microsoft.com/en-us/windows/win32/api/timeapi/nf-timeapi-timebeginperiod
- timeBeginPeriod可提升系统的等待函数的精度,达到ms级别。
- timeBeginPeriod与timeEndPeriod配对使用。
- timeBeginPeriod并不会提升QueryPerformanceCounter (QPC)函数的精度
- timeBeginPeriod只对当前进程有效。其他进程精度还是按照默认。
- timeBeginPeriod会降低系统的整体性能,我在实际使用中, 对这个下降并没太大感觉。即使有,我们的需求也是Sleep(1)的精度比性能更加重要。
增加timeBeginPeriod相关代码后测试
Sleep(1000)
UINT Time_Period = 1;
MMRESULT result = timeBeginPeriod(Time_Period); // Setup the high accuracy clock.
AccurateClock clock;
while (true) {
ULONGLONG t1 = GetTickCount64();
__int64 c1 = clock.getTick();
Sleep(1000);
ULONGLONG t2 = GetTickCount64();
__int64 c2 = clock.getTick();
printf("Sleep(1000) cost %lld , %lld msn", t2 - t1, c2 - c1);
}
timeEndPeriod(Time_Period);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2p9KF7HA-1640677558224)(https://note.youdao.com/yws/res/b/WEBRESOURCE5203f2c232b76ab8c2474d01e092bc5b)]
Sleep(1000)偶尔会有1ms的误差,算比较实用的状态。
1000个Sleep(1)
UINT Time_Period = 1;
MMRESULT result = timeBeginPeriod(Time_Period); // Setup the high accuracy clock.
AccurateClock clock;
while (true) {
ULONGLONG t1 = GetTickCount64();
__int64 c1 = clock.getTick();
for (int i = 0; i < 1000; i++) {
Sleep(1);
}
ULONGLONG t2 = GetTickCount64();
__int64 c2 = clock.getTick();
printf("Sleep(1000) cost %lld , %lld msn", t2 - t1, c2 - c1);
}
timeEndPeriod(Time_Period);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xZixeB2A-1640677558225)(https://note.youdao.com/yws/res/2/WEBRESOURCEf62a7d1e6d490c83a63797c9a223c962)]
最终测试结果接近2000ms, 每个Sleep(1)大约耗费了2ms的时间。
100个Sleep(10)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d2gZVu0n-1640677558226)(https://note.youdao.com/yws/res/4/WEBRESOURCE4f6b051b17744a7c1ea2b2a35397a1b4)]
平均每个Sleep(10)耗费了接近11ms的时间。
10个Sleep(100)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vEytHIou-1640677558226)(https://note.youdao.com/yws/res/6/WEBRESOURCEbcc09f26c9e67ff654b9e660c7d20b76)]
每个Sleep(100)看似101ms。
std::this_thread::sleep_for
用c++11的std::this_thread::sleep_for
代替Sleep上面几个测试结果与Sleep几乎一样。
高精度Sleep总结
从以上几个测试看,似乎跟Sleep调用次数有关,每个Sleep调用可能会有1ms的误差, 当Sleep的值比较大时,例如1000ms,这个误差相对就很小。 如果值比较小,例如Sleep(1),误差就会显得很大。
是否存在比timeBeginPeriod + Sleep更高精度的休眠方案?
网络编程socket API里的select函数同样具有休眠作用。
利用select函数实现的sleep功能。
/*
对select睡眠功能做封装。
*/
class SelectSleeper
{
public:
SelectSleeper()
{
_sock_ = INVALID_SOCKET;
}
~SelectSleeper()
{
CloseAndSetNull(&_sock_);
}
bool create(const char* local_interface, unsigned short port)
{
CloseAndSetNull(&_sock_);
if (local_interface == nullptr) {
return false;
}
_sock_ = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (_sock_ == INVALID_SOCKET) {
return false;
}
SOCKADDR_IN local_addr;
memset(&local_addr, 0x0, sizeof(local_addr));
local_addr.sin_family = AF_INET;
local_addr.sin_addr.S_un.S_addr = inet_addr(local_interface);
local_addr.sin_port = htons(port);
if (bind(_sock_, (sockaddr*)&local_addr, sizeof(local_addr)) != 0) {
CloseAndSetNull(&_sock_);
return false;
}
return true;
}
void close()
{
CloseAndSetNull(&_sock_);
}
// 单位微秒 10的-6次方秒。
bool usleep(__int64 timeout_us)
{
if (_sock_ == INVALID_SOCKET) {
return false;
}
if (timeout_us < 0) {
return false;
}
fd_set fd_read;
FD_ZERO(&fd_read);
FD_SET(_sock_, &fd_read);
struct timeval time_val;
time_val.tv_sec = timeout_us / 1000000L;
time_val.tv_usec = timeout_us % 1000000L;
int result = select(0, 0, 0, &fd_read, &time_val);
if (result != 0) {
return false; // 意料之外的情况, <0表示错误, >0表示有客户端连接过来了。
}
return true;
}
private:
SOCKET _sock_;
static void CloseAndSetNull(SOCKET* p_sock)
{
if (*p_sock != INVALID_SOCKET) {
closesocket(*p_sock);
*p_sock = INVALID_SOCKET;
}
}
};
用select来睡眠的测试程序。
WSADATA wsa_data;
WSAStartup(MAKEWORD(2, 2), &wsa_data);
UINT Time_Period = 1;
MMRESULT result = timeBeginPeriod(Time_Period); // Setup the high accuracy clock.
SelectSleeper select_sleeper;
select_sleeper.create("127.0.0.1", 29999);
AccurateClock clock;
while (true) {
ULONGLONG t1 = GetTickCount64();
__int64 c1 = clock.getTick();
for (int i = 0; i < 1000; i++) {
select_sleeper.usleep(1000);
}
ULONGLONG t2 = GetTickCount64();
__int64 c2 = clock.getTick();
printf("Sleep(1000) cost %lld , %lld msn", t2 - t1, c2 - c1);
}
timeEndPeriod(Time_Period);
WSACleanup();
开启timeBeginPeriod测试结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oQAx4LRV-1640677558227)(https://note.youdao.com/yws/res/1/WEBRESOURCEdf238c32b8950ad3f9ddb5898eebd941)]
表现比Sleep(1)要提升约50%, select睡眠1ms的平均耗时为1.5ms。
不开启timeBeginPeriod测试结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ufifomr0-1640677558228)(https://note.youdao.com/yws/res/7/WEBRESOURCE07d5406626bc5c20faf33a46207d0fe7)]
跟Sleep的表现差不多。睡眠1ms也是耗时15.xx ms。
select实现sleep功能的注意事项
- 时间精确度上较Sleep函数有一定提升。
- 占用socket资源, 需要与其他模块协调,避免冲突。
- 不允许多线程使用,每个线程独立申请socket资源。
更换成新的CPU以及操作系统
除了我自己的老机器,我分别还在两台不同的强劲的新机器上更新的系统上做测试。测试发现,没有开启timeBeginPeriod以及开启的测试结果一样, 感觉好像默认支持了高精度。另外有一个重大发现, Sleep的表现我的老机器跟新机器表现差别不大, 但是select函数的表现有巨大差异。
其中一台16系统, Intel双CPU
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zht78yQs-1640677558228)(https://note.youdao.com/yws/res/9/WEBRESOURCE89d6f3d99a605b7cbe81d339e32d4ac9)]
1000次select睡眠1ms的结果如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y8eiyWjW-1640677558229)(https://note.youdao.com/yws/res/e/WEBRESOURCEb50b74cc90350e9266b2175d3714965e)]
平均每个1ms的select休眠1.05ms左右。而且是否开启timeBeginPeriod结果一样。
另外一台19系统,AMD的CPU
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U1wqhZqO-1640677558230)(https://note.youdao.com/yws/res/6/WEBRESOURCE8d04331d721ac159e0893ce49df366d6)]
1000次select睡眠1ms的结果如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GZZjUxEd-1640677558230)(https://note.youdao.com/yws/res/d/WEBRESOURCEebb13059211cbdfab76f3aab8718a7cd)]
简直神了,几乎完全精确。
总结
- timeBeginPeriod能够提高进程的等待函数的时间精度, 在某些新的硬件平台和操作系统上,好像默认就是高精度。建议:兼容所有情况,坚持调用。
- Sleep的精度在不同平台默认的表现差异可能会比较大,开启timeBeginPeriod高精度以后,一般每个Sleep调用在精确的基础上会多1ms时间左右。例如Sleep(1000)可能实际是1001ms, Sleep(10)实际是11ms, Sleep(1)实际是2ms…
- 在开启timeBeginPeriod高精度的情况下,select函数睡眠的精度普遍比Sleep要好。一些新的硬件平台和操作系统, 甚至可以做到完全精确。 由于我没有更多的测试环境了, 也不确定select的精度跟操作系统,还是硬件资源有关系。
最后
以上就是开放招牌为你收集整理的Win平台高精度Sleep实现的全部内容,希望文章能够帮你解决Win平台高精度Sleep实现所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复