浏览器内核_测量时间:从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 { @Benchmarkpublic long nano() {return System.nanoTime();}@Benchmarkpublic 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_mode
到VCLOCK_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到内核再到相关推荐
- java内核_测量时间:从Java到内核再到
java内核 问题陈述 当您深入研究时,即使是最基本的问题也会变得很有趣. 今天,我想深入研究一下Java时间. 我们将从Java API的最基础知识开始,然后逐步降低堆栈:通过OpenJDK源代码g ...
- linux内核中测量时间的方法,Linux内核中获取时间函数do_gettimeofday
内核代码能一直获取一个当前时间的表示, 通过查看 jifies 的值. 常常地, 这个值只代表从最后一次启动以来的时间, 这个事实对驱动来说无关, 因为它的生命周期受限于系统的 uptime. 如所示 ...
- 编译内核_将驱动编译进内核(Kernel)的步骤记录
1.首先在/kernel/drivers下建立驱动文件:以建立hello文件为例 2.在hello文件下创建.c/Makefile/Kconfig三个文件 3..c文件存放驱动程序:Makefile存 ...
- java 浏览器 组件_最好的Java / Swing浏览器组件?
什么是最好的跨平台Java Swing浏览器组件,至少能够在Swing界面中很好地播放(轻型组件?),并且能够在MacOSX和Windows上运行? 诸如:FlyingSaucer,JDIC或其他? ...
- python绑定内核_向Ipython添加python2内核
我也遇到了同样的问题,发现在python2.7系统站点目录和用户站点目录中都安装了backports包.此外,backports.shutil_get_terminal_size包仅位于系统站点目录中 ...
- java浏览器内核_深入理解浏览器内核 - 概述
欢迎点击「算法与编程之美」↑关注我们! 本文首发于微信公众号:"算法与编程之美",欢迎关注,及时了解更多此系列文章. 继"汉芯"事件造假之后,近期"红 ...
- linux uname内核,Linux下confstr与uname函数_获取C库与内核信息
Linux下confstr与uname函数_获取C库与内核信息 #include #include //uname int main(int argc, char **argv[]) { struc ...
- java线程和内核线程的,Java中内核线程理论及实例详解
1.概念 内核线程是直接由操作系统内核控制的,内核通过调度器来完成内核线程的调度并负责将其映射到处理器上执行.内核态下的线程执行速度理论上是最高的,但是用户不会直接操作内核线程,而是通过内核线程的接口 ...
- 鸿蒙系统浏览器内核,四大浏览器横评出炉:Chromium 内核版 Edge 四项夺冠,优于原生 Chrome...
四大浏览器横评出炉:Chromium 内核版 Edge 四项夺冠,优于原生 Chrome 2020-01-16 14:04:30 51点赞 382收藏 243评论 如此前所预告的一样,微软于美国时间 ...
最新文章
- 安卓中AIDL的使用方法快速入门
- nyoj905 卡片游戏
- SAP CDS view redirect(重定向)的调试
- 同批号不同批次同一单据中出现数量不限制
- Mysql远程登录及常用命令
- LeetCode 460. LFU缓存(哈希双链表)
- 性能测试过程中oracle数据库报ORA-27301 ORA-27302错
- 基于Python+tkinter+pygame的音乐播放器完整源码
- ERROR: Could not find a version that satisfies the requirement absl (from versions: none) ERROR: No
- 57个深度学习专业术语
- P1215 [USACO1.4]母亲的牛奶 Mother's Milk
- #脚本实现宠物动作行为_短视频剧情创作方法有哪些?爆款短视频的标配,只需88个脚本模板...
- 快速上手数据挖掘之Solr5搜索引擎高级教程
- 十四五规划下建筑企业智慧建造数字化转型规划战略
- meanshift算法图解
- 麻省理工十亿美元计算机学院,麻省理工学院10亿美元打造全新计算机科学学院,让所有学科的研究人员都能接触到最新的计算科学...
- 团队项目(3) -- 搭载于MSP430F6638_FFTB的仿《像素小鸟》小游戏
- java 多线程(四)—— 线程同步/互斥=队列+锁
- 魅族Android10内测招募答案,魅族flyme9内测招募答案,魅族16系列flyme9内测招募题目答案免费分享预约 v1.0-手游汇...
- C++后端开发的一些工具