测量时间:从Java到内核再到

问题陈述

当您深入研究时,即使是最基本的问题也会变得很有趣。 今天,我想深入研究一下Java时间。 我们将从Java API的最基础知识开始,然后逐步降低堆栈:通过OpenJDK源代码glibc一直到Linux内核。 我们将研究各种环境下的性能开销,并尝试对结果进行推理。

我们将探索经过时间的度量:从某个活动的开始事件到结束事件所经过的时间。 这对于性能改进,操作监控和超时执行很有用。

以下伪代码是我们几乎可以在任何代码库中看到的常见用法:

 START_TIME = getCurrentTime()  executeAction()  ELAPSED_TIME = getCurrentTime() - START_TIME 

有时它不太明确。 我们可以使用面向方面的编程原则来避免本质上与操作有关的污染我们的业务代码,但是它仍然以一种或另一种形式存在。

Java中经过的时间

Java提供了两个用于测量时间的基本原语: System.currentTimeMillis()System.nanoTime() 。 这两个调用之间有几个区别,让我们对其进行分解。

1.起点的稳定性

System.currentTimeMillis()返回自Unix纪元开始(1970年1月1日UTC)以来的毫秒数。 另一方面, System.nanoTime()返回自过去某个任意点以来的纳秒数。

这立即告诉我们currentTimeMillis()的最佳粒度为1毫秒。 它使得不可能测量任何短于1ms的东西。 currentTimeMillis()使用1970年1月1日UTC作为参考点的事实是好事。

为什么好呢? 我们可以比较两个不同的JVM甚至两个不同的计算机返回的currentTimeMillis()值。
为什么不好? 当我们的计算机没有同步时间时,比较将不会很有用。 典型服务器场中的时钟未完全同步,并且始终会有一些差距。 如果我要比较两个不同系统的日志文件,这仍然可以接受:如果时间戳记不能完全同步,则可以。 但是,有时这种差距可能导致灾难性的结果,例如,当将其用于分布式系统中的冲突解决时。

2.时钟单调性

另一个问题是,不能保证返回值会单调增加。 这是什么意思? 当您连续两次调用currentTimeMillis() ,第二个调用返回的值可能小于第一个。 这是违反直觉的,并且可能导致无意义的结果,例如经过时间为负数。 显然, currentTimeMillis()不是衡量应用程序内部经过时间的好选择。 那nanoTime()呢?

System.nanoTime()不使用Unix纪元作为参考点,而是过去的一些未指定点。 在执行单个JVM的过程中,问题仍然存在,仅此而已。 因此,甚至比较在同一台计算机上运行的两个不同JVM返回的nanoTime()值也没有意义,更不用说在单独的计算机上了。 参考点通常与上一次计算机启动有关,但这纯粹是实现细节,我们根本不能依赖它。 这样做的好处是,即使计算机中的挂钟时间由于某种原因而倒退,也不会对nanoTime()产生任何影响。 这就是为什么nanoTime()是一个不错的工具,可以测量单个JVM上两个事件之间的经过时间,但是我们无法比较两个不同JVM上的时间戳。

Java实现

让我们探讨一下Java中如何实现currentTimeMillis()nanoTime() 。 我将使用来自OpenJDK 14当前负责人的资源 。 System.currentTimeMillis()是一种本地方法,因此我们的Java IDE不会告诉我们它是如何实现的。 这个本地代码看起来更好一些:

 JVM_LEAF(jlong, JVM_CurrentTimeMillis(JNIEnv *env, jclass ignored)) JVMWrapper( "JVM_CurrentTimeMillis" ); return os::javaTimeMillis();  JVM_END 

我们可以看到,这只是委派,因为实现因操作系统而异。 这是Linux的实现 :

 jlong os::javaTimeMillis() { timeval time; int status = gettimeofday(&time, NULL); assert (status != - 1 , "linux error" ); return jlong(time.tv_sec) * 1000 + jlong(time.tv_usec / 1000 );  } 

该代码委托给Posix函数gettimeofday() 。 此函数返回一个简单的结构:

 struct timeval { time_t tv_sec; /* seconds */ suseconds_t tv_usec; /* microseconds */  }; 

该结构包含自该纪元以来的秒数和给定秒数内的微秒数。 currentTimeMillis()的约定将返回自该纪元以来的毫秒数,因此它必须进行简单的转换: jlong(time.tv_sec) * 1000 + jlong(time.tv_usec / 1000)

