| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 系统运维 -> 统计代码执行时间时,System.currentTimeMillis()与System.nanoTime()哪个更适合? -> 正文阅读 |
|
[系统运维]统计代码执行时间时,System.currentTimeMillis()与System.nanoTime()哪个更适合? |
目录 3.nanoTime与currentTimeMillis在JDK中阐述 4.nanoTime与currentTimeMillis使用对比 5.深究从OpenJDK源代码、glibc,一直到Linux内核 1.nanoTime是什么?ns(nanosecond):纳秒, 时间单位。一秒的10亿分之一,即等于10的负9次方秒。常用作 内存读写速度的单位。 java中System.nanoTime()返回的是纳秒,nanoTime而返回的可能是任意时间,甚至可能是负数 2.currentTimeMillis是什么?java中System.currentTimeMillis()返回的毫秒,这个毫秒其实就是自1970年1月1日0时起的毫秒数。 3.nanoTime与currentTimeMillis在JDK中阐述?System.nanoTime提供相对精确的计时,但是不能用他来计算当前日期,在jdk中的说明如下: public static long nanoTime() 返回最准确的可用系统计时器的当前值,以毫微秒为单位。 此方法只能用于测量已过的时间,与系统或钟表时间的其他任何时间概念无关。返回值表示从某一固定但任意的时间算起的毫微秒数(或许从以后算起,所以该值可能为负)。此方法提供毫微秒的精度,但不是必要的毫微秒的准确度。它对于值的更改频率没有作出保证。在取值范围大于约 292 年(263?毫微秒)的连续调用的不同点在于:由于数字溢出,将无法准确计算已过的时间。 例如,测试某些代码执行的时间长度: ?? long startTime = System.nanoTime(); ?? // ... the code being measured ... ?? long estimatedTime = System.nanoTime() - startTime; 返回: 系统计时器的当前值,以毫微秒为单位。 从以下版本开始:1.5 ?System.currentTimeMillis返回的是从1970.1.1 UTC 零点开始到现在的时间,精确到毫秒,平时我们可以根据System.currentTimeMillis来计算当前日期,星期几等,可以方便的与Date进行转换,下面时jdk中的介绍: public static long currentTimeMillis() 返回以毫秒为单位的当前时间(从1970.1.1 UTC 零点开始到现在的时间)。注意,当返回值的时间单位是毫秒时,值的粒度取决于底层操作系统,并且粒度可能更大。例如,许多操作系统以几十毫秒为单位测量时间。 请参阅?Date?类的描述,了解可能发生在“计算机时间”和协调世界时(UTC)之间的细微差异的讨论。 返回:当前时间与协调世界时 1970 年 1 月 1 日午夜之间的时间差(以毫秒为单位测量)。 转换时间戳测试:
4.nanoTime与currentTimeMillis使用对比
输出: 纳秒:1049877781195000 毫秒:1627616968258 10万List初始化纳米对应毫秒数1421 10万List初始化毫秒数1421 100万List初始化纳米对应毫秒数1613 100万List初始化毫秒数1614 5.深究从OpenJDK源代码、glibc,一直到Linux内核Java提供了两个时间测量基础调用:System.currentTimeMillis()和System.nanoTime()。这两个调用有以下几个区别。 1.计时起点的稳定性 System.currentTimeMillis()返回自UTC时间1970年1月1日(Unix纪元开始)开始经过的毫秒数。另一个调用,System.nanoTime()返回自某一固定但任意的时间点开始经过的纳秒数。 很明显,currentTimeMillis()的测量粒度为1毫秒。因此测量时间不能短于1毫秒。currentTimeMillis()用UTC时间1970年1月1日作为参照点,既有优点也有缺点。 有什么优点?可以在两个不同JVM上调用currentTimeMillis(),比较返回值。 有哪些缺点?如果计算机没有进行时间同步,那么比较的结果没有意义。服务集群中的时钟通常无法做到完全同步,总会有一些偏差。如果是比较两个不同系统产生的日志文件,时间戳不完全同步尚且可以接受。但是,有时候这种偏差可能会导致灾难性后果,例如分布式系统中的冲突解决。 2.时钟的单调性 另一个问题,不能保证返回值单调递增。这是什么含义?连续两次调用currentTimeMillis(),第二次调用的返回值可能小于第一次。这种结果违反直觉,而且可能导致荒谬的结果,比如经过的时间为负数。很明显,要计算程序内部运行经过的时间currentTimeMillis()不是一个好的选项。那么,用nanoTime()怎么样? System.nanoTime()没有用Unix纪元作为参考点,而是使用过去某个未指定时间点。该时间点仅在一次JVM运行期间保持不变,仅此而已。因此,即使比较同一台计算机上两个不同JVM的nanoTime()返回值也毫无意义,更不用说比较不同计算机上的调用结果。参考时间点通常与上一次计算机启动有关,不能依赖这种实现。这种做法的好处是,即使计算机的时钟时间由于某种原因倒退,也不会对nanoTime()产生任何影响。因此,nanoTime()是一个很棒的工具,可以测量单个JVM中两个事件之间经过的时间,但是不能用来比较不同JVM中的时间。 ?Java实现 接下来会探讨currentTimeMillis()和nanoTime()在Java中的实现机制。这里使用最新的OpenJDK 14源代码。System.currentTimeMillis()是一个本地方法,因此Java IDE不会告诉我们具体实现。这段本地代码看起来稍微好一点:
可以看到,这里会根据不同的操作系统委派不同的实现。下面是Linux上的实现: ?jlong os::javaTimeMillis() {
这段代码委托给一个Posix函数gettimeofday()。该函数返回一个简单的结构:
结构体中包含自Unix纪元开始经过的秒数,以及最后的微秒数。currentTimeMillis()返回的是自Unix纪元以来经过的毫秒数,因此必须进行简单的转换:jlong(time.tv_sec) * 1000 + jlong(time.tv_usec / 1000) gettimeofday()函数由glibc实现,最终调用Linux内核。稍后会进行更深入了解。 让我们看看nanoTime()如何实现:事实证明没有太大区别,System.nanoTime()也是一个本机方法:public static native long nanoTime();和jvm.cpp会交给操作系统实现:?
Linux中的javaTimeNanos()实现非常有趣:
实现包含2个分支:如果操作系统支持,那么使用单调时钟;否则还是委托给gettimeofday()。Gettimeofday()与之前System.currentTimeMillis()中用的同一个Posix调用。显然,由于nanoTime()的粒度更细,因此转换实现看起来有些不同,但使用的是同一个Posix调用!这意味着,在某些情况下System.nanoTime()使用Unix纪元作为参考点,因此也可以追溯过去!换句话说:不能保证单调性! 好消息是,据我所知所有现代Linux发行版都支持单调时钟。我认为这个分支是为了兼容早期kernel/glibc版本。假如你对HotSpot如何检测操作系统支持单调时钟感兴趣,请参阅这段代码。对大多数人而言,重要的要知道OpenJDK实际总是调用的是Posix函数clock_gettime(),该函数在glibc中实现,glibc会委托给Linux内核调用。 ?基准测试I、本地笔记本 现在,我们对nanoTime()和currentTimeMillis()的实现有了一个直观的认识。让我们看看它们速度是快是慢。下面是一个简单的JMH基准测试: ?
我的笔记本装的是Ubuntu 19.10,运行上面的基准测试结果如下: System.currentTimeMillis()每次调用大约需要29纳秒,System.nanoTime()大约每次需要25纳秒。结果中规中矩。这也意味着,使用System.nano()测试短于几十纳秒的调用是不明智的。因为测量本身的开销可能大于所要测量的时间段。此外,还要避免在密集的循环中使用nanoTime(),因为这么做会使得延迟迅速增加。另一方面,使用nanoTime()计算远程服务器的响应时间或者开销很大的计算过程很合适。 ? 基准测试II、AWS 虽然在笔记本上运行基准测试很方便,但并不实用。除非把笔记本贡献出来,作为应用的生产环境。让我们在AWS EC2中运行相同的基准测试。 启动一台安装了Ubuntu 16.04 LTS的c5.xlarge机器,并使用SDKMAN安装AdoptOpenJDK构建的Java 13: ?
?
运行结果如下: 与笔记本上的运行结果几乎一样,还不错。现在,试一下c3.large实例。虽然比较老,但是现在仍然经常使用: ? 结果看起来很糟糕!尽管c3.large实例有点老,预期的运行结果会有所下降,但这也差太多了!currentTimeMillis()和nanoTime()结果都慢了一个数量级。360纳秒一开始听起来感觉还不错,但要考虑这只是测量一次需要的时间,而实际要调用两次。因此,实际每次测量耗费大约0.7微秒。如果有10个探针测量不同的执行阶段,那么实际需要7微秒。详细分析:40gbit网卡的往返大约需要10微秒。这意味着,如果向热点路径增加一堆探针可能会带来非常大的延迟! ? 关于内核的调查 为什么C3实例比笔记本电脑或者C5实例慢得多?事实证明,这与Linux时钟源有关,更主要的与glibc-kernel接口有关。我们已经知道,每次调用nanoTime()或者currentTimeMillis()OpenJDK都会调用本机代码,本机代码会调用glibc,后者会调用Linux内核。 最有趣的部分是glibc-Linux内核转换:通常,当进程调用Linux内核函数(也称为syscall)时,涉及从用户模式切换到内核模式然后返回。这个转换是一个开销相对很高的操作,涉及许多步骤:
? 这些操作的开销通常很大。而且伴随着旁道攻击和相关保障措施的出现,开销越来越大。 对性能敏感的应用程序通常会尽量避免从用户态切换到内核态。Linux内核本身为一些频繁执行的系统调用提供了快捷方式,称为vDSO — 虚拟动态共享对象。它的本质是导出一些函数,把它们映射到进程的地址空间中。用户进程可以像普通共享库中的普通函数一样调用它们。事实证明,clock_gettime()和gettimeofday()都提供了这种快捷调用。因此,当glibc调clock_gettime()时,实际上不会执行用户态-内核态切换,仅仅是内存地址跳转。 所有这些听起来更像是一个有趣的理论,但并不能解释为什么System.nanoTime()在c3实例上运行这么慢。 ? 实验 这里使用另一个出色的Linux工具监视系统调用次数:perf。最简单的办法,启动基准测试并计算操作系统中的所有系统调用。perf的语法非常简单: sudo perf stat -e raw_syscalls:sys_enter -I 1000 -a,会计算每秒钟系统调用总数。一个重要的细节:它只统计真正的系统调用,即执行了完整用户模式-内核模式转换过程。vDSO不在统计范围之内。下面是c5实例上运行时的结果:
可以看到每秒大约执行了130个系统调用。鉴于我们基准测试的每次循环都小于30纳秒,很明显应用程序应该使用vDSO避开了系统调用。 下面是在c3实例上的运行结果:
每秒执行超过130万次系统调用!而且nanoTime()和currentTimeMillis()用时翻倍,大约为每次操作700纳秒。这是一个相当有力的证明,每次基准测试循环都会执行一次真实的系统调用! 让我们再启动一个perf命令以收集其他证据。这个命令会计算5秒钟内调用的所有系统调用并按名称分组: ? sudo perf stat -e 'syscalls:sys_enter_*' -a sleep 5 在c5实例上运行时没有任何异常。但是在c3实例上运行时,会看到下面的结果: 15695416 syscalls:sys_enter_gettimeofday (99.98%) 这就是确凿的证据!这个证据有力地证明了,在c3实例上运行基准测试时,会进行真正的gettimeofday()系统调用!为什么? 这是4.4内核(Ubuntu 16.04)的相关说明:
这是Java调用System.currentTimeMillis()时,由glibc调用的用户态内存映射函数。它会调用do_realtime(),并使用当前时间填充struct tv返回。重要的是,所有这些操作均在用户模式下执行,不会产生系统调用。好吧,do_realtime()返回VCLOCK_NONE时除外。在这种情况下,会调用vdso_fallback_gtod()执行系统调用。 为什么c3会fallback执行系统调用而c5不会?这与虚拟化技术的变化有关!AWS从一开始就使用Xen虚拟化。大约2年前,AWS宣布从Xen转为KVM虚拟化。c3实例使用Xen虚拟化,较新的c5实例则使用KVM。对我们而言重要的是要知道,每种技术使用了不同的Linux Clock实现。在/sys/devices/system/clocksource/clocksource0/current_clocksource中可以看到Linux当前时钟。 下面是c3的结果:
这是c5的结果:
事实证明,kvm-clock实现中会设置vclock_mode为VCLOCK_PVCLOCK,这样不会fallback到较慢的分支。Xen时钟源没有设置mode,而是保持了VCLOCK_NONE默认值。这样会跳转到vdso_fallback_gtod()函数,最后执行真正的系统调用! ?
Linux的优点在于它具有高度的可配置性,并且经常会让我们自取灭亡。可以尝试更改c3上的时钟源并重新运行基准测试。可以执行下面命令查看:
TSC是Time Stamp Counter的缩写,是一种非常快速的时间源。对于我们而言最重要的是知道它是一种很好的vDSO实现。把c3实例中的时钟源从Xen切换到TSC: # echo tsc > /sys/devices/system/clocksource/clocksource0/current_clocksource 检查是否完成切换:
? 看起来不错!现在重新运行基准测试: 结果看起来不错!实际上,比使用kvm-clock的c5实例表现更好。每秒系统调用数与c5实例处于同一数量级: ?
有些人甚至建议,在使用Xen虚拟化时也建议将时钟源切换为TSC。我对于这种方案可能产生的副作用知之甚少,但是即使是一些大公司甚至在生产环境也这么干。显然,这并不证明这种方案是安全的,但可以说它对某些情况有效。 6.总结1.System.currentTimeMillis返回的是从1970.1.1 UTC 零点开始到现在的时间,精确到毫秒;System.nanoTime提供相对精确的计时,但不能用于计算当前日期。 2.执行效率中,System.currentTimeMillis()与System.nanoTime()差不多,要求精度更高场景使用System.nanoTime()。 |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/25 18:35:00- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |