Parallel Infinite Java Streams หน่วยความจำไม่เพียงพอ


16

ฉันพยายามที่จะเข้าใจว่าทำไมโปรแกรม Java ต่อไปนี้ถึงให้OutOfMemoryErrorในขณะที่โปรแกรมที่สอดคล้องกันโดย.parallel()ไม่ได้

System.out.println(Stream
    .iterate(1, i -> i+1)
    .parallel()
    .flatMap(n -> Stream.iterate(n, i -> i+n))
    .mapToInt(Integer::intValue)
    .limit(100_000_000)
    .sum()
);

ฉันมีสองคำถาม:

  1. ผลลัพธ์ที่ต้องการของโปรแกรมนี้คืออะไร?

    หากปราศจาก.parallel()ดูเหมือนว่านี่เป็นเพียงการแสดงผลsum(1+2+3+...)ซึ่งหมายความว่าเพียง "ติด" ที่กระแสแรกใน flatMap ซึ่งทำให้รู้สึก

    เมื่อใช้คู่ขนานฉันไม่รู้ว่ามีพฤติกรรมที่คาดหวังหรือไม่ แต่ฉันเดาว่าจะเป็นอย่างใดอย่างหนึ่งที่มันแทรกซึมnลำธารสายแรกหรือสายอื่นnๆ มันอาจแตกต่างกันเล็กน้อยขึ้นอยู่กับพฤติกรรมการ chunking / buffering

  2. ทำให้หน่วยความจำไม่เพียงพอคืออะไร ฉันพยายามทำความเข้าใจว่ากระแสเหล่านี้ถูกนำไปใช้อย่างไรภายใต้ประทุน

    ฉันคาดเดาสิ่งที่บล็อกกระแสดังนั้นมันไม่เคยเสร็จสิ้นและสามารถกำจัดค่าที่สร้างขึ้นได้ แต่ฉันไม่ค่อยรู้ว่าสิ่งใดที่เรียงลำดับตามการประเมินและที่การบัฟเฟอร์เกิดขึ้น

แก้ไข:ในกรณีที่มีความเกี่ยวข้องฉันใช้ Java 11

Editt 2:เห็นได้ชัดว่าสิ่งเดียวกันที่เกิดขึ้นแม้สำหรับโปรแกรมที่ง่ายIntStream.iterate(1,i->i+1).limit(1000_000_000).parallel().sum()ดังนั้นจึงอาจจะต้องทำอย่างไรกับ lazyness ของมากกว่าlimitflatMap


parallel () ใช้ ForkJoinPool ภายใน ฉันเดา ForkJoin Framework ใน Java จาก Java 7
aravind

คำตอบ:


9

คุณพูดว่า“ แต่ฉันไม่ค่อยรู้ว่าสิ่งใดที่ได้รับการประเมินตามลำดับและเกิดการบัฟเฟอร์ขึ้นที่ใด ” ซึ่งเป็นกระแสข้อมูลที่ขนานกันอย่างแม่นยำ ลำดับการประเมินผลไม่ได้ระบุไว้

.limit(100_000_000)ลักษณะที่สำคัญของตัวอย่างของคุณเป็น นี่ก็หมายความว่าการดำเนินการก็ไม่สามารถสรุปค่าโดยพลการ แต่ต้องสรุป100,000,000 แรกตัวเลข โปรดทราบว่าในการนำการอ้างอิงไปใช้.unordered().limit(100_000_000)จะไม่เปลี่ยนแปลงผลลัพธ์ซึ่งบ่งชี้ว่าไม่มีการใช้งานพิเศษสำหรับกรณีที่ไม่มีการเรียงลำดับ แต่เป็นรายละเอียดการใช้งาน

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

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

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

เมื่อเราทำให้เธรดผู้ทำงานช้าลงทั้งหมดยกเว้นอันที่ประมวลผลกลุ่มข้อมูลด้านซ้ายสุดเราสามารถทำให้การสตรีมยุติลง (อย่างน้อยก็ในการรันส่วนใหญ่):

System.out.println(IntStream
    .iterate(1, i -> i+1)
    .parallel()
    .peek(i -> { if(i != 1) LockSupport.parkNanos(1_000_000_000); })
    .flatMap(n -> IntStream.iterate(n, i -> i+n))
    .limit(100_000_000)
    .sum()
);

¹ฉันทำตามคำแนะนำของ Stuart Marksเพื่อใช้ลำดับซ้าย - ขวา - ขวาเมื่อพูดถึงลำดับการเผชิญหน้ามากกว่าลำดับการประมวลผล


คำตอบที่ดีมาก! ฉันสงสัยว่ามีความเสี่ยงหรือไม่ที่เธรดทั้งหมดเริ่มต้นรันการดำเนินการ flatMap และไม่มีใครได้รับการจัดสรรเพื่อล้างบัฟเฟอร์ (รวม) จริงหรือไม่ ในกรณีการใช้งานจริงของฉันลำธารที่ไม่มีที่สิ้นสุดแทนไฟล์ที่ใหญ่เกินไปที่จะเก็บไว้ในหน่วยความจำ ฉันสงสัยว่าฉันจะเขียนกระแสเพื่อให้การใช้งานหน่วยความจำลดลงได้อย่างไร?
โทมัส Ahle

1
คุณกำลังใช้Files.lines(…)? ได้รับการปรับปรุงอย่างมีนัยสำคัญใน Java 9
Holger

1
นี่คือสิ่งที่มันทำใน Java 8 ใน JRE ที่ใหม่กว่ามันจะยังคงถอยกลับไปBufferedReader.lines()ในบางสถานการณ์ (ไม่ใช่ระบบไฟล์เริ่มต้นชุดอักขระพิเศษหรือขนาดที่ใหญ่กว่าInteger.MAX_FILES) หากใช้วิธีใดวิธีหนึ่งโซลูชันที่กำหนดเองอาจช่วยได้ นี่จะคุ้มค่ากับคำถาม & คำตอบใหม่ ...
Holger

