Chi phí (ẩn) của val lười biếng của Scala là gì?


165

Một tính năng tiện dụng của Scala là lazy val, trong đó việc đánh giá a valbị trì hoãn cho đến khi cần thiết (ở lần truy cập đầu tiên).

Tất nhiên, lazy valphải có một số chi phí chung - ở đâu đó Scala phải theo dõi xem giá trị đã được đánh giá chưa và đánh giá phải được đồng bộ hóa, bởi vì nhiều luồng có thể cố gắng truy cập giá trị lần đầu tiên cùng một lúc.

Chính xác thì chi phí của một lazy val- có một cờ boolean ẩn liên quan đến một lazy valđể theo dõi nếu nó đã được đánh giá hay không, chính xác những gì được đồng bộ hóa và có thêm chi phí?

Ngoài ra, giả sử tôi làm điều này:

class Something {
    lazy val (x, y) = { ... }
}

Đây có giống như có hai lazy vals riêng biệt xytôi chỉ nhận được chi phí một lần cho cặp này (x, y)?

Câu trả lời:


86

Điều này được lấy từ danh sách gửi thư scala và cung cấp chi tiết triển khai về lazymặt mã Java (chứ không phải mã byte):

class LazyTest {
  lazy val msg = "Lazy"
}

được biên dịch thành một cái gì đó tương đương với mã Java sau:

class LazyTest {
  public int bitmap$0;
  private String msg;

  public String msg() {
    if ((bitmap$0 & 1) == 0) {
        synchronized (this) {
            if ((bitmap$0 & 1) == 0) {
                synchronized (this) {
                    msg = "Lazy";
                }
            }
            bitmap$0 = bitmap$0 | 1;
        }
    }
    return msg;
  }

}

33
Tôi nghĩ rằng việc triển khai phải thay đổi kể từ khi phiên bản Java này được đăng vào năm 2007. Chỉ có một khối được đồng bộ hóa và bitmap$0trường này không ổn định trong triển khai hiện tại (2.8).
Mitch Blevins

1
Có - tôi nên chú ý nhiều hơn đến những gì tôi đã đăng!
oxbow_lakes

8
@Mitch - Tôi hy vọng việc thực hiện đã thay đổi! Mẫu chống khởi tạo được kiểm tra hai lần là một lỗi tinh tế cổ điển. Xem en.wikipedia.org/wiki/Double-checked_locking
Malvolio

20
Nó là antipotype lên tới Java 1.4. Do từ khóa biến động Java 1.5 có ý nghĩa chặt chẽ hơn một chút và bây giờ việc kiểm tra kép như vậy là ổn.
iirekm

8
Vì vậy, kể từ scala 2.10, việc thực hiện hiện tại là gì? Ngoài ra, có thể xin vui lòng ai đó đưa ra gợi ý bao nhiêu chi phí này có nghĩa là trong thực tế và một số quy tắc khi nào nên sử dụng, khi nào nên tránh?
ib84

39

Dường như trình biên dịch sắp xếp cho một trường int bitmap cấp độ lớp để gắn cờ nhiều trường lười biếng như được khởi tạo (hoặc không) và khởi tạo trường đích trong một khối được đồng bộ hóa nếu xor có liên quan của bitmap cho thấy cần thiết.

Sử dụng:

class Something {
  lazy val foo = getFoo
  def getFoo = "foo!"
}

sản xuất mã byte mẫu:

 0  aload_0 [this]
 1  getfield blevins.example.Something.bitmap$0 : int [15]
 4  iconst_1
 5  iand
 6  iconst_0
 7  if_icmpne 48
10  aload_0 [this]
11  dup
12  astore_1
13  monitorenter
14  aload_0 [this]
15  getfield blevins.example.Something.bitmap$0 : int [15]
18  iconst_1
19  iand
20  iconst_0
21  if_icmpne 42
24  aload_0 [this]
25  aload_0 [this]
26  invokevirtual blevins.example.Something.getFoo() : java.lang.String [18]
29  putfield blevins.example.Something.foo : java.lang.String [20]
32  aload_0 [this]
33  aload_0 [this]
34  getfield blevins.example.Something.bitmap$0 : int [15]
37  iconst_1
38  ior
39  putfield blevins.example.Something.bitmap$0 : int [15]
42  getstatic scala.runtime.BoxedUnit.UNIT : scala.runtime.BoxedUnit [26]
45  pop
46  aload_1
47  monitorexit
48  aload_0 [this]
49  getfield blevins.example.Something.foo : java.lang.String [20]
52  areturn
53  aload_1
54  monitorexit
55  athrow

