调试:(i!=t++)与(t++!=i)的区别。

3

我遇到了一个问题,需要使用 (i!=t++) 这个条件。但不幸的是,使用这个条件会导致 TLE。但当我改用 (t++!=i) 时,代码便成功通过了。我认为对于下面的程序,这两种条件是相同的。在我的编译器上运行该程序的输出结果为:5259 3,即第一次和第二次循环所需的时间。我找不到这两种条件之间的区别。当然,可能存在某处的 bug 我无法找到。

void test()
{
    long b=System.currentTimeMillis();
    for(int i=0;i<100001;i++) {
        int t=0;
        while (i!=t++)
        {
            int x=34;
            x++;

        }
    }
    System.out.println(System.currentTimeMillis()-b);

    b=System.currentTimeMillis();
    for(int i=0;i<100001;i++) {
        int t=0;
        while (t++!=i)
        {
            int x=34;
            x--;
            x++;
        }
    }
}

我已经复制了这些结果,并且验证了这不仅仅是因为第一个循环先运行 - 即使我将其放在另一个循环之后,while (i!=t++) 也会更慢。此外,我通过添加计数器验证了两个循环运行相同的次数。 - D M
@DM,是的,我也尝试了所有这些。两个循环运行完全相同。 - Nishant Sharma
@NishantSharma 你有没有在在线编译器上做这个?因为如果我缩短循环,两者都需要相同的时间...也许你没有足够的时间了?第一个循环需要近5秒才能完成。一些在线编译器/编程练习有编译限制,即程序可以运行多少次。 - Gijs Den Hollander
@GijsDenHollander 我在IDE和在线编译器上运行它,两者的结果几乎相同。即使我将较低的循环放在顶部,结果仍然相同,但交换了位置。 - Nishant Sharma
那么在你的IDE上也无法运行吗?怎么会超时呢? - Gijs Den Hollander
显示剩余13条评论
3个回答

2
这只是一个初步的答案,未来还需要更多调查。但是目前的答案是——这是JIT优化的效果。此外,需要注意的是微基准测试不是性能测试的最佳选择,特别是在像Java这样的动态编译语言中(例如,请参见这篇专家论文)。
我正在使用Windows 10 Home,java -version命令输出如下:

java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)

我已经对您的代码进行了重构,并添加了x作为外部计数器来确保循环不会被优化掉。
void test1() {
    int x = 0;
    long b = System.currentTimeMillis();
    for (int i = 0; i < 100_001; i++) {
        int t = 0;
        while (i != t++) {
            x++;
        }
    }
    long b1 = System.currentTimeMillis();
    System.out.println("T(test1) = " + (b1 - b));
    System.out.println("x(test1) = " + x);
}

void test2() {
    int x=0;
    long b = System.currentTimeMillis();
    for (int i = 0; i < 100001; i++) {
        int t = 0;
        while (t++ != i) {
            x++;
        }
    }
    long b1 = System.currentTimeMillis();
    System.out.println("T(test2) = " + (b1 - b));
    System.out.println("x(test2) = " + x);
}

每个函数被调用两次:

t.test1();
t.test1();
t.test2();
t.test2();

好的,让我们看一下标准java Test调用的结果(没有提供其他解释器参数):

T(test1) = 3745
x(test1) = 705082704
T(test1) = 0
x(test1) = 705082704
T(test2) = 5
x(test2) = 705082704
T(test2) = 0
x(test2) = 705082704

如您所见,在第二次调用之后,两种情况下的运行时间都等于0。即使我们将int x = 0的初始化更改为int x = new Random().nextInt()以确保第二次调用的结果没有被缓存或其他什么,情况也是如此。通常,应在进行测量之前“热身”Java解释器,即在同一次运行中测量代码性能两次,并且舍弃第一个结果,以确保优化已经就位。但这是在线评测习题时不可得到的奢侈品。
现在来看另一部分。Oracle的JDK有一个-Xint解释器开关,可以完全关闭JIT。让我们使用它并看看会发生什么。我还使用了-XX:+PrintCompilation标志,以确保不进行任何编译(即解释器被调用为java -XX:+PrintCompilation -Xint Test;如果没有打印其他诊断信息,则表示代码未编译):

T(test1) = 56610
x(test1) = 705082704
T(test1) = 55635
x(test1) = 705082704
T(test2) = 60247
x(test2) = 705082704
T(test2) = 58249
x(test2) = 705082704

