Có đắt không?


111

Sau khi đọc Sách dạy nấu ăn JSR-133 cho Người viết trình biên dịch về việc triển khai biến biến động, đặc biệt là phần "Tương tác với hướng dẫn nguyên tử", tôi giả định rằng việc đọc một biến dễ bay hơi mà không cập nhật biến đó cần LoadLoad hoặc rào cản LoadStore. Đi sâu xuống trang, tôi thấy rằng LoadLoad và LoadStore là những thứ không cần thiết trên CPU X86. Điều này có nghĩa là các hoạt động đọc biến động có thể được thực hiện mà không có sự vô hiệu hóa bộ nhớ cache rõ ràng trên x86 và nhanh như đọc biến bình thường (bỏ qua các ràng buộc sắp xếp lại của biến động)?

Tôi tin rằng tôi không hiểu điều này một cách chính xác. Ai đó có thể quan tâm để khai sáng cho tôi?

CHỈNH SỬA: Tôi tự hỏi nếu có sự khác biệt trong môi trường đa xử lý. Trên các hệ thống CPU đơn lẻ, CPU có thể xem xét các bộ nhớ đệm luồng riêng của nó, như John V. nói, nhưng trên các hệ thống nhiều CPU, phải có một số tùy chọn cấu hình cho các CPU mà điều này là không đủ và bộ nhớ chính phải bị tấn công, làm cho biến động chậm hơn trên hệ thống nhiều cpu, phải không?

Tái bút: Trên con đường tìm hiểu thêm về vấn đề này, tôi tình cờ thấy những bài báo tuyệt vời sau đây và vì câu hỏi này có thể thú vị với những người khác, tôi sẽ chia sẻ các liên kết của mình ở đây:


1
Bạn có thể đọc bản chỉnh sửa của tôi về cấu hình với nhiều CPU mà bạn đang đề cập đến. Có thể xảy ra rằng trên các hệ thống nhiều CPU cho một tham chiếu tồn tại trong thời gian ngắn, không còn một lần đọc / ghi vào bộ nhớ chính nữa.
John Vint

2
bản thân việc đọc dễ bay hơi không đắt. chi phí chính là cách nó ngăn chặn tối ưu hóa. trong thực tế, chi phí trung bình cũng không cao lắm, trừ khi biến động được sử dụng trong một vòng lặp chặt chẽ.
chối cãi

2
Bài viết này trên infoq ( infoq.com/articles/memory_barriers_jvm_concurrency ) cũng có thể khiến bạn quan tâm, nó cho thấy tác động của sự biến động và đồng bộ hóa trên mã được tạo cho các kiến ​​trúc khác nhau. Đây cũng là một trường hợp mà jvm có thể hoạt động tốt hơn trình biên dịch đi trước, vì nó biết liệu nó có đang chạy trên hệ thống đơn xử lý hay không và có thể bỏ qua một số rào cản bộ nhớ.
Jörn Horstmann

Câu trả lời:


123

Trên Intel, một giá trị biến động không có đối thủ khá rẻ. Nếu chúng ta xem xét trường hợp đơn giản sau:

public static long l;

public static void run() {        
    if (l == -1)
        System.exit(-1);

    if (l == -2)
        System.exit(-1);
}

Sử dụng khả năng của Java 7 để in mã lắp ráp, phương thức chạy trông giống như sau:

# {method} 'run2' '()V' in 'Test2'
#           [sp+0x10]  (sp of caller)
0xb396ce80: mov    %eax,-0x3000(%esp)
0xb396ce87: push   %ebp
0xb396ce88: sub    $0x8,%esp          ;*synchronization entry
                                    ; - Test2::run2@-1 (line 33)
0xb396ce8e: mov    $0xffffffff,%ecx
0xb396ce93: mov    $0xffffffff,%ebx
0xb396ce98: mov    $0x6fa2b2f0,%esi   ;   {oop('Test2')}
0xb396ce9d: mov    0x150(%esi),%ebp
0xb396cea3: mov    0x154(%esi),%edi   ;*getstatic l
                                    ; - Test2::run@0 (line 33)
0xb396cea9: cmp    %ecx,%ebp
0xb396ceab: jne    0xb396ceaf
0xb396cead: cmp    %ebx,%edi
0xb396ceaf: je     0xb396cece         ;*getstatic l
                                    ; - Test2::run@14 (line 37)
0xb396ceb1: mov    $0xfffffffe,%ecx
0xb396ceb6: mov    $0xffffffff,%ebx
0xb396cebb: cmp    %ecx,%ebp
0xb396cebd: jne    0xb396cec1
0xb396cebf: cmp    %ebx,%edi
0xb396cec1: je     0xb396ceeb         ;*return
                                    ; - Test2::run@28 (line 40)
