ByteBuffer.allocate () so với ByteBuffer.allocateDirect ()


144

Đến allocate()hay allocateDirect(), đó là câu hỏi.

Trong một số năm nay, tôi đã bị mắc kẹt với suy nghĩ rằng vì DirectByteBuffernó là ánh xạ bộ nhớ trực tiếp ở cấp hệ điều hành, nên nó sẽ thực hiện nhanh hơn với các cuộc gọi get / put hơn HeapByteBuffers. Tôi chưa bao giờ thực sự quan tâm đến việc tìm hiểu các chi tiết chính xác liên quan đến tình hình cho đến bây giờ. Tôi muốn biết loại nào trong hai loại ByteBuffernhanh hơn và với điều kiện gì.


Để đưa ra một câu trả lời cụ thể, bạn cần nói cụ thể những gì bạn đang làm với họ. Nếu cái này luôn nhanh hơn cái kia, tại sao lại có hai biến thể. Có lẽ bạn có thể mở rộng lý do tại sao bây giờ bạn "thực sự quan tâm đến việc tìm hiểu chi tiết chính xác" BTW: Bạn đã đọc mã, đặc biệt cho DirectByteBuffer chưa?
Peter Lawrey

Chúng sẽ được sử dụng để đọc và ghi vào SocketChannels được cấu hình để không chặn. Vì vậy, liên quan đến những gì @bmargulies nói, DirectByteBuffers sẽ hoạt động nhanh hơn cho các kênh.

@Gnarly Ít nhất phiên bản hiện tại của câu trả lời của tôi nói rằng các kênh dự kiến ​​sẽ có lợi.
bmargulies

Câu trả lời:


150

Ron Hitches trong cuốn sách tuyệt vời Java NIO của mình dường như cung cấp những gì tôi nghĩ có thể là một câu trả lời tốt cho câu hỏi của bạn:

Hệ điều hành thực hiện các thao tác I / O trên vùng nhớ. Các vùng nhớ này, theo như hệ điều hành có liên quan, là các chuỗi byte liền kề nhau. Không có gì ngạc nhiên khi chỉ có bộ đệm byte mới đủ điều kiện tham gia vào các hoạt động I / O. Cũng nhắc lại rằng hệ điều hành sẽ truy cập trực tiếp vào không gian địa chỉ của quy trình, trong trường hợp này là quy trình JVM, để truyền dữ liệu. Điều này có nghĩa là các vùng nhớ là mục tiêu của các chuỗi I / O phải là các chuỗi byte liền kề nhau. Trong JVM, một mảng byte có thể không được lưu trữ liên tục trong bộ nhớ hoặc Trình thu gom rác có thể di chuyển nó bất cứ lúc nào. Mảng là các đối tượng trong Java và cách dữ liệu được lưu trữ bên trong đối tượng đó có thể thay đổi từ một triển khai JVM khác.

Vì lý do này, khái niệm về một bộ đệm trực tiếp đã được giới thiệu. Bộ đệm trực tiếp được dự định để tương tác với các kênh và các thói quen I / O gốc. Họ nỗ lực hết sức để lưu trữ các phần tử byte trong vùng nhớ mà kênh có thể sử dụng để truy cập trực tiếp hoặc thô, bằng cách sử dụng mã gốc để báo cho hệ điều hành thoát hoặc lấp đầy vùng nhớ trực tiếp.

Bộ đệm byte trực tiếp thường là lựa chọn tốt nhất cho các hoạt động I / O. Theo thiết kế, chúng hỗ trợ cơ chế I / O hiệu quả nhất có sẵn cho JVM. Bộ đệm byte không gián tiếp có thể được chuyển đến các kênh, nhưng làm như vậy có thể phải chịu một hình phạt hiệu suất. Thông thường, bộ đệm không gián tiếp thường là mục tiêu của hoạt động I / O gốc. Nếu bạn chuyển một đối tượng ByteBuffer không trực tiếp sang một kênh để ghi, kênh có thể ngầm thực hiện các thao tác sau trên mỗi cuộc gọi:

  1. Tạo một đối tượng ByteBuffer trực tiếp tạm thời.
  2. Sao chép nội dung của bộ đệm không gián tiếp vào bộ đệm tạm thời.
  3. Thực hiện thao tác I / O mức thấp bằng cách sử dụng bộ đệm tạm thời.
  4. Đối tượng bộ đệm tạm thời đi ra khỏi phạm vi và cuối cùng là rác được thu thập.

