ByteBuffer.allocate () กับ ByteBuffer.allocateDirect ()


144

ถึงallocate()หรือallocateDirect()นั่นคือคำถาม

เป็นเวลาหลายปีตอนนี้ฉันติดอยู่เพียงแค่ความคิดที่ว่าตั้งแต่DirectByteBuffers มีการทำแผนที่หน่วยความจำโดยตรงในระดับ OS, ว่าจะดำเนินการได้เร็วขึ้นด้วยการรับ / วางสายกว่าHeapByteBuffers ฉันไม่เคยสนใจที่จะหารายละเอียดที่แน่นอนเกี่ยวกับสถานการณ์จนถึงปัจจุบัน ฉันต้องการทราบว่าByteBuffers สองประเภทใดที่เร็วและเงื่อนไขอะไร


ในการให้คำตอบเฉพาะคุณต้องพูดโดยเฉพาะสิ่งที่คุณทำกับพวกเขา หากหนึ่งเร็วกว่าอื่นเสมอทำไมจะมีสองรุ่น บางทีคุณสามารถขยายเหตุผลว่าทำไมคุณถึงตอนนี้ "สนใจจริงๆในการหารายละเอียดที่แน่นอน" BTW: คุณอ่านรหัสแล้วหรือยังสำหรับ DirectByteBuffer?
Peter Lawrey

พวกเขาจะใช้ในการอ่านและเขียนถึงSocketChannels ที่มีการกำหนดค่าสำหรับการไม่บล็อก ดังนั้นเกี่ยวกับสิ่งที่ @bmargulies พูดว่าDirectByteBuffers จะทำงานได้เร็วขึ้นสำหรับช่อง

@Gnarly อย่างน้อยเวอร์ชันปัจจุบันของคำตอบของฉันบอกว่าช่องทางคาดว่าจะได้รับประโยชน์
bmargulies

คำตอบ:


150

Ron Hitches ในหนังสือที่ยอดเยี่ยมของเขาJava NIOดูเหมือนจะเสนอสิ่งที่ฉันคิดว่าอาจเป็นคำตอบที่ดีสำหรับคำถามของคุณ:

ระบบปฏิบัติการทำการดำเนินงาน I / O ในพื้นที่หน่วยความจำ พื้นที่หน่วยความจำเหล่านี้เท่าที่เกี่ยวข้องกับระบบปฏิบัติการเป็นลำดับที่ต่อเนื่องกันของไบต์ ไม่น่าแปลกใจเลยที่บัฟเฟอร์ไบต์เท่านั้นที่มีสิทธิ์เข้าร่วมในการดำเนินการ I / O ยังจำได้ว่าระบบปฏิบัติการจะเข้าถึงพื้นที่ที่อยู่ของกระบวนการโดยตรงในกรณีนี้กระบวนการ JVM เพื่อถ่ายโอนข้อมูล ซึ่งหมายความว่าพื้นที่หน่วยความจำที่เป็นเป้าหมายของ I / O perations จะต้องต่อเนื่องกันเป็นไบต์ ใน JVM อาร์เรย์ของไบต์อาจไม่ถูกจัดเก็บอย่างต่อเนื่องในหน่วยความจำหรือ Garbage Collector สามารถย้ายได้ตลอดเวลา อาร์เรย์เป็นวัตถุใน Java และวิธีการจัดเก็บข้อมูลภายในวัตถุนั้นอาจแตกต่างจากการใช้ JVM หนึ่งไปยังอีก

ด้วยเหตุผลนี้จึงมีการแนะนำแนวคิดของบัฟเฟอร์โดยตรง บัฟเฟอร์โดยตรงมีไว้สำหรับการโต้ตอบกับช่องสัญญาณและรูทีน I / O ดั้งเดิม พวกเขาใช้ความพยายามอย่างดีที่สุดในการจัดเก็บองค์ประกอบไบต์ในพื้นที่หน่วยความจำที่ช่องสามารถใช้สำหรับการเข้าถึงโดยตรงหรือแบบดิบโดยใช้รหัสเนทีฟเพื่อบอกระบบปฏิบัติการให้ระบายหรือเติมพื้นที่หน่วยความจำโดยตรง

