当前位置: 移动技术网 > IT编程>开发语言>JavaScript > 深入V8引擎-Time核心方法之win篇(2)

深入V8引擎-Time核心方法之win篇(2)

2019年05月29日  | 移动技术网IT编程  | 我要评论

  这一篇讲windows系统下timeticks的实现。

  对于tick,v8写了相当长的一段discussion来讨论windows系统上计数的三种实现方法以及各自的优劣,注释在time.cc的572行,这里直接简单翻译一下,不贴出来了。

cpu cycle counter.(retrieved via rdtsc)

  cpu计数器拥有最高的分辨率,消耗也是最小的。然而,在一些老的cpu上会有问题;1、每个处理器独立唯一各自的tick,并且处理器之间不会同步数据。2、计数器会因为温度、功率等原因频繁变化,有些情况甚至会停止。

queryperformancecounter (qpc)

  qpc计数法就是之前libuv用的api,分辨率也相当的高。比起cpu计数器,优点就是不存在多处理器有多个tick,保证数据的唯一。但是在老的cpu上,也会因为bios、hal而出现一些问题。

system time

  通过别的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;

  // 如果windows不支持qpc或者该方法不可靠 会降级去使用低分辨率的lowb方法
  timeticksnowfunction now_function;
  cpu cpu;
  // qpc不好使的情况
  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 = {};
  // according to the msdn documentation for queryperformancecounter(), this
  // will never fail on systems that run xp or later.
  // https://msdn.microsoft.com/library/windows/desktop/ms644904.aspx
  // 这里说理论上xp以后的系统都支持qpc
  bool result = ::queryperformancecounter(&perf_counter_now);
  return perf_counter_now.quadpart;
}

// to avoid overflow in qpc to microseconds calculations, since we multiply
// by kmicrosecondspersecond, then the qpc value should not exceed
// (2^63 - 1) / 1e6. if it exceeds that threshold, we divide then multiply.
static constexpr int64_t kqpcoverflowthreshold = int64_c(0x8637bd05af7);

timedelta qpcvaluetotimedelta(longlong qpc_value) {
  // 这里的if/else逻辑见上面静态变量的注释 也可以看我下面翻译的
  // 理论上的计算公式是 (qpc_count * 1e6) / qpc_count_per_second 得到微秒单位的硬件时间戳
  // 但是int64类型最大只能处理2^63 - 1 而这个windowsapi返回的数字(换算乘以1e6后)可能超过这个范围
  // 如果数字过大 就用先除再乘的方式计算避免溢出

  // 正常情况
  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++深入研究后再来解释。

  1. 按照英文注释,qpc乘以1e6后过大,再除以一个数时会溢出。但是下面的那个方法用的是1个溢出数加上1个小整数,为啥这样就不会出问题。难道加减不存在threshold?
  2. 那个计算误差是我理解的,实际上如果上过小学,把上面的变量代入第二个算式,会得到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;

  有三个条件表明qpc不适用。

  第一个很直白,api在当前操作系统不支持。

  第二个是通过cpu判断qpc是否可靠,具体原理十分麻烦,有兴趣单独开一篇解释吧。

  第三个就比较简单,有些牌子的cpu就是垃圾,直接根据内置api返回的参数判断是不是不支持的类型,如下。

bool isbuggyathlon(const cpu& cpu) {
  // on athlon x2 cpus (e.g. model 15) queryperformancecounter is unreliable.
  return strcmp(cpu.vendor(), "authenticamd") == 0 && cpu.family() == 15;
}

  

  正式进入qpc不支持分支。

union lasttimeandrolloversstate {
  // 完整的32位时间
  int32_t as_opaque_32;

  struct {
    // 时间头8位
    uint8_t last_8;
    // 时间重置次数
    uint16_t rollovers;
  } as_values;
};

timeticks rolloverprotectednow() {
  // 见上面的解释
  lasttimeandrolloversstate state;
  dword now;  // dword is always unsigned 32 bits.

  // 这是一个原子操作数 线程安全
  int32_t original = g_last_time_and_rollovers.load(std::memory_order_acquire);
  while (true) {
    // 类型为int32位整数
    state.as_opaque_32 = original;
    // 定义如下 实际上就是windowsapi的timegettime
    // dword timegettimewrapper() { return timegettime(); }
    // dword (*g_tick_function)(void) = &timegettimewrapper;
    now = g_tick_function();
    // 移位后只获取头8位
    uint8_t now_8 = static_cast<uint8_t>(now >> 24);
    // 当头8位的时间比保存的要小时 说明返回值重置了
    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;
    }
  }
  // 返回次数 * 2^32 加上 当前时间
  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举例子,内存结构如下所示。

  整个过程大概是这样的。

  1. 每次获取timegettime的值,只获取头8位的值now_8。
  2. 判断now_8是否小于union里面保存的last_8,如果小了(从1111...1111变成000...1),说明时间重置了,将重置次数+1。
  3. 替换last_8为新获取的now_8。
  4. 判断当前整个整数是否与上一次获取时相同(涉及多线程操作),相同的话直接返回输出结果。

  最后返回值的计算也很简单了,就是重置次数rollovers乘以重置一次的时间2^32,加上当前获取的now,得到总的硬件时间戳。

 

  完事了。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网