函数gettimeofday()由glibc实现,它最终会调用Linux内核。 稍后我们将更深入地了解。

让我们看看nanoTime()的实现方式:事实并没有太大不同System.nanoTime()也是一种本地方法: public static native long nanoTime();jvm.cpp委托给特定于操作系统的实现:

 JVM_LEAF(jlong, JVM_NanoTime(JNIEnv *env, jclass ignored)) JVMWrapper( "JVM_NanoTime" ); return os::javaTimeNanos();  JVM_END 

javaTimeNanos()的Linux实现非常有趣:

 jlong os::javaTimeNanos() { if (os::supports_monotonic_clock()) { struct timespec tp; int status = os::Posix::clock_gettime(CLOCK_MONOTONIC, &tp); assert (status == 0 , "gettime error" ); jlong result = jlong(tp.tv_sec) * ( 1000 * 1000 * 1000 ) + jlong(tp.tv_nsec); return result; } else { timeval time; int status = gettimeofday(&time, NULL); assert (status != - 1 , "linux error" ); jlong usecs = jlong(time.tv_sec) * ( 1000 * 1000 ) + jlong(time.tv_usec); return 1000 * usecs; }  } 

有两个分支:如果操作系统支持单调时钟,它将使用它,否则它将委托给我们的老朋友gettimeofday()Gettimeofday()与Posix调用的System.currentTimeMillis()相同! 显然,随着nanoTime()粒度更高,转换看起来有些不同,但这是相同的Posix调用! 这意味着在某些情况下, System.nanoTime()使用Unix纪元作为参考,因此它可以回到过去! 换句话说:它不能保证是单调的!

好消息是,据我所知,所有现代Linux发行版都支持单调时钟。 我认为该分支是为了与早期版本的kernel / glibc兼容。 如果您对HotSpot如何检测操作系统是否支持单调时钟的详细信息感兴趣,请参见此代码 。 对我们大多数人来说,重要的是要知道OpenJDK实际上总是调用Posix函数clock_gettime() ,该函数在glibc和Linux内核的glibc委托中实现。

基准I –本地笔记本电脑

至此,我们对如何实现nanoTime()currentTimeMillis()有了一些直觉。 让我们看看他们是快闪还是慢速。 这是一个简单的JMH基准:

 @BenchmarkMode (Mode.AverageTime)  @OutputTimeUnit (TimeUnit.NANOSECONDS)  public class Bench { @Benchmark public long nano() { return System.nanoTime(); } @Benchmark public long millis() { return System.currentTimeMillis(); }  } 

当我在装有Ubuntu 19.10的笔记本电脑上运行此基准测试时,得到以下结果:

基准测试 模式 碳纳米管 得分了 错误 单位
板凳 平均 25 29.625 ±2.172 ns / op
Benchnano 平均 25 25.368 ±0.643 ns / op

每个调用System.currentTimeMillis()大约需要29纳秒,而System.nanoTime()大约需要25纳秒。 不好,不可怕。 这意味着使用System.nano()测量花费少于几十纳秒的任何东西可能是不明智的,因为我们仪器的开销会高于所测量的间隔。 我们还应该避免在紧密的循环中使用nanoTime() ,因为延迟会Swift增加。 另一方面,使用nanoTime()来衡量例如来自远程服务器的响应时间或昂贵的计算时间似乎是明智的。

基准II – AWS

在便携式计算机上运行基准测试很方便,但不是很实用,除非您愿意放弃便携式计算机并将其用作应用程序的生产环境。 相反,让我们在AWS EC2中运行相同的基准测试。

让我们使用Ubuntu 16.04 LTS启动一台c5.xlarge机器,并使用出色的SDKMAN工具安装由AdoptOpenJDK项目上的精湛人员构建的Java 13:

板凳
板凳

结果如下:

基准测试 模式 碳纳米管 得分了 错误 单位
板凳 平均 25 28.467 ±0.034 ns / op
Benchnano 平均 25 27.331 ±0.003 ns / op

这几乎与笔记本电脑上的一样,还不错。 现在让我们尝试c3.large实例。 它是较老的一代,但仍经常使用:

基准测试 模式 碳纳米管 得分了 错误 单位
板凳 平均 25 362.491 ±0.072 ns / op
Benchnano 平均 25 367.348 ±6.100 ns / op

这看起来一点都不好! c3.large是一个较旧的较小实例,因此预计会有所降低,但这太多了! currentTimeMillis()nanoTime()都慢一个数量级。 起初360 ns听起来可能还不错,但是请考虑一下:要仅测量一次经过时间,您需要两次调用。 因此,每次测量花费大约0.7μs。 如果您有10个探针测量不同的执行阶段,则您的时间为7μs。 透视一下:40gbit网卡的往返行程约为10μs。 这意味着向我们的热路径添加一堆探针可能会对延迟产生非常大的影响!

一点内核调查

为什么C3实例比笔记本电脑或C5实例慢得多? 事实证明,这与Linux时钟源有关,更重要的是与glibc-kernel接口有关。 我们已经知道,每次调用nanoTime()currentTimeMillis()调用OpenJDK中的本地代码,该本地代码调用glibc,后者又调用Linux内核。

有趣的部分是glibc-Linux内核转换:通常,当进程调用Linux内核函数(也称为syscall)时,它涉及从用户模式切换到内核模式,然后再返回。 此转换是一个相对昂贵的操作,涉及许多步骤:

  • 将CPU寄存器存储在内核堆栈中
  • 使用实际功能运行内核代码
  • 将结果从内核空间复制到用户空间
  • 从内核堆栈恢复CPU寄存器
  • 跳回用户代码

这从来都不是便宜的操作,并且随着边信道安全攻击和相关缓解技术的出现,它变得越来越昂贵。

对性能敏感的应用程序通常会尽力避免用户到内核的转换。 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 ns,因此很明显,该应用程序使用vDSO绕过了系统调用。

这是在c3实例上的外观:

板凳

每秒超过1,300,000个系统调用! 同样, nanoTime()currentTimeMillis()的延迟也大约翻了一番,达到700ns /操作。 这是一个相当有力的指示,每个基准测试迭代都会调用一个真实的系统调用!

让我们使用另一个perf命令来收集其他证据。 此命令将计算5秒钟内调用的所有系统调用并按名称分组:
sudo perf stat -e 'syscalls:sys_enter_*' -a sleep 5
在c5实例上运行时,没有任何异常情况。 但是,在c3实例上运行时,我们可以看到以下内容:

板凳

这是我们的吸烟枪! 非常有力的证据表明,当基准测试在c3框上运行时,它将进行真正的gettimeofday()系统调用! 但为什么?

这是 4.4内核(在Ubuntu 16.04中使用) 的相关部分 :

板凳

当Java调用System.currentTimeMillis()时,它是映射到用户内存并由glibc调用的函数。 它调用do_realtime() ,该struct tv使用当前时间填充struct tv ,然后返回给调用者。 重要的是所有这些操作都在用户模式下执行,而没有任何缓慢的系统调用。 好吧,除非do_realtime()返回VCLOCK_NONE 。 在这种情况下,它将调用vdso_fallback_gtod() ,这将执行缓慢的系统调用。

为什么c3实例进行回退做系统调用而c5不做? 好吧,这与虚拟化技术的变化有关! 自成立以来,AWS一直在使用Xen虚拟化 。 大约2年前, 他们宣布从Xen过渡到KVM虚拟化 。 C3实例使用Xen虚拟化,较新的c5实例使用KVM。 对我们而言重要的是,每种技术都使用Linux Clock的不同实现。 Linux在/sys/devices/system/clocksource/clocksource0/current_clocksource显示当前时钟源。

这是c3:

板凳

这是c5:

板凳

原来,KVM-时钟实现套vclock_modeVCLOCK_PVCLOCK这意味着慢回退分支以上不采取。 Xen时钟源根本没有设置此模式 ,而是停留在VCLOCK_NONE 。 这将导致跳入vdso_fallback_gtod()函数,该函数最终将启动实际的系统调用!

板凳

关于Linux的好处是它具有高度的可配置性,并且经常给我们足够的绳索来吊死自己。 我们可以尝试更改c3上的时钟源并重新运行基准测试。 可通过$ cat /sys/devices/system/clocksource/clocksource0/available_clocksource
xen tsc hpet acpi_pm
$ cat /sys/devices/system/clocksource/clocksource0/available_clocksource
xen tsc hpet acpi_pm

TSC代表时间戳记计数器 ,它是一种非常快速的来源,并且对我们而言重要的是适当的vDSO实施。 让我们将c3实例中的时钟源从Xen切换到TSC:

板凳

检查它是否真的被切换:

板凳

看起来不错! 现在,我们可以重新运行基准测试:

基准测试 模式 碳纳米管 得分了 错误 单位
板凳 平均 25 25.558 ±0.070 ns / op
Benchnano 平均 25 24.101 ±0.037 ns / op