บัฟเฟอร์ไบต์โดยตรงมักเป็นตัวเลือกที่ดีที่สุดสำหรับการดำเนินการ I / O โดยการออกแบบพวกเขาสนับสนุนกลไก I / O ที่มีประสิทธิภาพสูงสุดที่มีให้กับ JVM บัฟเฟอร์แบบไม่ระบุทิศทางสามารถส่งผ่านไปยังแชนเนลได้ แต่การทำเช่นนั้นอาจส่งผลให้เกิดการปรับประสิทธิภาพ ปกติแล้วมันเป็นไปไม่ได้ที่บัฟเฟอร์แบบไม่มีทิศทางจะเป็นเป้าหมายของการดำเนินการ I / O แบบดั้งเดิม หากคุณส่งผ่านวัตถุ ByteBuffer แบบ nondirect ไปยังช่องสัญญาณเพื่อเขียนช่องทางนั้นอาจทำสิ่งต่อไปนี้ในการโทรแต่ละครั้งโดยปริยาย:

  1. สร้างวัตถุ ByteBuffer โดยตรงชั่วคราว
  2. คัดลอกเนื้อหาของบัฟเฟอร์แบบไม่เปลี่ยนทิศทางไปยังบัฟเฟอร์ชั่วคราว
  3. ดำเนินการ I / O ระดับต่ำโดยใช้บัฟเฟอร์ชั่วคราว
  4. วัตถุบัฟเฟอร์ชั่วคราวออกนอกขอบเขตและในที่สุดก็มีการรวบรวมขยะ

สิ่งนี้อาจส่งผลให้เกิดการคัดลอกบัฟเฟอร์และวัตถุปั่นป่วนในทุก I / O ซึ่งเป็นสิ่งที่เราต้องการหลีกเลี่ยง อย่างไรก็ตามขึ้นอยู่กับการใช้งานสิ่งต่าง ๆ อาจไม่เลวร้ายขนาดนี้ รันไทม์จะแคชและนำบัฟเฟอร์โดยตรงมาใช้ใหม่หรือใช้กลอุบายที่ฉลาดอื่น ๆ เพื่อเพิ่มปริมาณงาน หากคุณเพียงแค่สร้างบัฟเฟอร์สำหรับใช้ครั้งเดียวความแตกต่างก็ไม่สำคัญ ในทางกลับกันถ้าคุณจะใช้บัฟเฟอร์ซ้ำ ๆ ในสถานการณ์ที่มีประสิทธิภาพสูงคุณควรจัดสรรบัฟเฟอร์โดยตรงแล้วนำกลับมาใช้ใหม่

บัฟเฟอร์โดยตรงเหมาะสมที่สุดสำหรับ I / O แต่มันอาจมีราคาแพงกว่าการสร้างมากกว่าบัฟเฟอร์แบบไม่ระบุทิศทาง หน่วยความจำที่ใช้โดยบัฟเฟอร์โดยตรงได้รับการจัดสรรโดยการเรียกผ่านรหัสดั้งเดิมของระบบปฏิบัติการโดยผ่านฮีป JVM มาตรฐาน การตั้งค่าและการแยกบัฟเฟอร์โดยตรงอาจมีราคาแพงกว่าบัฟเฟอร์ฮีพแบบอาศัยอย่างมากทั้งนี้ขึ้นอยู่กับระบบปฏิบัติการโฮสต์และการนำ JVM มาใช้ พื้นที่จัดเก็บหน่วยความจำของบัฟเฟอร์โดยตรงไม่อยู่ภายใต้การรวบรวมขยะเนื่องจากอยู่นอกฮีป JVM มาตรฐาน

การแลกเปลี่ยนประสิทธิภาพของการใช้บัฟเฟอร์ Direct กับ nondirect อาจแตกต่างกันไปตาม JVM ระบบปฏิบัติการและการออกแบบรหัส โดยการจัดสรรหน่วยความจำภายนอกฮีปคุณอาจทำให้แอปพลิเคชันของคุณมีการบังคับใช้เพิ่มเติมที่ JVM ไม่รู้จัก เมื่อนำชิ้นส่วนที่เคลื่อนไหวเพิ่มเข้ามาในการเล่นตรวจสอบให้แน่ใจว่าคุณได้รับเอฟเฟกต์ที่ต้องการ ฉันขอแนะนำ maxim ซอฟต์แวร์เก่า: ก่อนอื่นให้ใช้งานได้แล้วทำให้เร็วขึ้น อย่ากังวลมากเกินไปเกี่ยวกับการปรับให้เหมาะสมล่วงหน้า มีสมาธิครั้งแรกกับความถูกต้อง การใช้งาน JVM อาจสามารถทำการแคชบัฟเฟอร์หรือการเพิ่มประสิทธิภาพอื่น ๆ ที่จะให้ประสิทธิภาพที่คุณต้องการโดยไม่ต้องใช้ความพยายามที่ไม่จำเป็นในส่วนของคุณ


