這一篇講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,獲得總的硬件時間戳。