统计代码执行时间时,System.currentTimeMillis()与System.nanoTime()哪个更适合?

目录

1.nanoTime是什么?

2.currentTimeMillis是什么?

3.nanoTime与currentTimeMillis在JDK中阐述

4.nanoTime与currentTimeMillis使用对比

5.深究从OpenJDK源代码、glibc,一直到Linux内核

6.总结

1.nanoTime是什么?

ns(nanosecond):纳秒, 时间单位。一秒的10亿分之一,即等于10的负9次方秒。常用作 内存读写速度的单位。
  1纳秒=0.000001 毫秒
  1纳秒=0.00000 0001秒

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 日午夜之间的时间差(以毫秒为单位测量)。

转换时间戳测试:

public class SystemCurrentTimeMillis2NanoTime {
    public static void main(String[] args) {
        SimpleDateFormat sf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println(sf.format(System.currentTimeMillis()));//输出:2021-07-30 11:45:55
        System.out.println(sf.format(System.nanoTime()));//输出:35232-07-23 22:41:54
        System.out.println(sf.format(new Date()));//输出:2021-07-30 11:45:55
        System.out.println(sf.format(0));//输出:1970-01-01 08:00:00

    }

}

4.nanoTime与currentTimeMillis使用对比

public class SystemCurrentTimeMillis2NanoTime {
   
public static void main(String[] args) {

        System.out.println(纳秒:”+System.nanoTime());
        System.
out.println(毫秒:”+System.currentTimeMillis());

        long time11 = System.nanoTime();
       
long time21 = System.currentTimeMillis();
        createTestList(
100000);//10
       
long time12 = System.nanoTime();
       
long time22 = System.currentTimeMillis();
        createTestList(
1000000);//100
       
long time13 = System.nanoTime();
       
long time23 = System.currentTimeMillis();
        System.
out.println(“10万List初始化纳米对应毫秒数”+((time12-time11)/1000000));
        System.
out.println(“10万List初始化毫秒数”+(time22-time21));
        System.
out.println(“100万List初始化纳米对应毫秒数”+((time13-time12)/1000000));
        System.
out.println(“100万List初始化毫秒数”+(time23-time22));
    }

    public static List<String> createTestList(int size) {
        List<String> list =
new ArrayList<>(size);
       
for (int i = 0; i < size; i++) {
            list.add(UUID.randomUUID().toString());
        }
       
return list;
    }
}

输出:

纳秒: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不会告诉我们具体实现。这段本地代码看起来稍微好一点:

  1. JVM_LEAF(jlong, JVM_CurrentTimeMillis(JNIEnv *env, jclass ignored))
  2.   JVMWrapper(“JVM_CurrentTimeMillis”);
  3.   return os::javaTimeMillis();
  4. JVM_END

可以看到,这里会根据不同的操作系统委派不同的实现。下面是Linux上的实现:

 jlong os::javaTimeMillis() {

  1.   timeval time;
  2.   int status = gettimeofday(&time, NULL);
  3.   assert(status != -1, “linux error”);
  4.   return jlong(time.tv_sec) * 1000 + jlong(time.tv_usec / 1000);
  5. }

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

  1. struct timeval {
  2.   time_t tv_sec; /* seconds */
  3.   suseconds_t tv_usec; /* microseconds */
  4. };

结构体中包含自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会交给操作系统实现: 

  1. JVM_LEAF(jlong, JVM_NanoTime(JNIEnv *env, jclass ignored))
  2.   JVMWrapper(“JVM_NanoTime”);
  3.   return os::javaTimeNanos();
  4. JVM_END

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

  1. jlong os::javaTimeNanos() {
  2.   if (os::supports_monotonic_clock()) {
  3.     struct timespec tp;
  4.     int status = os::Posix::clock_gettime(CLOCK_MONOTONIC, &tp);
  5.     assert(status == 0, “gettime error”);
  6.     jlong result = jlong(tp.tv_sec) * (1000 * 1000 * 1000) + jlong(tp.tv_nsec);
  7.     return result;
  8.   } else {
  9.     timeval time;
  10.     int status = gettimeofday(&time, NULL);
  11.     assert(status != -1, “linux error”);
  12.     jlong usecs = jlong(time.tv_sec) * (1000 * 1000) + jlong(time.tv_usec);
  13.     return 1000 * usecs;
  14.   }
  15. }