两个观察结果:现在任务需要很长时间,并且所有调用的结果都相似。必须进行更多的调查,以发现为什么JIT会对这两个代码进行不同的优化。
编辑:有趣的JIT第二部分 所以,我开始尝试不同的编译选项。一般来说,JIT 使用两种类型的编译器。C1 (客户端) 编译器旨在提供更快的 JVM 启动时间,但不如 C2 (服务器端) 编译器快。我使用的 64 位 Java 8 JVM 似乎只有服务器是唯一可用的选项(请参见此 FAQ;但是可以使用 -XX:TieredStopAtLevel= 标志选择不同的编译级别;为了简洁,我不会粘贴使用它获取的结果,但它们支持这个命题,即第一个调用 test2 的速度更快)。
我也恰好在我的机器上安装了 32 位 JRE。它不支持服务器编译器,并给出以下版本信息:

java 版本 "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) Client VM (build 25.121-b13, mixed mode)

这个 JVM 的结果如下所示:

T(test1) = 3947
x(test1) = 705082704
T(test1) = 3985
x(test1) = 705082704
T(test2) = 4031
x(test2) = 705082704
T(test2) = 4172
x(test2) = 705082704


0

我已经测试了你的代码并检查了它运行的时间。以下是结果(循环1和2的毫秒时间):

 time1   time2 
 175.2   171.0
 189.6   187.1
 162.1   164.2
 162.3   162.1
 162.3   166.0

两个循环同时运行。

以下是代码

private static void test()
  {
    long b;

    float time1=0;
    float time2=0;

    int x=0;
    for(int l=0;l<10;l++) {
        b=System.currentTimeMillis();
        for(int i=0;i<20001;i++) {
          int t=0;
          while (i!=t++)
            {
            x++;
            }
        }


        time1+=System.currentTimeMillis()-b;

        b=System.currentTimeMillis();
        x=0;
        for(int i=0;i<20001;i++) 
            {
            int t=0;
            while (t++!=i) 
                {
                    x++;
                }


        }

    time2+=System.currentTimeMillis()-b;

    }

    time1/=10;
    time2/=10 ;   
    System.out.println(time1);  
    System.out.println(time2);
}

我不确定您为什么认为这些语句会改变情况。但我认为您的程序在在线编译器上已经超时(它有一个结束时间),然后您更改了参数并认为现在它可以运行了。 这可能是由于您的代码已经热身,这可能会略微减少程序的运行时间。但这只是猜测... 不应该作为答案。

请亲自测试以上代码。

回答您的问题,两个逻辑表达式是相同的。虽然后缀i++(复制值,增加当前值,产生副本)和前缀++i(增加当前值并产生结果)之间存在轻微差异。其中第二个操作次数较少。但这不应导致不同的计算时间。

查看此帖子为什么“while (i++ < n) {}”比“while (++i < n) {}”慢得多?以解释为什么您的IDE会给出一些奇怪的结果。


我运行了你的代码,得到了193.8和0.5的时间。你用的是哪个Java版本? - D M

-1

我最好的猜测是这是字节码生成和/或执行方式的怪癖。

计算机处理器在任何给定时间内可以使用有限数量的寄存器来存储它们正在处理的值。可能根据首先出现的变量将变量放置在不同的寄存器中。如果变量最初位于不同的寄存器中,那么可能会影响哪个变量被删除以为其他内容腾出空间。

如果一个值在稍后需要时不在寄存器中,则处理器必须从计算机的内存(或处理器的缓存)中获取它,这比已经在寄存器中要花费更多的时间。

还有一些情况,处理器可以尝试在另一个语句完成之前开始执行一个语句。但是,如果一个值发生了变化,这可能会被中断。如果 t++ 在尝试再次加载 t 之前很快发生,那么处理器可能会被中断并不得不重新开始指令。(但是,我认为这不是发生这种情况的主要原因;它不会产生如此大的影响。)

也许这只是Java编译器看到它可以在一个实例中执行某些优化而在另一个实例中不能执行的优化问题。


我不认为这是原因。如果我们为每个循环创建两个不同的程序,执行时间仍然会有很大差异。我也不明白你在第四段中想说什么,t和t++。我的意思是,这两个循环都有t++。所以你是在说,并行处理是问题的原因吗? - Nishant Sharma

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接