数字看起来不错! 实际上比具有kvm-clock的c5实例更好。 每秒系统调用数与c5实例处于同一级别:

板凳

有些人甚至在使用Xen虚拟化时也建议将时钟源切换为TSC。 我对它可能产生的副作用知之甚少,但是显然,即使是一些大公司也在生产中做到了这一点。 显然,这并不证明它是安全的,但这表明它对某些人有效。

最后的话

我们已经看到了底层实现细节如何对普通Java调用的性能产生重大影响。 这不仅仅是在微基准测试中可见的理论问题, 实际系统也会受到影响 。 您可以直接在Linux内核源代码树中阅读有关vDSO的更多信息。

没有我在Hazelcast的出色同事,我将无法进行调查。 这是一支世界一流的团队,我从他们那里学到了很多东西! 我要感谢布伦丹·格雷格(Brendan Gregg)收集的各种技巧 ,我的记忆力一直很差,布伦丹创造了一个出色的备忘单。

最后但并非最不重要的一点:如果您对性能,运行时或分布式系统感兴趣,请关注我 !

翻译自: https://www.javacodegeeks.com/2019/12/measuring-time-from-java-to-kernel-and-back.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/340352.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

小程序 const moment = require('moment')_C++大作业-XXX管理程序

理工科大一往往会学习C/C,期末会有大作业。这篇文章就是一个简单的C大作业程序。我也是大一,所以觉着哪里写得不好欢迎在评论区提出。程序总体上讲是个“总分总”结构。一 实现这个程序要八步。第一步 程序的功能设计程序的目标有两个。一是辅助我备考四…

c语言osversioninfoex,xi52qian

头文件 一. 对终端的操作相关头文件#include 1. 输入istream2. 输出ostream3. iostream继承istream和ostream 所以它具有输入输出功能。为了方便这个库定义了下列三个标准流对象:1. cin 代表标准输入istream类对象一般地cin使我们能够从用户终端读入数据。2. cout …

go hive skynet_云风的skynet在国内外来看究竟算什么水平?可以一统国内游戏服务端框架吗?...

它和云风过往放出来的东西一样,是非常具有实践性的,可以解决实际开发问题的。目前我所在的手游项目使用 Erlang 进行服务器端开发的,如果重新开始,我会选择使用 skynet。游戏服务器开发中的难点,上面 无瞳已经提到了两…

数据库 测试数据生成_测试数据生成器和对象母亲:另一种外观

数据库 测试数据生成在测试中构造对象通常是一项艰巨的工作,通常会产生大量可重复且难以阅读的代码。 有两种用于处理复杂测试数据的常见解决方案: Object Mother和Test Data Builder 。 两者都有优点和缺点,但是(巧妙地&#xff…

电脑机器人_视频|电话积分换平板电脑和扫地机器人?女子拿回家后……-

报警人小王(左二)讲述事情经过。沙坪坝警方供图 华龙网-新重庆客户端 发华龙网-新重庆客户端11月9日11时讯(记者 张勇)“警察叔叔,这个店好坑人哦,我好气愤!”11月6日11时许,重庆市沙坪坝区一名年轻女子拨打110报警电话称&#xf…

位置环PID模糊C语言,PID和位置环

EDA365欢迎您登录!您需要 登录 才可以下载或查看,没有帐号?注册x所谓PID 自动控制,是对一个确定系统的- -个过程量的自动调节过程:* q* }3 B" * V P# H1)举例说,直流电机的速度,就是-一个过程量&#…

seata xid是什么_阿里开源的分布式事务框架 Seata

1. Seata 概述Seata 是 Simple Extensible Autonomous Transaction Architecture 的简写,由 feascar 改名而来。Seata 是阿里开源的分布式事务框架,属于二阶段提交模式。目前github上已经有 12267 颗星了,也很活跃,最新的提交时间…

python输入print跳到documentation-习题 48: 更复杂的用户输入

习题 48: 更复杂的用户输入 你的游戏可能一路跑得很爽,不过你处理用户输入的方式肯定让你不胜其烦了。每一个房间都需要一套自己的语句,而且只有用户完全输入正确后才能执行。你需要一个设备,它可以允许用户以各种方式输入语汇。例如下面的机…

有关有效企业测试的视频课程