0xb396cec3: add    $0x8,%esp
0xb396cec6: pop    %ebp
0xb396cec7: test   %eax,0xb7732000    ;   {poll_return}
;... lines removed

Nếu bạn nhìn vào 2 tham chiếu để getstatic, tham chiếu đầu tiên liên quan đến tải từ bộ nhớ, tham chiếu thứ hai bỏ qua tải vì giá trị được sử dụng lại từ (các) thanh ghi mà nó đã được tải vào (dài là 64 bit và trên máy tính xách tay 32 bit của tôi nó sử dụng 2 thanh ghi).

Nếu chúng ta làm cho biến l dễ bay hơi thì lắp ráp kết quả sẽ khác.

# {method} 'run2' '()V' in 'Test2'
#           [sp+0x10]  (sp of caller)
0xb3ab9340: mov    %eax,-0x3000(%esp)
0xb3ab9347: push   %ebp
0xb3ab9348: sub    $0x8,%esp          ;*synchronization entry
                                    ; - Test2::run2@-1 (line 32)
0xb3ab934e: mov    $0xffffffff,%ecx
0xb3ab9353: mov    $0xffffffff,%ebx
0xb3ab9358: mov    $0x150,%ebp
0xb3ab935d: movsd  0x6fb7b2f0(%ebp),%xmm0  ;   {oop('Test2')}
0xb3ab9365: movd   %xmm0,%eax
0xb3ab9369: psrlq  $0x20,%xmm0
0xb3ab936e: movd   %xmm0,%edx         ;*getstatic l
                                    ; - Test2::run@0 (line 32)
0xb3ab9372: cmp    %ecx,%eax
0xb3ab9374: jne    0xb3ab9378
0xb3ab9376: cmp    %ebx,%edx
0xb3ab9378: je     0xb3ab93ac
0xb3ab937a: mov    $0xfffffffe,%ecx
0xb3ab937f: mov    $0xffffffff,%ebx
0xb3ab9384: movsd  0x6fb7b2f0(%ebp),%xmm0  ;   {oop('Test2')}
0xb3ab938c: movd   %xmm0,%ebp
0xb3ab9390: psrlq  $0x20,%xmm0
0xb3ab9395: movd   %xmm0,%edi         ;*getstatic l
                                    ; - Test2::run@14 (line 36)
0xb3ab9399: cmp    %ecx,%ebp
0xb3ab939b: jne    0xb3ab939f
0xb3ab939d: cmp    %ebx,%edi
0xb3ab939f: je     0xb3ab93ba         ;*return
;... lines removed

Trong trường hợp này, cả hai tham chiếu getstatic đến biến l đều liên quan đến tải từ bộ nhớ, tức là giá trị không thể được giữ trong một thanh ghi qua nhiều lần đọc biến động. Để đảm bảo rằng có một lần đọc nguyên tử, giá trị được đọc từ bộ nhớ chính vào một thanh ghi MMX movsd 0x6fb7b2f0(%ebp),%xmm0làm cho hoạt động đọc trở thành một lệnh duy nhất (từ ví dụ trước, chúng ta đã thấy rằng giá trị 64 bit thông thường sẽ yêu cầu hai lần đọc 32 bit trên hệ thống 32 bit).

Vì vậy, chi phí tổng thể của một lần đọc biến động sẽ gần tương đương với tải bộ nhớ và có thể rẻ như truy cập bộ nhớ đệm L1. Tuy nhiên, nếu một lõi khác đang ghi vào biến biến động, dòng bộ đệm sẽ bị vô hiệu yêu cầu bộ nhớ chính hoặc có thể là truy cập bộ đệm L3. Chi phí thực tế sẽ phụ thuộc nhiều vào kiến ​​trúc CPU. Ngay cả giữa Intel và AMD, các giao thức đồng tiền trong bộ nhớ cache cũng khác nhau.


lưu ý phụ, java 6 có khả năng tương tự để hiển thị lắp ráp (đó là điểm nóng nào đó)
bestsss

+1 Trong JDK5 dễ bay hơi không thể được sắp xếp lại đối với bất kỳ lần đọc / ghi nào (ví dụ: sửa lỗi khóa kiểm tra kỹ). Điều đó có ngụ ý rằng nó cũng sẽ ảnh hưởng đến cách thao tác các trường không biến động không? Sẽ rất thú vị khi kết hợp quyền truy cập vào các trường biến động và không biến động.
ewernli