Điều này có khả năng dẫn đến việc sao chép bộ đệm và khởi động đối tượng trên mỗi I / O, đó chính xác là những điều chúng ta muốn tránh. Tuy nhiên, tùy thuộc vào việc thực hiện, mọi thứ có thể không tệ như vậy. Thời gian chạy sẽ có khả năng lưu trữ và sử dụng lại bộ đệm trực tiếp hoặc thực hiện các thủ thuật thông minh khác để tăng thông lượng. Nếu bạn chỉ đơn giản là tạo một bộ đệm để sử dụng một lần, sự khác biệt là không đáng kể. Mặt khác, nếu bạn sẽ sử dụng bộ đệm nhiều lần trong một kịch bản hiệu suất cao, tốt hơn hết bạn nên phân bổ bộ đệm trực tiếp và sử dụng lại chúng.

Bộ đệm trực tiếp là tối ưu cho I / O, nhưng chúng có thể tốn kém hơn để tạo hơn bộ đệm byte không gián tiếp. Bộ nhớ được sử dụng bởi các bộ đệm trực tiếp được phân bổ bằng cách gọi qua mã riêng, của hệ điều hành, bỏ qua heap JVM tiêu chuẩn. Thiết lập và phá bỏ bộ đệm trực tiếp có thể tốn kém hơn đáng kể so với bộ đệm thường trú, tùy thuộc vào hệ điều hành máy chủ và triển khai JVM. Các vùng lưu trữ bộ nhớ của bộ đệm trực tiếp không phải là bộ sưu tập rác vì chúng nằm ngoài heap JVM tiêu chuẩn.

Sự đánh đổi hiệu năng của việc sử dụng bộ đệm trực tiếp so với không trực tiếp có thể khác nhau tùy theo JVM, hệ điều hành và thiết kế mã. Bằng cách phân bổ bộ nhớ ngoài heap, bạn có thể khiến ứng dụng của mình chịu các lực bổ sung mà JVM không biết. Khi đưa các bộ phận chuyển động bổ sung vào chơi, hãy đảm bảo rằng bạn đang đạt được hiệu quả mong muốn. Tôi khuyên bạn nên châm ngôn phần mềm cũ: đầu tiên làm cho nó hoạt động, sau đó làm cho nó nhanh. Đừng lo lắng quá nhiều về việc tối ưu hóa trước; tập trung đầu tiên vào sự đúng đắn. Việc triển khai JVM có thể có thể thực hiện bộ đệm ẩn hoặc các tối ưu hóa khác sẽ cung cấp cho bạn hiệu năng bạn cần mà không cần nhiều nỗ lực không cần thiết từ phía bạn.


9
Tôi không thích câu nói đó vì nó chứa quá nhiều phỏng đoán. Ngoài ra, JVM chắc chắn không cần phân bổ ByteBuffer trực tiếp khi thực hiện IO cho ByteBuffer không trực tiếp: đủ để malloc một chuỗi byte trên heap, thực hiện IO, sao chép từ byte sang ByteBuffer và giải phóng byte. Những khu vực thậm chí có thể được lưu trữ. Nhưng việc phân bổ một đối tượng Java cho việc này là hoàn toàn không cần thiết. Câu trả lời thực sự sẽ chỉ có được từ việc đo lường. Lần trước tôi đã làm các phép đo không có sự khác biệt đáng kể. Tôi sẽ phải làm lại các bài kiểm tra để đưa ra tất cả các chi tiết cụ thể.
Robert Klemme

4
Một câu hỏi đặt ra là liệu một cuốn sách mô tả NIO (và các hoạt động bản địa) có thể có những điều chắc chắn trong đó không. Xét cho cùng, các JVM và hệ điều hành khác nhau quản lý mọi thứ khác nhau, vì vậy tác giả không thể đổ lỗi cho việc không thể đảm bảo hành vi nhất định.
Martin Tuskevicius

@RobertKlemme, +1, tất cả chúng ta đều ghét phỏng đoán, tuy nhiên, có thể không thể đo lường hiệu suất cho tất cả các HĐH chính, vì có quá nhiều HĐH chính. Một bài đăng khác đã cố gắng, nhưng chúng ta có thể thấy nhiều vấn đề với điểm chuẩn của nó, bắt đầu với "kết quả dao động rộng rãi tùy thuộc vào HĐH". Ngoài ra, nếu có một con cừu đen làm những thứ khủng khiếp như sao chép bộ đệm trên mỗi I / O thì sao? Sau đó vì con cừu đó, chúng tôi có thể buộc phải ngăn chặn việc viết mã mà chúng tôi sẽ sử dụng, chỉ để tránh những tình huống xấu nhất này.
Pacerier

@RobertKlemme tôi đồng ý. Có quá nhiều phỏng đoán ở đây. Ví dụ, JVM không thể phân bổ các mảng byte một cách thưa thớt.
Hầu tước Lorne