实现包含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基准测试:

 

  1. @BenchmarkMode(Mode.AverageTime)
  2. @OutputTimeUnit(TimeUnit.NANOSECONDS)
  3. public class Bench {
  4.  
  5.   @Benchmark
  6.   public long nano() {
  7.     return System.nanoTime();
  8.   }
  9.  
  10.   @Benchmark
  11.   public long millis() {
  12.     return System.currentTimeMillis();
  13.   }
  14. }

我的笔记本装的是Ubuntu 19.10,运行上面的基准测试结果如下:

《统计代码执行时间时,System.currentTimeMillis()与System.nanoTime()哪个更适合?》

System.currentTimeMillis()每次调用大约需要29纳秒,System.nanoTime()大约每次需要25纳秒。结果中规中矩。这也意味着,使用System.nano()测试短于几十纳秒的调用是不明智的。因为测量本身的开销可能大于所要测量的时间段。此外,还要避免在密集的循环中使用nanoTime(),因为这么做会使得延迟迅速增加。另一方面,使用nanoTime()计算远程服务器的响应时间或者开销很大的计算过程很合适。

 

基准测试IIAWS

虽然在笔记本上运行基准测试很方便,但并不实用。除非把笔记本贡献出来,作为应用的生产环境。让我们在AWS EC2中运行相同的基准测试。

启动一台安装了Ubuntu 16.04 LTS的c5.xlarge机器,并使用SDKMAN安装AdoptOpenJDK构建的Java 13:

 

  1. $ sudo apt install zip unzip
  2. $ curl -s “https://get.sdkman.io” | bash
  3. # 我直到使用Pipecurl结果输出到shell不酷。但是我很懒。
  4. $ source “/home/ubuntu/.sdkman/bin/sdkman-init.sh”
  5. $ sdk install java 13.0.1.hs-adpt

 

  1. $ java -version
  2. openjdk version “13.0.1” 2019-10-15
  3. OpenJDK Runtime Environment AdoptOpenJDK (build 13.0.1+9)
  4. OpenJDK 64-Bit Server VM AdoptOpenJDK (build 13.0.1+9, mixed mode, sharing)
  5. ubuntu@ip-10-0-0-133:~$

运行结果如下:

与笔记本上的运行结果几乎一样,还不错。现在,试一下c3.large实例。虽然比较老,但是现在仍然经常使用:

 《统计代码执行时间时,System.currentTimeMillis()与System.nanoTime()哪个更适合?》

结果看起来很糟糕!尽管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)时,涉及从用户模式切换到内核模式然后返回。这个转换是一个开销相对很高的操作,涉及许多步骤:

  1. 把CPU寄存器存储在内核堆栈中
  2. 运行内核代码调用函数
  3. 把运行结果从内核空间复制到用户空间
  4. 从内核堆栈恢复CPU寄存器
  5. 跳转到用户代码

 

这些操作的开销通常很大。而且伴随着旁道攻击和相关保障措施的出现,开销越来越大。

对性能敏感的应用程序通常会尽量避免从用户态切换到内核态。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实例上运行时的结果:

  1. $ sudo perf stat -e raw_syscalls:sys_enter -I 1000 -a
  2. # time counts unit events
  3. 1.000082884 138 raw_syscalls:sys_enter
  4. 2.000222087 252 raw_syscalls:sys_enter
  5. 3.000343414 127 raw_syscalls:sys_enter
  6. 4.000455977 136 raw_syscalls:sys_enter
  7. 5.000569565 140 raw_syscalls:sys_enter
  8. 6.000678534 136 raw_syscalls:sys_enter

可以看到每秒大约执行了130个系统调用。鉴于我们基准测试的每次循环都小于30纳秒,很明显应用程序应该使用vDSO避开了系统调用。

