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

标签 java c++

我编写了一个小程序来生成唯一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);
        }
    }
}


结果似乎有些奇怪。
53
690531076282879
5
690531076281343
0
690531076283903
0
690531076282367
0
690531076280831
0
690531076283391
0
690531076281855
0
690531076284415
0
690531076282879
0
690531076281343

它使用0毫秒来获得正确的结果,而C++版本需要2.3亿秒来获得一个结果。当我将内部循环的数量从1000000000更改为类型为double的1e9时,每个结果要花费一秒钟以上的时间。怎么会这样
我更改了C++版本的循环数,并且没有任何变化。因此,我想Java优化了循环并省略了前999999999个循环。 Java如何实际优化它并免费运行却获得正确的结果?以及如何优化同一代码的C++版本以跳过无用的循环?我使用-O3标志,但似乎无法正常工作。
#include <iostream>
#include <chrono>

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

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


std::int64_t get() {
//    auto current = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
    auto current = 164635438;
    if (current != lastTimeStamp) {
        snowFlakeId = 0;
        lastTimeStamp = current;
    }else{
        snowFlakeId++;
    }
    unsigned long long id = 0;
    // Datetime part
    id |= static_cast<unsigned long long>(static_cast<unsigned long long>(current) & dateTimeMask);

    // DataCenter Part
    id <<= dataCenterNumberShiftBits;
    id |= static_cast<uint>(static_cast<uint>(DataCenterID)&dataCenterNumberMask);

    // Machine Part
    id <<= machineNumberShiftBits;
    id |= static_cast<uint>(static_cast<uint>(MachineID)&machineNumberMask);

    // Random Number Part
    id <<= randomNumberShiftBits;
    id |= static_cast<uint>(snowFlakeId&randomNumberMask);

    return id;
}

int main() {
    for (int out = 0; out < 10; out++) {
        uint64_t result = 0;
        auto start = std::chrono::duration_cast<std::chrono::milliseconds>(
                std::chrono::system_clock::now().time_since_epoch()).count();
        for (int i = 0; i < 1000000000; i++) {
            result = get();
        }
        auto end = std::chrono::duration_cast<std::chrono::milliseconds>(
                std::chrono::system_clock::now().time_since_epoch()).count();
        std::cout << (end - start) << std::endl;
        std::cout<<result<<std::endl;
    }
    return 0;
}

这是C++版本及其结果:
1419
690531076282879
1385
690531076281343
1388
690531076283903
1457
690531076282367
1407
690531076280831
1402
690531076283391
1441
690531076281855
1389
690531076284415
1395
690531076282879
1360
690531076281343

至于时间的测量,这只是主函数中的代码。我知道算法是错误的,我很好奇为什么Java可以做到这一点,以及如何使C++也跳过循环。

最佳答案