9
ฉันไม่ชอบคำพูดนั้นเพราะมันมีการเดามากเกินไป นอกจากนี้ JVM ไม่จำเป็นต้องจัดสรร ByteBuffer โดยตรงเมื่อทำการ IO สำหรับ ByteBuffer ที่ไม่ใช่ direct: มันเพียงพอที่จะ malloc ลำดับของไบต์บน heap ทำ IO คัดลอกจากไบต์ไปยัง ByteBuffer และปล่อย bytes พื้นที่เหล่านั้นอาจถูกแคช แต่มันไม่จำเป็นเลยที่จะจัดสรรวัตถุ Java ให้กับสิ่งนี้ คำตอบที่แท้จริงจะได้รับจากการวัดเท่านั้น ครั้งล่าสุดที่ฉันทำการวัดไม่มีความแตกต่างที่สำคัญ ฉันจะต้องทำการทดสอบซ้ำเพื่อให้ได้รายละเอียดเฉพาะทั้งหมด
Robert Klemme

4
เป็นเรื่องที่น่าสงสัยหากหนังสือที่อธิบาย NIO (และการดำเนินงานดั้งเดิม) สามารถมีความมั่นใจได้ ท้ายที่สุด JVM และระบบปฏิบัติการที่แตกต่างกันจะจัดการสิ่งต่าง ๆ ดังนั้นผู้เขียนจึงไม่สามารถตำหนิว่าไม่สามารถรับประกันพฤติกรรมที่แน่นอนได้
Martin Tuskevicius

@RobertKlemme, +1 เราทุกคนเกลียดการคาดเดาอย่างไรก็ตามมันอาจเป็นไปไม่ได้ที่จะวัดประสิทธิภาพสำหรับระบบปฏิบัติการหลักทั้งหมดเนื่องจากมีระบบปฏิบัติการหลักหลายวิธีมากเกินไป โพสต์อื่น พยายามนั้น แต่เราสามารถเห็นปัญหามากมายกับเกณฑ์มาตรฐานเริ่มต้นด้วย "ผลลัพธ์มีความผันผวนอย่างกว้างขวางขึ้นอยู่กับระบบปฏิบัติการ" นอกจากนี้ถ้ามีแกะดำที่ทำสิ่งที่น่ากลัวเช่นการคัดลอกบัฟเฟอร์ในทุก I / O? แล้วเพราะแกะที่เราอาจจะถูกบังคับเพื่อป้องกันไม่ให้เขียนโค้ดที่เราอาจจะใช้เพียงเพื่อหลีกเลี่ยงสถานการณ์ที่เลวร้ายที่สุดกรณีเหล่านี้
Pacerier

@RobertKlemme ฉันเห็นด้วย มีการคาดเดามากเกินไปที่นี่ JVM ไม่น่าจะหายไปในการจัดสรรไบต์อาร์เรย์อย่างกระจัดกระจายตัวอย่างเช่น
มาร์ควิสแห่ง Lorne

@Edwin Dalorzo: ทำไมเราต้องบัฟเฟอร์ไบต์ดังกล่าวในโลกแห่งความจริง? พวกเขาคิดค้นเป็นแฮ็คเพื่อแชร์หน่วยความจำระหว่างกระบวนการหรือไม่ พูดเช่น JVM ทำงานในกระบวนการและมันจะเป็นอีกกระบวนการหนึ่งที่ทำงานบนเครือข่ายหรือดาต้าลิงค์เลเยอร์ - ซึ่งรับผิดชอบการส่งข้อมูล - บัฟเฟอร์ไบต์เหล่านี้ถูกจัดสรรเพื่อแบ่งปันหน่วยความจำระหว่างกระบวนการเหล่านี้หรือไม่ โปรดแก้ไขให้ฉันด้วยถ้าฉันผิด ..
Tom Taylor

25

ไม่มีเหตุผลที่คาดว่าบัฟเฟอร์โดยตรงจะเร็วขึ้นสำหรับการเข้าถึงภายใน jvm ข้อได้เปรียบของพวกเขามาเมื่อคุณส่งพวกเขาไปยังรหัสเนทีฟ - เช่นโค้ดเบื้องหลังแชเนลทุกชนิด


จริง เช่นเมื่อจำเป็นต้องทำ IO ใน Scala / Java และเรียกใช้ Python / native libs แบบฝังที่มีข้อมูลหน่วยความจำขนาดใหญ่สำหรับการประมวลผลอัลกอริทึมหรือป้อนข้อมูลโดยตรงกับ GPU ใน Tensorflow
SemanticBeeng

21

เนื่องจาก DirectByteBuffers เป็นการจับคู่หน่วยความจำโดยตรงที่ระดับ OS

