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

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

java内核

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

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

相关文章

XML——文档类型定义(DTD-Document Type Definition)

【0】README 0.1)本文文字描述转自 core java volume 2 , 旨在理解 XML——文档类型定义(DTD-Document Type Definition) 的基础知识; 0.2) for source code, please visit https://github.com/pacosonTa…

职称计算机考试 数量,职称计算机考试WPS基础考点:自动求和

职称计算机考试WPS基础考点:自动求和导语:随着信息技术的发展,计算机日益走进人们的工作、学习和生活,成为专业技术人员不可或缺的工具。下面我们一起来看看职称计算机考试WPS的内容吧。1. 对不相邻单元格的数据求和假如要将单元格…

java静态导入

一、静态导入 在Java 5中,import语句得到了增强,以便提供甚至更加强大的减少击键次数功能,虽然一些人争议说这是以可读性为代价的。这种新的特性成为静态导入。当你想使用static成员时,可以使用静态导入(在API中的类和…

java流写入数据库_成为Java流大师–第4部分:数据库流

java流写入数据库SQL一直是一种声明性语言,而Java长期以来势在必行。 Java流改变了游戏规则。 通过本动手文章编写您的方式,并学习如何使用Java流对RDBMS数据库执行声明性查询,而无需编写任何SQL代码。 您会发现,Java流和SQL命令的…

XML——XML Schema

【0】README 0.0)本文文字描述转自 core java volume 2 , 旨在理解 XML——XML Schema 的基础知识 ;(本文不涉及源代码, 很遗憾,以后会补充) 【1】XML Schema 相关 1.0)为什么引入…

怎么为pdf文件添加水印?

怎么为pdf文件添加水印?PDF是一种很好用的文件格式,这种格式能够很有效的保护我们的文件,但有时可能还会被破解,这种时候在PDF上添加水印就是比较好的方法。 综上所述,PDF是保密性很强的文件,但添加水印能够…

皖西学院计算机协会组织部,皖西学院

皖西学院关于卢义忠同志兼任安徽省高校后勤协会理事的请示安徽省委组织部:根据《关于进一步规范党政机关和企事业单位领导干部兼任社会组织职务审批管理的通知》(皖组通字〔2016〕40号)等文件精神,经皖西学院党委1月17日研究建议:因校领导分工调整&#…

java 用流创建流_成为Java流大师–第1部分:创建流

java 用流创建流在许多情况下,声明性代码(例如,具有Streams的功能组合)提供了出色的代码指标。 通过该动手实验文章系列进行编码,并成为Java Streams的主教练,从而成为一名更好的Java程序员。 Streams的整…

java的for循环

一、普通for循环(1)for循环语法 for(initialization; condition; update){statements;} (2)语法解释 1、for语句执行时,首先执行初始化操作(initialization)&#xff0c…

XML——使用 XPath来定位信息+使用命名空间

【0】README 0.1)本文文字描述转自 core java volume 2 , 旨在理解 XML——使用 XPath来定位信息使用命名空间 的基础知识 ; 0.2) for detailed XPath info : please visit ( http://www.ruanyifeng.com/blog/2009/…

计算机教室英语怎么读音,网络教室,network classroom,音标,读音,翻译,英文例句,英语词典...

多媒体教室1.多媒体教室基本组成多媒体教室由多媒体计算机、液晶投影机、数字视频展示台、中央控制系统、投影屏幕、音响设备等多种现代教学设备组成。(1)多媒体液晶投影机是整个多媒体演示教室中最重要的也是最昂贵的设备,它连接着计算机系统、所有视频输出系统及数…

算法一之简单选择排序

一、 选择排序的思想 选择排序的基本思想是:每一趟在n-i1(i1,2,…n-1)个记录中选取关键字最小的记录作为有序序列中第i个记录。基于此思想的算法主要有简单选择排序、树型选择排序和堆排序。简单选择排序的基本思想&a…

XML——流机制解析器

【0】README 0.1) 本文文字描述转自 core java volume 2 , 旨在理解 XML——流机制解析器 的基础知识 ; 0.2) for detailed StAX, please visit http://blog.csdn.net/pacosonswjtu/article/details/50569728 【1】XML——流机制…

第512章 河系量子计算机,第512章、河系量子计算机

69中文网 www.69zww.cc,最快更新分身投胎万界最新章节!但AI智能所容身的量子计算机就大得有些恐怖了。其总体积丝毫不亚于一个小型河系!什么是河系?银河系,仙女星系等等都是大小不等的河系!而AI智能容身的量…

idea内置junit5_JUnit 5和Selenium –使用Selenium内置的`PageFactory`实现页面对象模式

idea内置junit5Selenium是一组支持浏览器自动化的工具和库,主要用于Web应用程序测试。 Selenium的组件之一是Selenium WebDriver,它提供客户端库,JSON有线协议(与浏览器驱动程序进行通信的协议)和浏览器驱动程序。 Sel…

算法二之树形选择排序

一、树形选择排序的基本思想 (1) 树形选择排序又称锦标赛排序(Tournament Sort),是一种按照锦标赛的思想进行选择排序的方法。首先对n个记录的关键字进行两两比较,然后在n/2个较小者之间再进行两两比较&am…

计算机声卡型号价格,怎么查看电脑的声卡型号?查看声卡型号方法介绍

声卡是电脑上的硬件设施,任何电脑上的硬件都需要使用驱动使其运行,那么我们下载驱动的时候就会用到声卡型号,那么怎么查看电脑的声卡型号?下面小编就为大家详细介绍一下,一起来看看吧!使用命令查看1、首先呢&#xff…

spring 配置只读事务_只读副本和Spring Data第1部分:配置数据库

spring 配置只读事务这是有关我们为利用只读副本来提高应用程序性能而寻求的一系列博客文章。 对于这个项目,我们的目标是建立我们的spring数据应用程序,并使用read仓库进行写操作,并基于read副本进行读操作。 为了模拟这种环境&#xff0c…

XML—— XSL 转换

【0】README 0.1) 本文文字描述转自 core java volume 2 , 旨在理解 XML—— XSL 转换 的基础知识 ; 0.2)XSL 转换: XSL 转换(eXtensible StyleSheet Language扩展样式表语言) (for detailed…

算法三之堆排序

一、堆(Heap)定义 &#xff08;1&#xff09;n个关键字序列Kl&#xff0c;K2&#xff0c;…&#xff0c;Kn称为&#xff08;Heap&#xff09;&#xff0c;当且仅当该序列满足如下性质&#xff08;简称为堆性质&#xff09;&#xff1a; k(i)<k(2i&#xff09;且k(i)<k(2i…