我已经制作了一些有关有效企业测试的视频。 我仍然在现实世界项目中看到这个主题的巨大重要性。 这是我在测试Enterprise Java项目中的经验以及一些示例。 1.介绍和有效的Maven使用 在此视频中,我将介绍测试过程,并演示如何使用Maven在标准企业项目中构…

android自定义弹出对话框,使用FlyDialog实现自定义Android弹窗对话框

前言学习的时候要用到弹窗,但是又觉得i同自带的弹窗样式有点不太美观,搜索资料后发现了FlycoDialog这个开源库,效果很好,而且实现起来也比较方便。先列举一些比较好看的效果:NormalListDialogActionSheetDialog这篇文章主要来讲一下他的自定义…

nacos 本地测试_Nacos集群配置实例(windows下测试)

1、首先 fork 一份 nacos 的代码到自己的 github 库,然后把代码 clone 到本地。git地址:https://github.com/alibaba/nacos.git2、然后将你的项目导入到ideal编辑器中(找到子项目distribution)3、添加集群节点:找到文件distribution->conf…

Android手机如何修改Mac地址,安卓手机怎么修改mac地址

有些时候我们如果绑定了某个mac,那么其他用mac就无法上网,应该怎么修改呢?学习啦小编从网上搜集整理了3种修改安卓手机mac 地址的方法。修改安卓手机mac 地址的方法修改安卓手机mac 地址的方法第一种:软件法下面介绍一款软件 叫物理地址修改…

应用回归分析何晓群_二战上岸人大20年应用统计高分经验帖

首先介绍一下本人的基本情况。男,本科金融学,同时修过数学双学位。一战人大经济学硕,总分 370(专业课没过线)。二战决定考人大应统专硕(除了专业课换了,公共课都一样)。二战总分430&…

基于单片机步进电机ppt答辩_基于单片机的步进电机式汽车仪表的设计(含电路原理图,程序)...

基于单片机的步进电机式汽车仪表的设计(含电路原理图,程序)(课题申报表,任务书,开题报告,中期检查表,外文翻译,论文21000字,程序,答辩PPT)摘 要汽车仪表是驾驶者和汽车的交互界面,为驾驶员提供所需要的运行参数、故障、里程等实时信息,是不可或缺的部分。…

android webview js 交互框架,自定义android混合框架开发实践1:实现基础andorid和webview交互...

1. 本地web资源1.构建assets/web文件夹2.创建index.html你的html代码3.使用本地web资源WebView mv findViewById(...);mv.loadUrl("file:///android_asset/web/index.html")2.实现基础的android和js交互(1). 实现js调用andorid方法在Acitivity内构建一个functionJav…

jxls使用excel公司_使用jXLS将Excel文件解析为JavaBeans

jxls使用excel公司这篇文章展示了如何使用jXLS将Excel文件解析为JavaBeans列表。 这是我编写的通用实用程序方法&#xff1a; /** * Parses an excel file into a list of beans. * * param <T> the type of the bean * param xlsFile the excel data file to parse * …

网站部署后无法访问sqlserver_.NET Core跨平台部署

1. Windows-IIS大家对于在IIS上部署.NET站点已经驾轻就熟了&#xff0c;部署.NET Core也没有什么本质区别&#xff0c;但是这其中仍然有一些细节是不同的&#xff0c;下面记录了一些我在部署时遇到的问题1.1 安装.NET Core Windows Server Hosting要在IIS上运行http://ASP.NET …

ajax如何传超长字符串_解决ajax超长字符串、中文乱码问题

在最近的项目测试中发现通过ajax发送超长参数时遇到这个脚本错误&#xff1a;系统找不到指定资源the system cannot locate the resource specified意思是参数太长&#xff0c;无法发送&#xff0c;测试的浏览器是IE6//IE7//IE8&#xff0c;由于利用了ActiveX所以其他浏览器的情…

Android代码数字证书,有关Android中读取证书

最近在项目中遇到了读取证书中内容与读取keystore中对应公钥的需求&#xff0c;在此做一下笔记读取证书最近项目中遇到后台返回个byte[]数组类型的证书&#xff0c;需要从证书中获取相关内容&#xff0c;先看一下相关代码BufferedInputStream mStream null;try {String s new…

Spring Boot中的高级配置文件管理

我们都知道Spring Boot中的配置文件管理及其为不同环境配置应用程序时提供的灵活性。 此功能的另一个强大方面是&#xff0c;在任何给定时间&#xff0c;我们都可以拥有多个活动配置文件。 这样做的好处是我们可以将部署环境配置文件与业务用例相关的配置文件混合在一起。 让我…