@evemli, bạn cần phải cẩn thận, tôi đã tự đưa ra tuyên bố này một lần, nhưng bị phát hiện là không chính xác. Có một trường hợp cạnh. Mô hình bộ nhớ Java cho phép chuyển đổi ngữ nghĩa nhà nghỉ, khi các cửa hàng có thể được sắp xếp lại trước các cửa hàng dễ bay hơi. Nếu bạn chọn điều này từ bài báo của Brian Goetz trên trang IBM, thì điều đáng nói là bài viết này đơn giản hóa đặc tả JMM.
Michael Barker

20

Nói chung, trên hầu hết các bộ vi xử lý hiện đại, tải dễ bay hơi có thể so sánh với tải bình thường. Một cửa hàng dễ bay hơi bằng khoảng 1/3 thời gian của một lần nhập / theo dõi-thoát. Điều này được thấy trên các hệ thống có bộ nhớ cache nhất quán.

Để trả lời câu hỏi của OP, các bài viết biến động rất đắt trong khi các bài đọc thường thì không.

Điều này có nghĩa là các hoạt động đọc biến động có thể được thực hiện mà không có sự vô hiệu hóa bộ nhớ cache rõ ràng trên x86 và nhanh như đọc biến bình thường (bỏ qua những mâu thuẫn sắp xếp lại của biến đổi)?

Đúng vậy, đôi khi khi xác thực một trường, CPU thậm chí có thể không đánh trúng bộ nhớ chính, thay vào đó là do thám các bộ nhớ đệm luồng khác và lấy giá trị từ đó (giải thích rất chung chung).

Tuy nhiên, tôi thứ hai gợi ý của Neil rằng nếu bạn có một trường được nhiều luồng truy cập, bạn nên bọc nó dưới dạng AtomicReference. Là một AtomicReference, nó thực thi cùng một thông lượng để đọc / ghi nhưng rõ ràng hơn là trường sẽ được truy cập và sửa đổi bởi nhiều luồng.

Chỉnh sửa để trả lời Chỉnh sửa của OP:

Tính kết hợp của bộ nhớ đệm là một giao thức phức tạp, nhưng nói tóm lại: CPU sẽ chia sẻ một dòng bộ đệm chung được gắn vào bộ nhớ chính. Nếu một CPU tải bộ nhớ và không có CPU nào khác có bộ nhớ đó thì CPU đó sẽ cho rằng đó là giá trị cập nhật nhất. Nếu một CPU khác cố gắng tải cùng một vị trí bộ nhớ thì CPU đã được tải sẽ nhận thức được điều này và thực sự chia sẻ tham chiếu được lưu trong bộ nhớ cache đến CPU yêu cầu - bây giờ CPU yêu cầu có bản sao của bộ nhớ đó trong bộ nhớ cache của CPU. (Nó không bao giờ phải tìm trong bộ nhớ chính để tham khảo)

Có khá nhiều giao thức liên quan nhưng điều này cho ta ý tưởng về những gì đang diễn ra. Ngoài ra, để trả lời câu hỏi khác của bạn, với sự vắng mặt của nhiều bộ xử lý, việc đọc / ghi biến động trên thực tế có thể nhanh hơn với nhiều bộ xử lý. Có một số ứng dụng trên thực tế sẽ chạy nhanh hơn đồng thời với một CPU sau đó là nhiều.


5
AtomicReference chỉ là một trình bao bọc cho một trường dễ bay hơi với các chức năng gốc được bổ sung cung cấp chức năng bổ sung như getAndSet, so sánhAndSet, v.v., do đó, từ quan điểm hiệu suất, việc sử dụng nó chỉ hữu ích nếu bạn cần thêm chức năng. Nhưng không hiểu sao bạn lại tham khảo HĐH ở đây? Chức năng được triển khai trực tiếp trong các mã quang CPU. Và điều này có ngụ ý rằng trên các hệ thống đa bộ xử lý, nơi một CPU không biết gì về nội dung bộ nhớ đệm của các CPU khác bay hơi chậm hơn vì CPU luôn phải sử dụng bộ nhớ chính?
Daniel

Bạn nói đúng, tôi đã bỏ lỡ cuộc nói chuyện về hệ điều hành nên viết CPU, sửa lỗi đó ngay bây giờ. Và vâng, tôi biết AtomicReference chỉ đơn giản là một trình bao bọc cho các trường dễ bay hơi nhưng nó cũng thêm vào như một loại tài liệu mà bản thân trường sẽ được nhiều luồng truy cập.
John Vint

@John, tại sao bạn lại thêm một hướng khác thông qua AtomicReference? Nếu bạn cần CAS - ok, nhưng AtomicUpdater có thể là một lựa chọn tốt hơn. Theo như tôi nhớ thì không có bản chất nào về AtomicReference.
bestsss