您的操作存在很多误解。
System.currentTimeMillis()作为ID是个坏主意
这是一个非常糟糕的主意。您的代码显然设计为将System.currentTimeMillis(cTM)视为严格增加的序列。例如,如果当前时间是10000,并且我要求输入ID,则得到10000:0。如果我再次询问,我会得到10000:1。如果时间变成10001,我将得到10001:0,如果时间然后回到10000,我将再次得到10000:0,这违反了生成唯一数字的意图。
但是事情是这样的: cTM绝对不能保证它会严格增加。
cTM反射(reflect)系统时钟。在某些落后的系统上,系统时钟代表本地时间,而不是UTC。 Java应该可以“解决”这一问题,但是众所周知,在夏令时调整期间,时间浪费了360万毫秒(相当于一个小时)。更一般而言,大多数计算机都是从某个网络来源获取时间,并且会一直将时间调整几秒钟(很容易几千毫秒)。如果您必须具有唯一的ID,并且有系统时间是唯一可能的提供者,那么有解决方案,但是完整的博士学位研究论文已经写了关于如何执行的方法(这被称为“涂片”,您的计算机可能没有这样做,而JVM仅会报告操作系统在说什么,因此也不会抹黑。)
System.nanoTime()或多或少地保证会增加,但是大约每隔36天就会循环一次。如果您想要唯一的ID,请使用正确的工具:UUID。生成唯一ID的难度比您想象的要难,并且已解决了问题。使用现有的解决方案。
用cTM计时性能是个坏主意
也是错的Java的工作原理大致如下:
非常缓慢和愚蠢地运行所有代码,并跟踪各种完全不相关的信息,例如“对于此if分支,表达式多长时间解析为“true”与“false”,或者“此频率为多少”方法称为“。收集这些统计信息会使其效率更低。 JVM现在效率极低。但这很好,您稍后会看到。与C代码相反,gcc或您使用的任何编译器都将分析源代码中的异常内容,并制造出可能的最优化机器代码,但这就是结束的地方:无簿记。从编译停止就对代码进行了优化。 VS. java; javac非常简单,也很愚蠢,它几乎没有优化。确实是java在运行时实现的。
然后,不时进行分析:系统中的哪一种方法占用最多的CPU时间?然后,花点时间以及所有这些看似无用的统计信息来生成此方法的惊人的优化优化机器编码版本。它可以而且经常会胜过手写代码。毕竟,对于这种实际的工作量,java的优点是实时了解行为,而诸如C编写的代码则无法知道这些行为。 Java甚至可以使用内置的假设来生成代码,因为如果其中一个假设在稍后失败,则Java可以使该优化变体“无效”。
结果是,再次简化了很多,任何给定方法的一般性能特征是每次运行花费X时间一会儿(X为1000),然后一个调用花费的时间更长进行分析(例如10000000),然后所有进一步的调用花费Y时间,其中Y比X小很多(例如10)。
在1000处的周期数以及重新编译时的一个瞬动信号是“恒定的”,然后实际的10表示所有其他周期。随着越来越多的循环被应用(并且由于我们仅优化通常被称为方法的循环,所以10个循环使其他循环相形见)),10个循环是实现性能的唯一重要数字。
,但这确实意味着您需要等到发生这种情况之后才能衡量性能,这一点都不容易。您还会得到其他“噪音”。也许您的线程被winamp抢占了,因为它需要解压缩该MP3文件中的更多内容,从而导致您的时间任意分配问题。
答案是 JMH 。再说一遍,一些问题:手头的工作(定时方法调用)比您想象的要复杂几个数量级,但这是一个已解决的问题,因此请使用现有的解决方案。
关于您观察到的表现的一些猜测
如果您将它设为两倍,则必须将1加到两倍上,这可能会慢几个数量级,因为双重比较也是如此。最终,您的方法将永远运行(如果您使用大数,则x + 1就是x,在 double 域中。考虑一下: double 数是64位的,因此最多只能表示2 ^ 64个不同的数字。但是一个 double 数可以做到,例如1e308。如何在仅2 ^ 64孔中容纳1e308鸽子?答案是:不能,不是所有介于0和无穷大之间的数字都可以表示为 double 数,并且当您尝试设置某个数字不在2 ^ 64可表示空间的范围内,java默默地四舍五入到最接近的整数。最终可表示对象之间的距离超过1.0,此时i++无法对i进行任何更改。它不是完全等于1e9(我认为大约在2 ^ 53),但是用double进行增量计数始终是个坏主意,如果需要,请使用long
此外,C和Java(但不是Javac,我在第二点中谈到的那个热点分析器)都具有“优化程序”。如果优化程序意识到[A]您实际上并未在代码中的任何地方使用get()的结果,并且[B] get()方法要么根本没有副作用,要么仅由副作用完全覆盖运行get()中全部指令的一小部分,然后,优化器可以自由地不运行该方法,或者至少仅运行其中的一部分,这将导致表现出不同的性能。
JMH也解决了这个问题:例如,它强制您在测量方法中返回一些数字值,因为JMH会将这个数字混合到它跟踪的值中,从而迫使优化器意识到它不能仅仅通过“优化”跳过整个通话!
cTM不是免费的System.currentTimeMillis()非常昂贵,也可能非常昂贵。 C作为一种语言几乎对任何事情都不做任何 promise (甚至不保证int是32位的!),但是任何特定的给定库隐含的内容都倾向于对给定调用的行为做出非常明确的 promise 。 Java位于中间。这意味着,当您运行cTM时,java最终在操作系统级别上实际执行的操作可能有所不同,并且涉及一些缓存+使用CPU内核自己的内部时钟,这比“询问系统时间”要快多个数量级,而C调用每次调用都会将工作分配给系统时间,因为C代码假定,如果您想通过CPU内核更新进行优化和估算,则可以对其进行编程或获取一个库。您(很有可能)主要是在这里计时cTM的性能,而不是算法的时间,并且在C和Java代码之间,cTM可能有不同的实现。换句话说,您正在将枪支与祖母进行比较。
像往常一样,JMH可为您提供帮助,并避免了cTM的问题。我并不是知道可以将JMH结果与C结果进行比较的方法,但是至少JMH计时结果比cTM调用之间的手​​动增量更值得信赖。
cTM并不像您认为的那样稳定
cTM很烂。问题是:时钟真的很难。我知道,我知道,您可以去商店购买那里装有便宜 Crystal 的5美分手表,它的精确度令人惊讶。但是计算机芯片的表面是一个极其荒凉的地方,狂野的温度波动,电子在整个地方流动,大量空气在附近移动。在这些条件下试图保持 quartz 晶体稳定是很棘手的。因此,要么系统时钟距离CPU很远,但现在与基本指令相比,请求系统时间的成本却高得惊人(实际上,成千上万个周期,因为电子像缓慢的糖蜜一样通过数厘米长的电缆传播,以计算机CPU术语来说是永恒的),或者它已经存在(并且确实存在),并且不稳定程度不如您所愿。
CPU内核具有内部时钟,该时钟可能更稳定,但无法反射(reflect)任何实际时间,并且如果将代码移至具有完全不同的内核时钟的另一个内核,则会导致严重问题。 Java使您可以访问它-System.nanoTime,甚至尝试解决核心跃点问题,但正如该答案的主题一样:时间是一种方式,比您想象的要困难得多,但是幸运的是,这是一个已解决的大多数问题。请注意,nanoTime有意返回无意义的数字:它仅与其他对nanoTime的调用有关,没有意义(而cTM的含义是:自1970-1-1 UTC午夜以来的毫秒数)。这很棘手-JMH解决了这个问题,您应该使用它。

关于java - 为什么Java可以在不花费时间的情况下运行代码?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64141268/

相关文章:

c++ - SDL2 中的多个显示

c++ - 高级 Win32 图像文件 I/O?

c++ - 或者接线员不工作

c++ - "Updating"文件中的一行被覆盖

java - 即使设置了 spring.expression.compiler.mode,Spring SpEL 也不会编译

java - Maven:从 pom.xml 中的 settings.xml 读取加密密码

c++ - 从 C++ 调用静态 C++/CLI 方法

java - Firefox在Struts应用程序中剪切名称中包含空格的文件

当正则表达式的第一个字符是 * 时,Java PatternSyntaxException

java - Android MediaPlayer setNextMediaPlayer() 替代方案