1
Integer.MAX_VALUEแน่นอน ...
Holger

1
สตรีมภายนอกคืออะไรสตรีมของไฟล์ มันมีขนาดที่สามารถคาดเดาได้หรือไม่?
Holger

5

เดาที่ดีที่สุดของฉันคือว่าการเพิ่มparallel()การเปลี่ยนแปลงพฤติกรรมภายในของflatMap()ซึ่งปัญหาที่เกิดขึ้นแล้วมีการประเมินอย่างเฉื่อยชาก่อน

OutOfMemoryErrorผิดพลาดที่คุณจะได้รับการรายงานใน[JDK-8202307] รับ java.lang.OutOfMemoryError:. พื้นที่กอง Java เมื่อโทร Stream.iterator () ติด () ในกระแสที่ใช้อนันต์ / สตรีมใหญ่มากใน flatMap ถ้าคุณดูตั๋วมันเป็นสแต็คการติดตามแบบเดียวกับที่คุณได้รับมากขึ้นหรือน้อยลง ตั๋วถูกปิดเนื่องจากไม่สามารถแก้ไขได้ด้วยเหตุผลต่อไปนี้:

iterator()และspliterator()วิธีการเป็น "ฟักหลบหนี" เพื่อนำมาใช้เมื่อมันเป็นไปไม่ได้ที่จะใช้ดำเนินการอื่น ๆ พวกเขามีข้อ จำกัด บางอย่างเพราะพวกเขาเปลี่ยนรูปแบบการผลักดันของการใช้กระแสเป็นแบบดึง การเปลี่ยนแปลงดังกล่าวต้องใช้บัฟเฟอร์ในบางกรณีเช่นเมื่อองค์ประกอบคือ (แบน) แมปไปสองคนหรือมากกว่าองค์ประกอบ มันจะทำให้การใช้งานสตรีมมีความซับซ้อนซึ่งอาจเป็นไปได้ที่ค่าใช้จ่ายของกรณีทั่วไปจะสนับสนุนแนวคิดของแรงดันย้อนกลับเพื่อสื่อสารว่ามีองค์ประกอบจำนวนเท่าใดที่จะดึงผ่านชั้นซ้อนของการผลิตองค์ประกอบ


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

3

OOME เกิดไม่ได้ตามกระแสเป็นอนันต์ แต่จากข้อเท็จจริงที่ว่ามันไม่ได้เป็น

คือถ้าคุณคอมเม้นท์.limit(...)มันจะไม่มีวันหมดความทรงจำ - แต่แน่นอนว่ามันจะไม่จบด้วยเช่นกัน

เมื่อแยกแล้วสตรีมสามารถติดตามจำนวนองค์ประกอบได้เฉพาะเมื่อมีการสะสมภายในแต่ละเธรด (ดูเหมือนว่าจะมีการสะสมจริงSpliterators$ArraySpliterator#array)

ดูเหมือนว่าคุณสามารถทำซ้ำได้โดยไม่ต้องflatMapเรียกใช้ต่อไปนี้ด้วย-Xmx128m:

    System.out.println(Stream
            .iterate(1, i -> i + 1)
            .parallel()
      //    .flatMap(n -> Stream.iterate(n, i -> i+n))
            .mapToInt(Integer::intValue)
            .limit(100_000_000)
            .sum()
    );

อย่างไรก็ตามหลังจากที่แสดงความคิดเห็นออกมาlimit()มันก็จะทำงานได้ดีจนกว่าคุณจะตัดสินใจที่จะสำรองแล็ปท็อปของคุณ

นอกจากรายละเอียดการใช้งานจริงแล้วนี่คือสิ่งที่ฉันคิดว่ากำลังเกิดขึ้น:

ด้วยlimitตัวsumลดต้องการให้องค์ประกอบ X แรกรวมกันดังนั้นเธรดจึงไม่สามารถเปล่งผลรวมบางส่วนได้ "slice" แต่ละอัน (เธรด) จะต้องรวบรวมองค์ประกอบและส่งผ่าน ไม่ จำกัด ไม่มีข้อ จำกัด ดังกล่าวดังนั้น "ชิ้น" แต่ละอันจะคำนวณผลรวมบางส่วนจากองค์ประกอบที่ได้รับ (ถาวร) โดยสมมติว่ามันจะเปล่งผลลัพธ์ในที่สุด


คุณหมายถึง "เมื่อแยกแล้ว" วงเงินไม่แยกอย่างใด?
โทมัส Ahle

@ThomasAhle parallel()จะใช้ForkJoinPoolภายในเพื่อให้เกิดความเท่าเทียม Spliteratorจะใช้ในการทำงานกำหนดให้แต่ละForkJoinงานที่ผมคิดว่าเราสามารถเรียกหน่วยงานที่นี่เป็น "แยก"
Karol Dowbecki

แต่ทำไมถึงเกิดขึ้นกับขีด จำกัด เท่านั้น?
โทมัส Ahle

@ThomasAhle ฉันแก้ไขคำตอบด้วยสองเซ็นต์ของฉัน
Costi Ciudatu

1
@ThomasAhle ตั้งค่าเบรกพอยต์Integer.sum()โดยใช้ตัวIntStream.sumลด คุณจะเห็นว่าการโทรแบบไม่ จำกัด นั้นทำงานตลอดเวลาในขณะที่รุ่นที่ จำกัด จะไม่เรียกใช้ก่อน OOM
Costi Ciudatu
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.