@Edwin Dalorzo: Tại sao chúng ta cần bộ đệm byte như vậy trong thế giới thực? Có phải họ được phát minh như một hack để chia sẻ bộ nhớ giữa quá trình? Ví dụ, giả sử JVM chạy trên một quy trình và đó sẽ là một quy trình khác chạy trên mạng hoặc lớp liên kết dữ liệu - chịu trách nhiệm truyền dữ liệu - các bộ đệm byte này có được phân bổ để chia sẻ bộ nhớ giữa các quy trình này không? Xin hãy sửa tôi nếu tôi sai ..
Tom Taylor

25

Không có lý do để mong đợi bộ đệm trực tiếp sẽ nhanh hơn để truy cập bên trong jvm. Lợi thế của chúng đến khi bạn chuyển chúng sang mã gốc - chẳng hạn như mã đằng sau các loại kênh.


Thật. Giống như, khi cần thực hiện IO trong Scala / Java và gọi Python / libs nhúng với dữ liệu bộ nhớ lớn để xử lý thuật toán hoặc cung cấp dữ liệu trực tiếp cho GPU trong Tensorflow.
SemanticBeeng

21

vì DirectByteBuffers là ánh xạ bộ nhớ trực tiếp ở cấp hệ điều hành

Họ không. Chúng chỉ là bộ nhớ xử lý ứng dụng bình thường, nhưng không phải di chuyển trong Java GC, điều này giúp đơn giản hóa mọi thứ bên trong lớp JNI một cách đáng kể. Những gì bạn mô tả áp dụng cho MappedByteBuffer.

rằng nó sẽ thực hiện nhanh hơn với các cuộc gọi get / put

Kết luận không tuân theo nguyên tắc; tiền đề là sai; và kết luận cũng sai. Chúng nhanh hơn một khi bạn vào bên trong lớp JNI và nếu bạn đang đọc và viết từ cùng DirectByteBufferthì chúng sẽ nhanh hơn nhiều , vì dữ liệu không bao giờ phải vượt qua ranh giới JNI cả.


7
Đây là một điểm tốt và quan trọng: trên đường dẫn của IO, bạn phải vượt qua biên giới Java - JNI tại một số điểm. Bộ đệm byte trực tiếp và không trực tiếp chỉ di chuyển đường viền: với bộ đệm trực tiếp, tất cả các hoạt động đặt từ đất Java phải giao nhau, trong khi với bộ đệm không trực tiếp, tất cả các hoạt động IO phải giao nhau. Những gì nhanh hơn phụ thuộc vào ứng dụng.
Robert Klemme

@RobertKlemme Tóm tắt của bạn không chính xác. Với tất cả các bộ đệm, mọi dữ liệu đến và từ Java đều phải vượt qua ranh giới JNI. Điểm quan trọng của bộ đệm trực tiếp là nếu bạn chỉ sao chép dữ liệu từ kênh này sang kênh khác, ví dụ như tải lên một tệp, bạn hoàn toàn không phải đưa nó vào Java, nhanh hơn nhiều.
Hầu tước Lorne

chính xác thì tóm tắt của tôi ở đâu? Và "tóm tắt" để bắt đầu với? Tôi đã nói rõ ràng về "đưa các hoạt động từ đất Java". Nếu bạn chỉ sao chép dữ liệu xung quanh giữa các Kênh (nghĩa là không bao giờ phải xử lý dữ liệu trong vùng đất Java) thì đó là một câu chuyện khác.
Robert Klemme

@RobertKlemme Tuyên bố của bạn rằng 'với bộ đệm trực tiếp [chỉ] tất cả các thao tác đặt từ đất Java phải giao nhau' là không chính xác. Cả hai được và đặt phải vượt qua.
Hầu tước Lorne

EJP, rõ ràng bạn vẫn còn thiếu sự phân biệt dự định @RobertKlemme đang thực hiện bằng cách chọn sử dụng các từ "đưa hoạt động" vào một cụm từ và sử dụng các từ "Hoạt động IO" trong cụm từ trái ngược của câu. Trong cụm từ sau, ý định của anh là đề cập đến các hoạt động giữa bộ đệm và một loại thiết bị do hệ điều hành cung cấp.
naki

18

Tốt nhất để làm số đo của riêng bạn. Câu trả lời nhanh dường như là việc gửi từ allocateDirect()bộ đệm mất ít thời gian hơn 25% đến 75% so với allocate()biến thể (được thử nghiệm là sao chép tệp vào / dev / null), tùy thuộc vào kích thước, nhưng bản thân việc phân bổ có thể chậm hơn đáng kể (thậm chí bởi hệ số 100x).

Nguồn:


Cảm ơn. Tôi sẽ chấp nhận câu trả lời của bạn nhưng tôi đang tìm kiếm một số chi tiết cụ thể hơn về sự khác biệt trong hiệu suất.
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.