Win平台高精度Sleep实现

12 篇文章 0 订阅

获取时间戳

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(&current_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 ms\n", 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 ms\n", 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 ms\n", 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 ms\n", 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 ms\n", 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功能的注意事项

  1. 时间精确度上较Sleep函数有一定提升。
  2. 占用socket资源, 需要与其他模块协调,避免冲突。
  3. 不允许多线程使用,每个线程独立申请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)]

简直神了,几乎完全精确。

总结

  1. timeBeginPeriod能够提高进程的等待函数的时间精度, 在某些新的硬件平台和操作系统上,好像默认就是高精度。建议:兼容所有情况,坚持调用。
  2. Sleep的精度在不同平台默认的表现差异可能会比较大,开启timeBeginPeriod高精度以后,一般每个Sleep调用在精确的基础上会多1ms时间左右。例如Sleep(1000)可能实际是1001ms, Sleep(10)实际是11ms, Sleep(1)实际是2ms…
  3. 在开启timeBeginPeriod高精度的情况下,select函数睡眠的精度普遍比Sleep要好。一些新的硬件平台和操作系统, 甚至可以做到完全精确。 由于我没有更多的测试环境了, 也不确定select的精度跟操作系统,还是硬件资源有关系。
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值