下面是在c3实例上的运行结果:

  1. $ sudo perf stat -e raw_syscalls:sys_enter -I 1000 -a
  2. time counts unit events
  3. 1.000159646 1390979 raw_syscalls:sys_enter
  4. 2.000942549 1391734 raw_syscalls:sys_enter
  5. 3.001555643 1392785 raw_syscalls:sys_enter
  6. 4.002148930 1393389 raw_syscalls:sys_enter
  7. 5.002734293 1392383 raw_syscalls:sys_enter
  8. 6.003311904 1338051 raw_syscalls:sys_enter

每秒执行超过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)的相关说明:

  1. notrace int __vdso_gettimeofday(struct timeval *tv, struct timezone *tz)
  2. {
  3.   if (likely(tv != NULL)) {
  4.     if (unlikely(do_realtime((struct timespec *)tv) == VCLOCK_NONE))
  5.       return vdso_fallback_gtod(tv, tz);
  6.     tv->tv_usec /= 1000;
  7.   }
  8.   if (unlikely(tz != NULL)) {
  9.     tz->tz_minuteswest = gtod->tz_minuteswest;
  10.     tz->tz_dsttime = gtod->tz_dsttime;
  11.   }
  12.  
  13.   return 0;
  14. }

这是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的结果:

  1. $ cat /sys/devices/system/clocksource/clocksource0/current_clocksource
  2. Xen

这是c5的结果:

  1. $ cat /sys/devices/system/clocksource/clocksource0/current_clocksource
  2. kvm-clock

事实证明,kvm-clock实现中会设置vclock_mode为VCLOCK_PVCLOCK,这样不会fallback到较慢的分支。Xen时钟源没有设置mode,而是保持了VCLOCK_NONE默认值。这样会跳转到vdso_fallback_gtod()函数,最后执行真正的系统调用!

 

  1. #define VCLOCK_NONE 0/* No vDSO clock available.*/
  2. #define VCLOCK_TSC 1/* vDSO should use vread_tsc.*/
  3. #define VCLOCK_HPET 2/* vDSO should use vread_hpet.*/
  4. #define VCLOCK_PVCLOCK 3/* vDSO should use vread_pvclock.*/
  5.  
  6. struct arch_clocksource_data {
  7.   int vclock_mode;
  8. };

Linux的优点在于它具有高度的可配置性,并且经常会让我们自取灭亡。可以尝试更改c3上的时钟源并重新运行基准测试。可以执行下面命令查看:

  1. $ cat /sys/devices/system/clocksource/clocksource0/available_clocksource
  2. xen tsc hpet acpi_pm

TSC是Time Stamp Counter的缩写,是一种非常快速的时间源。对于我们而言最重要的是知道它是一种很好的vDSO实现。把c3实例中的时钟源从Xen切换到TSC:

# echo tsc > /sys/devices/system/clocksource/clocksource0/current_clocksource

检查是否完成切换:

  1. $ cat /sys/devices/system/clocksource/clocksource0/current_clocksource
  2. tsc

 

看起来不错!现在重新运行基准测试:

结果看起来不错!实际上,比使用kvm-clock的c5实例表现更好。每秒系统调用数与c5实例处于同一数量级:

 

  1. ubuntu@ip-1000133:~$ sudo perf stat -e raw_syscalls:sys_enter -I 1000 -a
  2. # time counts unit events
  3. 1.000172233 144 raw_syscalls:sys_enter
  4. 2.000726439 203 raw_syscalls:sys_enter
  5. 3.001163704 180 raw_syscalls:sys_enter
  6. 4.001595597 177 raw_syscalls:sys_enter
  7. 5.002030641 192 raw_syscalls:sys_enter

有些人甚至建议,在使用Xen虚拟化时也建议将时钟源切换为TSC。我对于这种方案可能产生的副作用知之甚少,但是即使是一些大公司甚至在生产环境也这么干。显然,这并不证明这种方案是安全的,但可以说它对某些情况有效。

6.总结

1.System.currentTimeMillis返回的是从1970.1.1 UTC 零点开始到现在的时间,精确到毫秒;System.nanoTime提供相对精确的计时,但不能用于计算当前日期。

2.执行效率中,System.currentTimeMillis()与System.nanoTime()差不多,要求精度更高场景使用System.nanoTime()。

    原文作者:秋9
    原文地址: https://blog.csdn.net/jlq_diligence/article/details/119246689
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