在什么情况下,do-while循环比while循环更高效?

8

While与Do-while

当代码块为空时,while和do-while是功能上等同的,尽管while看起来更加自然:

do {} while (keepLooping());
while (keepLooping()) {}

在it技术中,while/do-while结构内空块的典型应用是使用compareAndSet(CAS)强制更新原子对象。例如,下面的代码将以线程安全的方式增加a的值:

int i;
AtomicInteger a = new AtomicInteger();
while (!a.compareAndSet(i = a.get(), i + 1)) {}

背景

java.util.concurrent 的几个部分使用 do {} while (...) 这种方式进行 CAS 操作,ForkJoinPool 的 javadoc 解释道:

有几个不寻常的 do {} while (!cas...) 出现,这是强制更新 CAS 变量的最简单方法。

既然他们承认这是不寻常的,我想他们可能是指最好而不是最简单。

问题

在什么情况下,do {} while (!cas) 可能比 while (!cas) {} 更有效,并出于什么原因?


也许这只是历史偏好。无论如何,为什么要使用 {} 呢---我会写成 while (condition); - Marko Topolnik
1
@MarkoTopolnik 同意使用分号。我在这个视频中发现,Doug Lea在57:00左右说:“不要使用while,而要使用do-while,因为安全点”,并且幻灯片提到“更小的竞争窗口”。虽然我不明白这里怎么有影响,但我猜他指的是GC安全点。 - assylias
1
Doug对此有点含糊其辞 :) 我能理解的最好方式是,这不是关于do-while vs. while的问题,而是关于使用循环体进行赋值与将所有内容塞入条件中的区别。 - Marko Topolnik
@MarkoTopolnik 我也是这样理解的 - 但是为什么会有麻烦呢?我猜SO不是最好的问询地点! - assylias
3
是的……这在 SO 上是“不具建设性”的,因为只有地球上大约三个人能够给出超越猜测的答案 :) - Marko Topolnik
显示剩余2条评论
3个回答

2
所以,'do while' 意味着它将运行一次 while 循环中的代码。然后,只有在条件为真时才会运行 while 循环内部的代码。
简单演示。
boolean condition = false;

do{
  System.out.Println("this text displayed");
}while(condition == true);

输出 "this text displayed"

正常

while(condition == true){
 System.out.Println("this text displayed");
}

输出 ""

  • *由于条件为假,未显示任何输出。

为什么或在哪里使用do while,我还没有遇到过需要这样做的情况,所以无法帮助您。这只是识别问题/需求,并使用已知知识解决它的问题,类似于乐高 - 机械类而不是“积木”。


0

可能会出现这样的情况,即期望值和更新值的计算过于复杂,无法与调用compareAndSet的同一行中可读。 那么你可以在do内部使其更易读:

do {
  int expect = a.get();
  int update = expect + 1;
} while (!a.compareAndSet(expect, update));

该问题假设空块,因此while和do-while是严格等效的。 - assylias
啊哈。也许这更接近它了。 - oe.elvik
请参考以下实际示例:http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/7-b147/java/util/concurrent/ForkJoinPool.java#ForkJoinPool.postBlock%28%29 - 我认为这不是问题所在。 - assylias
在大多数情况下,我同意使用 while (cas); 更好。我只是举了一个例子,说明在某些情况下,do {} while (!cas) 可能比 while (!cas) {} 更易读。这可能是为什么 do {} while 在文档中更常见的原因。 - oe.elvik
你的代码无法编译,因为一旦离开循环体,expectupdate就超出了作用域。不过,在循环之前声明这两个变量就可以解决问题,所以你的观点仍然是正确的。 - siegi

0
这不是效率问题。有些情况只能用do{}while()解决。看看java.util.Random.next(int bits)。如果你试图用while(){}做同样的事情,你会有一个代码重复,因为循环体必须在条件之前执行一次。
我已经问过一个非常类似的问题了:Java中编译循环
这段代码:
public class Test {

    static int i = 0;

    public static void main(String[] args) {
        method1();
        method2();
    }

    public static void method2() {
        do{}while(++i < 5);
    }

    public static void method1() {
        while(++i < 5);
    }
}

被编译成:

public static void method2();
  Code:
   0:   getstatic       #4; //Field i:I
   3:   iconst_1
   4:   iadd
   5:   dup
   6:   putstatic       #4; //Field i:I
   9:   iconst_5
   10:  if_icmplt       0
   13:  return

public static void method1();
  Code:
   0:   getstatic       #4; //Field i:I
   3:   iconst_1
   4:   iadd
   5:   dup
   6:   putstatic       #4; //Field i:I
   9:   iconst_5
   10:  if_icmpge       16
   13:  goto    0
   16:  return

你可能会注意到在method1()的第13行有额外的指令。但正如我的问题中的答案所建议的那样,当被JIT编译成机器指令时,这并没有任何区别。非常难以捉摸的性能提升。要证明它,你必须使用PrintAssembly键运行。理论上,method2更快,但在实践中它们应该是相等的。


2
你没有仔细阅读问题。根本没有循环体,而效率问题非常低级,涉及最终的本地代码排序。 - Marko Topolnik
我刚刚查看了汇编代码,没有什么引起我的注意。 - assylias
你的意思是什么?它们都一样吗? - Mikhail
只有两个指令的顺序不同,其余严格相同。 - assylias
1
为什么人们认为反汇编的字节码有指导意义呢?1)我们都知道whiledo while的含义。2)你无法从字节码中推断出代码效率的任何信息。显著的优化发生在JIT编译器中。(即使具有相同字节码序列的方法由于JIT编译器使用解释器的运行时统计数据而表现不同。) - Stephen C
显示剩余2条评论

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