Các giá trị được khởi tạo trong các bộ dữ liệu giống như lazy val (x,y) = { ... }đã lưu bộ đệm vào nhau thông qua cùng một cơ chế. Kết quả bộ dữ liệu được đánh giá và lưu vào bộ đệm một cách lười biếng và quyền truy cập của x hoặc y sẽ kích hoạt đánh giá bộ dữ liệu. Khai thác giá trị riêng lẻ từ bộ dữ liệu được thực hiện độc lập và lười biếng (và được lưu trữ). Vì vậy, các mã đúp instantiation trên tạo ra một x, yvà một x$1lĩnh vực loại Tuple2.


26

Với Scala 2.10, một giá trị lười biếng như:

class Example {
  lazy val x = "Value";
}

được biên dịch thành mã byte giống với mã Java sau:

public class Example {

  private String x;
  private volatile boolean bitmap$0;

  public String x() {
    if(this.bitmap$0 == true) {
      return this.x;
    } else {
      return x$lzycompute();
    }
  }

  private String x$lzycompute() {
    synchronized(this) {
      if(this.bitmap$0 != true) {
        this.x = "Value";
        this.bitmap$0 = true;
      }
      return this.x;
    }
  }
}

Lưu ý rằng bitmap được đại diện bởi a boolean. Nếu bạn thêm một trường khác, trình biên dịch sẽ tăng kích thước của trường để có thể biểu diễn ít nhất 2 giá trị, tức là a byte. Điều này chỉ diễn ra cho các lớp học lớn.

Nhưng bạn có thể tự hỏi tại sao điều này làm việc? Các bộ đệm cục bộ luồng phải được xóa khi nhập một khối được đồng bộ hóa sao cho xgiá trị không bay hơi được đưa vào bộ nhớ. Bài viết blog này đưa ra một lời giải thích .


11

Scala SIP-20 đề xuất một triển khai mới của val lười, điều này đúng hơn nhưng chậm hơn ~ 25% so với phiên bản "hiện tại".

Việc thực hiện đề xuất trông như sau:

class LazyCellBase { // in a Java file - we need a public bitmap_0
  public static AtomicIntegerFieldUpdater<LazyCellBase> arfu_0 =
    AtomicIntegerFieldUpdater.newUpdater(LazyCellBase.class, "bitmap_0");
  public volatile int bitmap_0 = 0;
}
final class LazyCell extends LazyCellBase {
  import LazyCellBase._
  var value_0: Int = _
  @tailrec final def value(): Int = (arfu_0.get(this): @switch) match {
    case 0 =>
      if (arfu_0.compareAndSet(this, 0, 1)) {
        val result = 0
        value_0 = result
        @tailrec def complete(): Unit = (arfu_0.get(this): @switch) match {
          case 1 =>
            if (!arfu_0.compareAndSet(this, 1, 3)) complete()
          case 2 =>
            if (arfu_0.compareAndSet(this, 2, 3)) {
              synchronized { notifyAll() }
            } else complete()
        }
        complete()
        result
      } else value()
    case 1 =>
      arfu_0.compareAndSet(this, 1, 2)
      synchronized {
        while (arfu_0.get(this) != 3) wait()
      }
      value_0
    case 2 =>
      synchronized {
        while (arfu_0.get(this) != 3) wait()
      }
      value_0
    case 3 => value_0
  }
}

Kể từ tháng 6 năm 2013, SIP này đã không được phê duyệt. Tôi hy vọng rằng nó có khả năng được phê duyệt và đưa vào phiên bản tương lai của Scala dựa trên thảo luận về danh sách gửi thư. Do đó, tôi nghĩ rằng bạn nên khôn ngoan để theo dõi sự quan sát của Daniel Spiewak :

Lazy val là * không * miễn phí (hoặc thậm chí rẻ). Chỉ sử dụng nó nếu bạn thực sự cần sự lười biếng cho chính xác, không phải để tối ưu hóa.


10

Tôi đã viết một bài liên quan đến vấn đề này https://dzone.com/articles/cost-laziness

Tóm lại, hình phạt nhỏ đến mức trong thực tế bạn có thể bỏ qua nó.


1
Cảm ơn vì điểm chuẩn đó. Bạn cũng có thể điểm chuẩn so với triển khai đề xuất SIP-20?
Turadg

-6

được cung cấp bởi mã được tạo bởi scala vì lười biếng, nó có thể gặp vấn đề về an toàn luồng như đã đề cập trong khóa kiểm tra kép http://www.javaworld.com/javaworld/jw-05-2001/jw-0525-double.html?page=1


3
Khiếu nại này cũng được đưa ra bởi một nhận xét cho câu trả lời được chấp nhận bởi mitch và bị bác bỏ bởi @iirekm: Mẫu này vẫn ổn từ java1.5 trở đi.
Jens Schauder
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.