为什么 Java 可以在不花费时间的情况下运行代码?

Kid*_*nbo 3 c++ java

我编写了一个小程序来生成唯一 ID。并打印出时间成本。这是代码:


public class JavaSF_OLD {

    static int randomNumberShiftBits = 12;
    static int randomNumberMask = (1 << randomNumberShiftBits) - 1;
    static int machineNumberShiftBits = 5;
    static int machineNumberMask = (1 << machineNumberShiftBits) - 1;
    static int dataCenterNumberShiftBits = 5;
    static int dataCenterNumberMask = (1 << dataCenterNumberShiftBits) - 1;
    static int dateTimeShiftBits = 41;
    static long dateTimeMask = (1L << dateTimeShiftBits)-1;

    static int snowFlakeId = 0;
    static long lastTimeStamp = 0;
    static int DataCenterID = 1;
    static int MachineID = 1;

    public static long get() {
//        var current = System.currentTimeMillis();
        var current = 164635438;
        if (current != lastTimeStamp) {
            snowFlakeId = 0;
            lastTimeStamp=current;
        }else{
            snowFlakeId++;
        }

        long id = 0;

        id |= current&dateTimeMask;

        id <<= dataCenterNumberShiftBits;
        id |= DataCenterID&dataCenterNumberMask;

        id <<= machineNumberShiftBits;
        id |= MachineID&machineNumberMask;

        id <<= randomNumberShiftBits;
        id |= snowFlakeId & randomNumberMask;

        return id;
    }

    public static void main(String[] args) {
        long result  = 0;
        for (int out = 0; out < 10; out++) {
            var start = System.currentTimeMillis();
            for (int i = 0; i < 1000000000; i++) {
                result = get();
            }
            var end = System.currentTimeMillis();
            System.out.println(end - start);
            System.out.println(result);
        }
    }
}


Run Code Online (Sandbox Code Playgroud)

结果似乎有点奇怪。

53
690531076282879
5
690531076281343
0
690531076283903
0
690531076282367
0
690531076280831
0
690531076283391
0
690531076281855
0
690531076284415
0
690531076282879
0
690531076281343

Run Code Online (Sandbox Code Playgroud)

它使用 0 百万秒来获得正确的结果,而 C++ 版本需要 2.3 亿秒才能获得一个结果。当我将内循环的编号从 1000000000 更改为 1e9(双精度类型)时,获得每个结果需要一秒钟以上的时间。这怎么可能?

我更改了 C++ 版本的循环次数,根本没有任何变化。所以我猜 Java 优化了循环并省略了前 999999999 个循环。以及 Java 如何实际优化它并免费运行它但获得正确的结果?以及如何优化相同代码的 C++ 版本以跳过无用循环?我使用 -O3 标志但它似乎不起作用。

53
690531076282879
5
690531076281343
0
690531076283903
0
690531076282367
0
690531076280831
0
690531076283391
0
690531076281855
0
690531076284415
0
690531076282879
0
690531076281343

Run Code Online (Sandbox Code Playgroud)

这是 C++ 版本及其结果:

1419
690531076282879
1385
690531076281343
1388
690531076283903
1457
690531076282367
1407
690531076280831
1402
690531076283391
1441
690531076281855
1389
690531076284415
1395
690531076282879
1360
690531076281343

Run Code Online (Sandbox Code Playgroud)

至于测量时间,就是main函数里面的代码。我知道算法是错误的,我只是好奇为什么 Java 可以这样做以及如何让 C++ 跳过循环。

rzw*_*oot 5

你在大量的误解下运作。

System.currentTimeMillis() 作为 ID 是个坏主意

这是一个非常糟糕的主意。您的代码显然旨在将 System.currentTimeMillis ( cTM) 视为严格递增的序列。例如,如果当前时间是 10000,并且我要求一个 id,我得到10000:0. 如果我再问,我会得到10000:1。如果时间变为 10001,我会得到10001:0,如果时间又回到 10000,我会10000:0再次得到,这违反了生成唯一数字的意图。

但事情是这样的:cTM绝对不能保证它是严格增加的。

cTM 反映系统时钟。在一些落后的系统上,系统时钟代表本地时间而不是 UTC。Java 应该“修复”这个问题,但已知在夏令时调整期间会发生时间扭曲 3600000 毫秒(一小时)。更一般地说,大多数计算机从某个网络源获取时间,并且始终将时间调整几秒(很容易达到数千毫秒)。如果您必须拥有唯一的 ID 并且系统时间是唯一可能的提供者,那么有一些解决方案,但整个博士研究论文都写了如何做到这一点(这称为“涂抹”,您的计算机可能没有这样做,而 JVM 只是报告操作系统告诉它的内容,因此它也不会涂抹)。

System.nanoTime() 或多或少肯定会增加,但它每 36 天左右循环一次。如果您需要唯一 ID,请使用正确的工具:UUID。生成唯一 ID 比您想象的要困难,而且是一个已解决的问题。使用现有的解决方案。

cTM 的计时性能是个坏主意

那也是错误的。Java 的工作原理大致如下:

非常缓慢和愚蠢地运行所有代码,并跟踪各种完全不相关的信息,例如“对于这个 if 分支,表达式解析为 'true' 与 'false' 的频率是多少”,或者“这多久是这样”方法调用'。收集这些统计数据使其效率更低。JVM 现在非常低效。但这很好,你马上就会看到。与 C 代码相比,gcc您使用的任何编译器都会分析您的源代码并制作出最优化的机器代码,但这就是它结束的地方:没有簿记。它是从编译停止开始优化的代码。对比 爪哇; javac非常简单而且非常愚蠢,它几乎没有优化。java在运行时,它本身就是这样。