พวกเขาไม่ได้ พวกเขาเป็นเพียงหน่วยความจำกระบวนการแอปพลิเคชันปกติ แต่ไม่อยู่ภายใต้การย้ายระหว่าง Java GC ซึ่งทำให้สิ่งต่าง ๆ ภายในเลเยอร์ JNI ง่ายขึ้นมาก MappedByteBufferสิ่งที่คุณนำไปใช้อธิบาย

มันจะทำงานได้เร็วขึ้นด้วยการโทรออก / รับสาย

ข้อสรุปไม่ได้ติดตามจาก premiss; premiss เป็นเท็จ และข้อสรุปก็เป็นเท็จเช่นกัน มันจะเร็วขึ้นเมื่อคุณเข้าไปในเลเยอร์ JNI และถ้าคุณกำลังอ่านและเขียนจากสิ่งเดียวกันDirectByteBufferมันจะเร็วกว่ามากเพราะข้อมูลไม่ต้องข้ามขอบเขต JNI เลย


7
นี่เป็นจุดที่ดีและสำคัญ: บนเส้นทางของ IO คุณต้องข้าม Java - JNI border ในบางจุด บัฟเฟอร์แบบไบต์ตรงและแบบไม่ย้ายขอบเท่านั้น: ด้วยบัฟเฟอร์โดยตรงการดำเนินการทั้งหมดที่ใส่จากที่ดินของจาวาจะต้องข้ามในขณะที่บัฟเฟอร์ที่ไม่ใช่โดยตรงการดำเนินการ IO ทั้งหมดจะต้องข้าม สิ่งที่เร็วขึ้นกับแอพพลิเคชั่น
Robert Klemme

@RobertKlemme ข้อมูลสรุปของคุณไม่ถูกต้อง ด้วยบัฟเฟอร์ทั้งหมดข้อมูลใด ๆ ที่เข้าและออกจาก Java จะต้องข้ามขอบเขต JNI จุดสำคัญของบัฟเฟอร์โดยตรงคือถ้าคุณเพียงแค่คัดลอกข้อมูลจากช่องหนึ่งไปยังอีกช่องทางหนึ่งเช่นการอัปโหลดไฟล์คุณไม่จำเป็นต้องไปที่จาวาเลยเลยซึ่งเร็วกว่ามาก
มาร์ควิสแห่ง Lorne

ข้อมูลสรุปของฉันไม่ถูกต้องตรงไหน และ "สรุป" สิ่งที่จะเริ่มต้นด้วย? ฉันพูดอย่างชัดเจนเกี่ยวกับ "ใส่การดำเนินงานจากที่ดิน Java" หากคุณคัดลอกข้อมูลระหว่างแชนเนลเท่านั้น (นั่นคือไม่ต้องจัดการกับข้อมูลใน Java land) นั่นเป็นเรื่องที่แตกต่างออกไป
Robert Klemme

@RobertKlemme คำสั่งของคุณว่า 'มีบัฟเฟอร์โดยตรง [เท่านั้น] การดำเนินการทั้งหมดที่นำมาจาก Java land ต้องข้าม' ไม่ถูกต้อง ทั้งสองได้รับและทำให้ต้องข้าม
มาร์ควิสแห่งลอร์น

EJP ดูเหมือนว่าคุณยังขาดความแตกต่างที่ตั้งใจไว้ @RobertKlemme กำลังสร้างโดยเลือกใช้คำว่า "ใส่การดำเนินงาน" ในวลีเดียวและใช้คำว่า "ปฏิบัติการ IO" ในวลีที่ตรงกันข้ามของประโยค ในวลีหลังความตั้งใจของเขาคือการอ้างถึงการดำเนินงานระหว่างบัฟเฟอร์และอุปกรณ์ที่ให้มากับระบบปฏิบัติการบางชนิด
naki

18

ดีที่สุดที่จะทำการวัดของคุณเอง คำตอบอย่างรวดเร็วดูเหมือนว่าการส่งจากallocateDirect()บัฟเฟอร์ใช้เวลาน้อยกว่าallocate()ตัวแปร25% ถึง 75% (ทดสอบเมื่อคัดลอกไฟล์ไปยัง / dev / null) ขึ้นอยู่กับขนาด แต่การจัดสรรตัวเองอาจช้าลงอย่างมาก ปัจจัย 100x)

แหล่งที่มา:


ขอบคุณ ฉันจะยอมรับคำตอบของคุณ แต่ฉันกำลังมองหารายละเอียดเฉพาะเพิ่มเติมเกี่ยวกับความแตกต่างของประสิทธิภาพ
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.