@bestsss Đối với tất cả các mục đích chung, bạn đúng là không có sự khác biệt giữa AtomicReference.set / get và tải và cửa hàng dễ bay hơi. Điều đó được cho là tôi có cùng cảm giác (và ở một mức độ nào đó) về thời điểm sử dụng cái nào. Phản hồi này có thể trình bày chi tiết một chút stackoverflow.com/questions/3964317/… . Sử dụng một trong hai là một ưu tiên hơn, lập luận duy nhất của tôi về việc sử dụng AtomicReference trên một biến thể đơn giản là để có tài liệu rõ ràng - bản thân điều đó cũng không tạo ra lập luận lớn nhất mà tôi hiểu
John Vint

Bên cạnh đó, một số người cho rằng việc sử dụng trường dễ bay hơi / AtomicReference (không cần CAS) dẫn đến mã lỗi old.nabble.com/…
John Vint

12

Theo cách diễn đạt của Mô hình bộ nhớ Java (như được định nghĩa cho Java 5+ trong JSR 133), bất kỳ thao tác nào - đọc hoặc ghi - trên một volatilebiến đều tạo ra mối quan hệ xảy ra trước đối với bất kỳ thao tác nào khác trên cùng một biến. Điều này có nghĩa là trình biên dịch và JIT buộc phải tránh một số tối ưu nhất định như sắp xếp lại các hướng dẫn trong luồng hoặc chỉ thực hiện các hoạt động trong bộ nhớ cache cục bộ.

Vì một số tính năng tối ưu không có sẵn, nên mã kết quả nhất thiết phải chậm hơn so với trước đây, mặc dù có lẽ không nhiều lắm.

Tuy nhiên, bạn không nên tạo một biến volatiletrừ khi bạn biết rằng nó sẽ được truy cập từ nhiều luồng bên ngoài các synchronizedkhối. Thậm chí sau đó bạn nên xem xét liệu dễ bay hơi là sự lựa chọn tốt nhất so với synchronized, AtomicReferencevà bạn bè của mình, rõ ràng Locklớp học, vv


4

Việc truy cập một biến dễ thay đổi theo nhiều cách tương tự như việc bao bọc quyền truy cập vào một biến thông thường trong một khối được đồng bộ hóa. Ví dụ: quyền truy cập vào một biến dễ thay đổi ngăn không cho CPU sắp xếp lại các lệnh trước và sau khi truy cập và điều này thường làm chậm quá trình thực thi (mặc dù tôi không thể nói rõ là bao nhiêu).

Nói chung hơn, trên hệ thống đa bộ xử lý, tôi không thấy cách nào có thể thực hiện việc truy cập vào một biến biến động mà không bị phạt - phải có một số cách để đảm bảo một lần ghi trên bộ xử lý A sẽ được đồng bộ hóa với một lần đọc trên bộ xử lý B.


4
Việc đọc các biến dễ bay hơi có cùng hình phạt so với việc nhập theo dõi, liên quan đến khả năng sắp xếp lại các lệnh, trong khi việc viết một biến dễ bay hơi tương đương với một lần ra theo dõi. Sự khác biệt có thể là biến nào (ví dụ như bộ nhớ đệm của bộ xử lý) bị xóa hoặc mất hiệu lực. Trong khi đồng bộ hóa làm mất hiệu lực hoặc làm mất hiệu lực của mọi thứ, quyền truy cập vào biến biến động phải luôn được bỏ qua bộ nhớ cache.
Daniel

12
-1, Việc truy cập một biến biến động hơi khác một chút so với việc sử dụng một khối đồng bộ. Việc nhập một khối được đồng bộ hóa yêu cầu một bản ghi dựa trên so sánh nguyên tử để lấy ra khóa và một bản ghi dễ bay hơi để giải phóng nó. Nếu khóa được thỏa mãn thì điều khiển phải truyền từ không gian người dùng sang không gian hạt nhân để phân xử khóa (đây là bit đắt tiền). Việc truy cập một biến động sẽ luôn ở trong không gian người dùng.
Michael Barker

@MichaelBarker: Bạn có chắc chắn rằng tất cả các màn hình phải được bảo vệ bởi hạt nhân chứ không phải ứng dụng?
Daniel

@Daniel: Nếu bạn đại diện cho một màn hình bằng cách sử dụng một khối được đồng bộ hóa hoặc một Khóa thì có, nhưng chỉ khi màn hình đó có nội dung. Cách duy nhất để làm điều này mà không cần phân xử hạt nhân là sử dụng cùng một logic, nhưng quay bận thay vì dừng luồng.
Michael Barker

@MichaelBarker: Okey, đối với các ổ khóa có nội dung, tôi hiểu điều này.
Daniel
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.