然后,时不时地做一个分析:系统中的所有方法中,哪个方法占用的 CPU 时间最多?然后,花点时间和所有那些看似无用的统计数据来生成一个惊人的微调优化机器编码版本的这种方法。它可以而且经常会胜过手写代码;毕竟,对于这种实际工作负载,java 具有实时了解行为的好处,而诸如 C 编写的代码之类的东西无法知道这一点。Java 甚至可以生成带有内置假设的代码,因为如果其中一个假设稍后失败,Java 可以“无效”这个优化的变体。

结果是,再一次,过度简化了很多,任何给定方法的一般性能特征是每次运行需要 X 时间一段时间(X 是,比如说,1000),然后一个调用需要更长的时间作为系统进行分析(例如 10000000),然后所有进一步的调用都需要 Y 时间,其中 Y 远小于 X(例如 10)。

1000 处的循环数以及重新编译时的那个 blip 是“常数”,然后 10 的实际时间用于所有进一步的循环。随着越来越多的循环被应用(并且因为我们只优化经常调用的方法,10 个循环使其他循环相形见绌),10 是性能目的唯一重要的数字。

但这确实意味着您需要等到那件事发生后才能衡量性能,这根本不容易。您还会收到其他“噪音”。也许你的线程被 winamp 抢占了,因为它需要解压更多的 MP3 文件,导致你的时间任意地出现巨大的昙花一现。

答案是JMH。还有一个问题:手头的工作(对方法调用进行计时)比您想象的要复杂几个数量级,但这是一个已解决的问题,因此请使用现有的解决方案。

关于你观察到的表现的一些猜测

如果将其设为双精度,则必须在双精度上加 1,这可能会慢几个数量级,双比较也是如此。最终你的方法将永远运行(如果你去处理大数,x+1 就是 x,在 double 的土地上。想想看:双打是 64 位的,所以最多只能代表 2^64 个不同的数字。然而比方说,double 可以做 1e308。你怎么能把 1e308 鸽子放在 2^64 洞里?答案是:你不能。不是 0 到无穷大之间的每个数字都可以表示为 double,当你尝试设置某个数字不在 2^64 个可表示的空间中,java 默默地四舍五入到最接近的位置。最终,可表示之间的差距超过 1.0,在这一点上,i++无法对 i 进行任何更改。它不完全是 1e9(我认为大约是 2^53),但是用双打进行增量计数总是一个坏主意。有条件就去吧long

此外,C 和 java(但不是 javac,我在第二点中谈到的热点分析器)都有“优化器”。如果优化器意识到 [A] 您实际上并未get()在代码中的任何地方使用结果,并且 [B] get() 方法要么根本没有副作用,要么仅通过运行就可以完全覆盖副作用总说明中的get()的一小部分,那么优化器是免费的,只是不能运行的方法,或者至少只运行其中的一部分,这将导致广泛的不同性能测量。

JMH 也解决了这个问题:例如,它强制您在测量方法中返回一些数值,因为 JMH 会将这个数字混合成一个它跟踪的值,从而迫使优化器意识到它不能仅仅通过跳过整个通话!

cTM 不是免费的

System.currentTimeMillis()是,也可以是,相当昂贵。作为一种语言,C 几乎不承诺任何事情(它甚至不承诺 anint是 32 位的!),但是任何特定的给定库 impl 倾向于对给定调用的功能做出极其具体的承诺。Java位于中间。这意味着当您运行时,java实际上最终在操作系统级别执行cTM可能会有所不同,并且涉及一些缓存 + 使用 CPU 内核自己的内部时钟,这比“询问系统时间”快许多数量级,而每次调用时,C 调用都会将工作转移到系统时间,因为 C 代码假定如果您想优化和估计 CPU 内核更新,那么您将对其进行编程或获取一个库。你(潜在的)大部分时间CTM的性能这里,而不是你的算法,以及C和Java代码之间的CTM可以有广泛不同的实现。换句话说,您将枪支与祖母进行比较。

JMH 像往常一样,在这里帮助您,并避免 cTM 的问题。并不是说我知道将 JMH 结果与 C 结果进行比较的方法,但至少 JMH 计时结果是您可以信任的东西,而不仅仅是 cTM 调用之间的手​​纺增量。

cTM 没有你想象的那么稳定

cTM 糟透了。问题是:时钟真的很难。我知道,我知道,你可以去商店,买一块 5 美分的手表,里面有一些便宜的水晶,它的准确度令人惊讶。但是计算机芯片的表面是一个极其荒凉的地方,温度波动剧烈,电子流遍整个地方,附近有大量空气流动。试图在这些条件下保持石英晶体稳定是很棘手的。因此,要么系统时钟远离 CPU,但现在要求系统时间与基本指令相比非常昂贵(实际上是数十万个周期,因为电子像缓慢的糖蜜一样,通过许多厘米长的电缆行进) ,在计算机 CPU 术语中是永恒的),或者它在机上(并且它们是),并且没有您想要的那么稳定。

CPU 内核具有内部时钟,可以更稳定,但更不受限制地反映任何实际时间,如果您的代码移动到另一个具有完全不同内核时钟的内核,则会导致严重问题。Java 使您可以访问 - System.nanoTime,甚至尝试消除核心跃点问题,但正如此答案中的主题一样:时间就是方式,比您想象的要困难得多,但幸运的是,这是一个已基本解决的问题。请注意 nanoTime 如何故意返回一个无意义的数字:它仅与对 nanoTime 的其他调用有关,它本身没有任何意义(而 cTM 表示:自 UTC 1970-1-1 午夜以来的毫秒数)。这很棘手 - JMH 解决了这个问题,你应该使用它。