这一篇讲windows系统下TimeTicks的实现。
对于tick,V8写了至关长的一段discussion来讨论windows系统上计数的三种实现方法以及各自的优劣,注释在time.cc的572行,这里直接简单翻译一下,不贴出来了。
CPU cycle counter.(Retrieved via RDTSC)
CPU计数器拥有最高的分辨率,消耗也是最小的。然而,在一些老的CPU上会有问题;一、每一个处理器独立惟一各自的tick,而且处理器之间不会同步数据。二、计数器会由于温度、功率等缘由频繁变化,有些状况甚至会中止。
QueryPerformanceCounter (QPC)
QPC计数法就是以前libuv用的API,分辨率也至关的高。比起CPU计数器,优势就是不存在多处理器有多个tick,保证数据的惟一。可是在老的CPU上,也会由于BIOS、HAL而出现一些问题。
经过别的windowsAPI返回的系统时间来计数。
上一篇Clock类的构造函数中,对TimeTicks属性的初始化也只是调用了老TimeTicks的Now方法,因此直接上Now的代码。
TimeTicks InitialTimeTicksNowFunction();
using TimeTicksNowFunction = decltype(&TimeTicks::Now);
TimeTicksNowFunction g_time_ticks_now_function = &InitialTimeTicksNowFunction;
TimeTicks TimeTicks::Now() {
TimeTicks ticks(g_time_ticks_now_function());
DCHECK(!ticks.IsNull());
return ticks;
}复制代码
windows系统下,会预先一个初始化方法,这里的语法不用去理解,只须要知道调用InitialTimeTicksNowFunction方法后,将其返回做为参数构造一个TimeTicks对象,返回的就是硬件时间戳。
TimeTicks InitialTimeTicksNowFunction() {
InitializeTimeTicksNowFunctionPointer();
return g_time_ticks_now_function();
}复制代码
能够看到,那个g_time_ticks_now_function又被调用了一次,可是做为一个函数指针,第二次调用的时候指向的就不是同一个方法。至于为何特地弄一个函数指针,后面会具体解释。
void InitializeTimeTicksNowFunctionPointer() {
LARGE_INTEGER ticks_per_sec = {};
if (!QueryPerformanceFrequency(&ticks_per_sec)) ticks_per_sec.QuadPart = 0;
TimeTicksNowFunction now_function;
CPU cpu;
if (ticks_per_sec.QuadPart <= 0 || !cpu.has_non_stop_time_stamp_counter() ||
IsBuggyAthlon(cpu)) {
now_function = &RolloverProtectedNow;
}
else {
now_function = &QPCNow;
}
g_qpc_ticks_per_second = ticks_per_sec.QuadPart;
ATOMIC_THREAD_FENCE(memory_order_release);
g_time_ticks_now_function = now_function;
}复制代码
从几个赋值能够看到,整个函数都是围绕着函数指针now_function的指向,其实也就是g_time_ticks_now_function,根据系统对QPC的支持,来选择不一样的方法实现TimeTicks。
因此,特地用一个函数指针来控制Now方法的目的也明显了,理论上只有第一次调用会进到这个特殊函数,检测当前操做系统的QPC是否适用,而后选择对应的方法。后面再次调用的时候,就直接进入选好的方法(具体思想能够参考《JavaScript高级程序设计》高级技巧章节的惰性载入函数)。这个状况有一点像我在
解析node事件轮询时提到的线程池初始化情形,不一样的是,这里V8没有特地去加一个锁来防止多线程竞态。缘由也很简单,由于此处只是对一个全局的函数指针作赋值,就算多赋值几回对后续的线程并无任何影响,没有必要特地作锁。
关于QueryPerformanceFrequency方法(这些函数名都好TM长)的具体用法,能够参考我
别的博客,啥都解释写不完啦。
存在两种状况的实现,先看支持QPC的,删掉了合法性检测宏,这些宏无处不在,太碍眼了。
TimeTicks QPCNow() { return TimeTicks() + QPCValueToTimeDelta(QPCNowRaw()); }
V8_INLINE uint64_t QPCNowRaw() {
LARGE_INTEGER perf_counter_now = {};
BOOL result = ::QueryPerformanceCounter(&perf_counter_now);
return perf_counter_now.QuadPart;
}
static constexpr int64_t kQPCOverflowThreshold = INT64_C(0x8637BD05AF7);
TimeDelta QPCValueToTimeDelta(LONGLONG qpc_value) {
if (qpc_value < TimeTicks::kQPCOverflowThreshold) {
return TimeDelta::FromMicroseconds(
qpc_value * TimeTicks::kMicrosecondsPerSecond / g_qpc_ticks_per_second);
}
int64_t whole_seconds = qpc_value / g_qpc_ticks_per_second;
int64_t leftover_ticks = qpc_value - (whole_seconds * g_qpc_ticks_per_second);
return TimeDelta::FromMicroseconds(
(whole_seconds * TimeTicks::kMicrosecondsPerSecond) +
((leftover_ticks * TimeTicks::kMicrosecondsPerSecond) /
g_qpc_ticks_per_second));
}复制代码
直接看注释就行了,不过我有一些问题,先记录下来,后面对C++深刻研究后再来解释。
-
按照英文注释,qpc乘以1e6后过大,再除以一个数时会溢出。可是下面的那个方法用的是1个溢出数加上1个小整数,为啥这样就不会出问题。难道加减不存在threshold?
-
那个计算偏差是我理解的,实际上若是上太小学,把上面的变量代入第二个算式,会获得leftover_ticks为0,这里的逻辑暂时没理清。
总之,最后仍是利用了QPC的两个API获得硬件时间戳,跟libuv的套路差很少。
下面来看不支持QPC的状况,不过先过一下那个if。
CPU cpu;
if (ticks_per_sec.QuadPart <= 0 || !cpu.has_non_stop_time_stamp_counter() ||
IsBuggyAthlon(cpu)) {
now_function = &RolloverProtectedNow;复制代码
第二个是经过CPU判断QPC是否可靠,具体原理十分麻烦,有兴趣单独开一篇解释吧。
第三个就比较简单,有些牌子的CPU就是垃圾,直接根据内置API返回的参数判断是否是不支持的类型,以下。
bool IsBuggyAthlon(const CPU& cpu) {
return strcmp(cpu.vendor(), "AuthenticAMD") == 0 && cpu.family() == 15;
}复制代码
union LastTimeAndRolloversState {
int32_t as_opaque_32;
struct {
uint8_t last_8;
uint16_t rollovers;
} as_values;
};
TimeTicks RolloverProtectedNow() {
LastTimeAndRolloversState state;
DWORD now;
int32_t original = g_last_time_and_rollovers.load(std::memory_order_acquire);
while (true) {
state.as_opaque_32 = original;
now = g_tick_function();
uint8_t now_8 = static_cast<uint8_t>(now >> 24);
if (now_8 < state.as_values.last_8) ++state.as_values.rollovers;
state.as_values.last_8 = now_8;
if (state.as_opaque_32 == original) break;
if (g_last_time_and_rollovers.compare_exchange_weak(
original, state.as_opaque_32, std::memory_order_acq_rel)) {
break;
}
}
return TimeTicks() +
TimeDelta::FromMilliseconds(
now + (static_cast<uint64_t>(state.as_values.rollovers) << 32));
}复制代码
这块的内容至关多,首先须要解释一下上面的核心方法timeGetTime,官网的解释以下。
The timeGetTime function retrieves the system time, in milliseconds. The system time is the time elapsed since Windows was started.(检测系统启动后所通过的毫秒数) The return value wraps around to 0 every 2^32 milliseconds, which is about 49.71 days.(返回值会从0一直涨到2^32,而后又从0开始无限循环)
上面的第二段代表了为何要用那么复杂的处理,由于这个返回值不是无限变大,而是会重置为0。并且union这个东西也颇有意思,JS里面找不到对比的数据类型,相似于struct结构体,但不一样点是内存共用。拿源码中的union举例子,内存结构以下所示。
-
每次获取timeGetTime的值,只获取头8位的值now_8。
-
判断now_8是否小于union里面保存的last_8,若是小了(从1111...1111变成000...1),说明时间重置了,将重置次数+1。
-
替换last_8为新获取的now_8。
-
判断当前整个整数是否与上一次获取时相同(涉及多线程操做),相同的话直接返回输出结果。
最后返回值的计算也很简单了,就是重置次数rollovers乘以重置一次的时间2^32,加上当前获取的now,获得总的硬件时间戳。