Java 8: ประสิทธิภาพของ Streams vs Collections


140

ฉันยังใหม่กับ Java 8 ฉันยังไม่รู้ API ในเชิงลึก แต่ฉันได้ทำเกณฑ์มาตรฐานอย่างไม่เป็นทางการเพื่อเปรียบเทียบประสิทธิภาพของ Streams API ใหม่กับคอลเลกชันเก่าที่ดี

การทดสอบประกอบด้วยในการกรองรายการIntegerและจำนวนคู่แต่ละคำนวณรากและเก็บไว้ในผลของListDouble

นี่คือรหัส:

    public static void main(String[] args) {
        //Calculating square root of even numbers from 1 to N       
        int min = 1;
        int max = 1000000;

        List<Integer> sourceList = new ArrayList<>();
        for (int i = min; i < max; i++) {
            sourceList.add(i);
        }

        List<Double> result = new LinkedList<>();


        //Collections approach
        long t0 = System.nanoTime();
        long elapsed = 0;
        for (Integer i : sourceList) {
            if(i % 2 == 0){
                result.add(Math.sqrt(i));
            }
        }
        elapsed = System.nanoTime() - t0;       
        System.out.printf("Collections: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));


        //Stream approach
        Stream<Integer> stream = sourceList.stream();       
        t0 = System.nanoTime();
        result = stream.filter(i -> i%2 == 0).map(i -> Math.sqrt(i)).collect(Collectors.toList());
        elapsed = System.nanoTime() - t0;       
        System.out.printf("Streams: Elapsed time:\t\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));


        //Parallel stream approach
        stream = sourceList.stream().parallel();        
        t0 = System.nanoTime();
        result = stream.filter(i -> i%2 == 0).map(i -> Math.sqrt(i)).collect(Collectors.toList());
        elapsed = System.nanoTime() - t0;       
        System.out.printf("Parallel streams: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));      
    }.

และนี่คือผลลัพธ์สำหรับเครื่องดูอัลคอร์:

    Collections: Elapsed time:        94338247 ns   (0,094338 seconds)
    Streams: Elapsed time:           201112924 ns   (0,201113 seconds)
    Parallel streams: Elapsed time:  357243629 ns   (0,357244 seconds)

สำหรับการทดสอบนี้โดยเฉพาะสตรีมจะช้ากว่าคอลเล็กชั่นประมาณสองเท่าและการขนานกันก็ไม่ได้ช่วยอะไร

คำถาม:

  • การทดสอบนี้ยุติธรรมหรือไม่ ฉันทำผิดพลาดหรือไม่?
  • สตรีมช้ากว่าคอลเล็กชันหรือไม่ มีใครทำมาตรฐานอย่างเป็นทางการที่ดีเกี่ยวกับเรื่องนี้?
  • ฉันควรพยายามหาวิธีใด

อัปเดตผลลัพธ์

ฉันรันการทดสอบ 1k ครั้งหลังจากการอุ่นเครื่อง JVM (การทำซ้ำ 1k) ตามคำแนะนำของ @pveentjer:

    Collections: Average time:      206884437,000000 ns     (0,206884 seconds)
    Streams: Average time:           98366725,000000 ns     (0,098367 seconds)
    Parallel streams: Average time: 167703705,000000 ns     (0,167704 seconds)

ในกรณีนี้สตรีมจะมีประสิทธิภาพมากกว่า ฉันสงสัยว่าจะพบสิ่งใดในแอปที่ฟังก์ชั่นการกรองถูกเรียกเพียงครั้งเดียวหรือสองครั้งในช่วงรันไทม์


1
คุณเคยลองใช้มันIntStreamแทนหรือไม่?
Mark Rotteveel

2
คุณกรุณาวัดอย่างถูกต้องได้ไหม หากสิ่งที่คุณกำลังทำคือการวิ่งครั้งเดียวแน่นอนว่ามาตรฐานของคุณจะถูกปิด
skiwi

2
@MisterSmith เราจะมีความโปร่งใสเกี่ยวกับวิธีที่คุณอุ่นเครื่อง JVM ของคุณด้วยการทดสอบ 1K ได้หรือไม่
skiwi

1
และสำหรับผู้ที่สนใจในการเขียน microbenchmarks ที่ถูกต้องนี่คือคำถาม: stackoverflow.com/questions/504103/ …
มิสเตอร์สมิ ธ

2
@assylias การใช้toListควรทำงานแบบขนานแม้ว่าจะรวบรวมไปยังรายการที่ไม่ปลอดภัยเนื่องจากเธรดที่แตกต่างกันจะรวบรวมไปยังรายการกลางที่ จำกัด เธรดก่อนที่จะรวมเข้าด้วยกัน
Stuart Marks

คำตอบ:


192
  1. หยุดใช้LinkedListเพื่ออะไรก็ได้ แต่การลบอย่างหนักจากส่วนกลางของรายการโดยใช้ตัววนซ้ำ

  2. หยุดเขียนโค้ดการเปรียบเทียบด้วยมือใช้JMH

มาตรฐานที่เหมาะสม:

@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
@OperationsPerInvocation(StreamVsVanilla.N)
public class StreamVsVanilla {
    public static final int N = 10000;

    static List<Integer> sourceList = new ArrayList<>();
    static {
        for (int i = 0; i < N; i++) {
            sourceList.add(i);
        }
    }

    @Benchmark
    public List<Double> vanilla() {
        List<Double> result = new ArrayList<>(sourceList.size() / 2 + 1);
        for (Integer i : sourceList) {
            if (i % 2 == 0){
                result.add(Math.sqrt(i));
            }
        }
        return result;
    }

    @Benchmark
    public List<Double> stream() {
        return sourceList.stream()
                .filter(i -> i % 2 == 0)
                .map(Math::sqrt)
                .collect(Collectors.toCollection(
                    () -> new ArrayList<>(sourceList.size() / 2 + 1)));
    }
}

ผลลัพธ์:

Benchmark                   Mode   Samples         Mean   Mean error    Units
StreamVsVanilla.stream      avgt        10       17.588        0.230    ns/op
StreamVsVanilla.vanilla     avgt        10       10.796        0.063    ns/op

เช่นเดียวกับที่ฉันคาดว่าการใช้งานสตรีมค่อนข้างช้า JIT สามารถอินไลน์ของแลมบ์ดาทั้งหมด แต่ไม่ได้สร้างโค้ดที่กระชับอย่างสมบูรณ์แบบเป็นรุ่นวานิลลา

โดยทั่วไปแล้วกระแสข้อมูล Java 8 ไม่ใช่เวทมนตร์ พวกเขาไม่สามารถเร่งความเร็วสิ่งที่ใช้งานได้ดี (ด้วยซ้ำหรืออาจเป็นธรรมดาหรือ Java 5's สำหรับแต่ละงบแทนที่ด้วยIterable.forEach()และCollection.removeIf()โทร) สตรีมเป็นเรื่องเกี่ยวกับความสะดวกและความปลอดภัยในการเขียนโค้ด ความสะดวกสบาย - การแลกเปลี่ยนความเร็วทำงานที่นี่


2
ขอบคุณที่สละเวลามานั่งบัลลังก์นี้ ฉันไม่คิดว่าการเปลี่ยนแปลง LinkedList for ArrayList จะเปลี่ยนแปลงอะไรเลยเนื่องจากการทดสอบทั้งสองควรเพิ่มเข้าไปเวลาไม่ควรได้รับผลกระทบ อย่างไรก็ตามคุณช่วยอธิบายผลลัพธ์ได้ไหม? เป็นการยากที่จะบอกว่าคุณวัดอะไรที่นี่ (หน่วยบอก ns / op แต่สิ่งที่ถือว่าเป็น op)
มิสเตอร์สมิ ธ

52
ข้อสรุปของคุณเกี่ยวกับประสิทธิภาพแม้ว่าถูกต้องแล้ว มีหลายกรณีที่รหัสสตรีมนั้นเร็วกว่ารหัสวนซ้ำส่วนใหญ่เนื่องจากค่าใช้จ่ายในการเข้าถึงองค์ประกอบต่อราคาถูกกว่าด้วยสตรีมมากกว่าด้วยตัววนซ้ำธรรมดา และในหลายกรณีเวอร์ชันสตรีมจะแทรกสิ่งที่เทียบเท่ากับเวอร์ชันที่เขียนด้วยมือ แน่นอนมารอยู่ในรายละเอียด; บิตใด ๆ ของรหัสอาจทำงานแตกต่างกัน
Brian Goetz

26
@BrianGoetz คุณช่วยระบุกรณีการใช้งานได้ไหมเมื่อสตรีมเร็วขึ้น?
Alexandr

1
ในรุ่นล่าสุดของ FMH: ใช้@Benchmarkแทน@GenerateMicroBenchmark
pdem

3
@BrianGoetz, คุณช่วยระบุกรณีการใช้งานได้ไหมเมื่อสตรีมเร็วขึ้น?
kiltek

17

1) คุณเห็นเวลาน้อยกว่า 1 วินาทีโดยใช้เกณฑ์มาตรฐานของคุณ ซึ่งหมายความว่าอาจมีอิทธิพลอย่างมากจากผลข้างเคียงต่อผลลัพธ์ของคุณ ดังนั้นฉันเพิ่มงานของคุณ 10 ครั้ง

    int max = 10_000_000;

และวิ่งมาตรฐานของคุณ ผลลัพธ์ของฉัน:

Collections: Elapsed time:   8592999350 ns  (8.592999 seconds)
Streams: Elapsed time:       2068208058 ns  (2.068208 seconds)
Parallel streams: Elapsed time:  7186967071 ns  (7.186967 seconds)

โดยไม่ต้องแก้ไข ( int max = 1_000_000) ผลลัพธ์คือ

Collections: Elapsed time:   113373057 ns   (0.113373 seconds)
Streams: Elapsed time:       135570440 ns   (0.135570 seconds)
Parallel streams: Elapsed time:  104091980 ns   (0.104092 seconds)

เป็นเหมือนผลลัพธ์ของคุณ: สตรีมช้ากว่าการรวบรวม สรุป:ใช้เวลาในการส่งข้อมูลเริ่มต้น / ค่ามากขึ้น

2) หลังจากเพิ่มกระแสงานให้เร็วขึ้น (ไม่เป็นไร) แต่กระแสขนานขนานช้าเกินไป เกิดอะไรขึ้น หมายเหตุ: คุณมีcollect(Collectors.toList())คำสั่งในตัวคุณ การรวบรวมลงในคอลเล็กชันเดี่ยวเป็นหลักจะแนะนำคอขวดของประสิทธิภาพและค่าใช้จ่ายในกรณีที่มีการดำเนินการพร้อมกัน มีความเป็นไปได้ที่จะประเมินต้นทุนสัมพัทธ์ของค่าใช้จ่ายโดยการแทนที่

collecting to collection -> counting the element count

collect(Collectors.counting())สำหรับกระแสก็สามารถทำได้โดย ฉันได้รับผลลัพธ์:

Collections: Elapsed time:   41856183 ns    (0.041856 seconds)
Streams: Elapsed time:       546590322 ns   (0.546590 seconds)
Parallel streams: Elapsed time:  1540051478 ns  (1.540051 seconds)

นั่นเป็นงานใหญ่! ( int max = 10000000) บทสรุป: การรวบรวมรายการเพื่อรวบรวมใช้เวลาส่วนใหญ่ ส่วนที่ช้าที่สุดคือการเพิ่มลงในรายการ BTW ง่ายใช้สำหรับArrayListCollectors.toList()


คุณจำเป็นต้อง microbenchmark การทดสอบนี้หมายความว่ามันควรจะอุ่นขึ้นครั้งแรกหลายครั้งแล้วดำเนินการจำนวนมากของ TMES และค่าเฉลี่ย
skiwi

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

JIT ในโหมดเซิร์ฟเวอร์เริ่มต้นหลังจากการประหารชีวิต 10k จากนั้นใช้เวลาในการรวบรวมรหัสและสลับมัน
pveentjer

เกี่ยวกับประโยคนี้: " คุณมีcollect(Collectors.toList())คำสั่งของคุณนั่นคืออาจมีสถานการณ์เมื่อคุณจำเป็นต้องจัดการคอลเลกชันเดียวโดยหลายกระทู้ " ฉันเกือบแน่ใจว่าtoListจะรวบรวมอินสแตนซ์รายการที่แตกต่างกันหลายรายการพร้อมกัน เป็นขั้นตอนสุดท้ายในการรวบรวมองค์ประกอบเท่านั้นที่จะถูกถ่ายโอนไปยังหนึ่งรายการจากนั้นส่งคืน ดังนั้นจึงไม่ควรมีการประสานค่าใช้จ่าย นี่คือเหตุผลที่นักสะสมมีทั้งซัพพลายเออร์ผู้จัดหาและฟังก์ชั่นคอมบิเนเตอร์ (มันอาจช้าด้วยเหตุผลอื่นแน่นอน)
Lii

@Lii ฉันคิดแบบเดียวกับcollectการใช้งานที่นี่ แต่ในท้ายที่สุดหลายรายการควรจะรวมเป็นหนึ่งเดียวและดูเหมือนว่าการรวมเป็นการดำเนินงานที่หนักที่สุดในตัวอย่าง
Sergey Fedorov

4
    public static void main(String[] args) {
    //Calculating square root of even numbers from 1 to N       
    int min = 1;
    int max = 10000000;

    List<Integer> sourceList = new ArrayList<>();
    for (int i = min; i < max; i++) {
        sourceList.add(i);
    }

    List<Double> result = new LinkedList<>();


    //Collections approach
    long t0 = System.nanoTime();
    long elapsed = 0;
    for (Integer i : sourceList) {
        if(i % 2 == 0){
            result.add( doSomeCalculate(i));
        }
    }
    elapsed = System.nanoTime() - t0;       
    System.out.printf("Collections: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));


    //Stream approach
    Stream<Integer> stream = sourceList.stream();       
    t0 = System.nanoTime();
    result = stream.filter(i -> i%2 == 0).map(i -> doSomeCalculate(i))
            .collect(Collectors.toList());
    elapsed = System.nanoTime() - t0;       
    System.out.printf("Streams: Elapsed time:\t\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));


    //Parallel stream approach
    stream = sourceList.stream().parallel();        
    t0 = System.nanoTime();
    result = stream.filter(i -> i%2 == 0).map(i ->  doSomeCalculate(i))
            .collect(Collectors.toList());
    elapsed = System.nanoTime() - t0;       
    System.out.printf("Parallel streams: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));      
}

static double doSomeCalculate(int input) {
    for(int i=0; i<100000; i++){
        Math.sqrt(i+input);
    }
    return Math.sqrt(input);
}

ฉันเปลี่ยนรหัสเล็กน้อยวิ่งบน mac book pro ซึ่งมี 8 คอร์ฉันได้รับผลลัพธ์ที่สมเหตุสมผล:

คอลเลกชัน: เวลาที่ผ่านไป: 1522036826 ns (1.522037 วินาที)

สตรีม: เวลาที่ผ่านไป: 4315833719 ns (4.315834 วินาที)

สตรีมแบบขนาน: เวลาที่ผ่านไป: 261152901 ns (0.261153 วินาที)


ฉันคิดว่าการทดสอบของคุณยุติธรรมคุณแค่ต้องการเครื่องที่มีแกน cpu มากขึ้น
เมลลอน

3

สำหรับสิ่งที่คุณพยายามจะทำฉันจะไม่ใช้จาวา api ปกติของต่อไป มีการชกมวย / การตีมวยเป็นจำนวนมากเกิดขึ้นดังนั้นจึงมีค่าใช้จ่ายมากมายในการแสดง

โดยส่วนตัวแล้วฉันคิดว่า API จำนวนมากที่ออกแบบมาเป็นอึเพราะพวกเขาสร้างวัตถุจำนวนมาก

ลองใช้อาร์เรย์ดั้งเดิมของ double / int และลองทำเธรดเดี่ยวและดูว่าประสิทธิภาพนั้นคืออะไร

PS: คุณอาจต้องการดู JMH เพื่อดูแลการทำเกณฑ์มาตรฐาน ดูแลข้อผิดพลาดทั่วไปบางอย่างเช่นทำให้ JVM อุ่นขึ้น


LinkedLists ยิ่งแย่กว่า ArrayLists เพราะคุณต้องสร้างวัตถุโหนดทั้งหมด ผู้ประกอบการ mod ยังเป็นสุนัขช้า ฉันเชื่อว่าบางสิ่งบางอย่างเช่น 10/15 รอบ + มันจะไปป์ไลน์คำสั่ง หากคุณต้องการหารอย่างรวดเร็วด้วย 2 ให้เลื่อนจำนวน 1 บิตไปทางขวา นี่เป็นเทคนิคพื้นฐาน แต่ฉันแน่ใจว่ามีเทคนิคขั้นสูงในโหมดเพื่อเพิ่มความเร็ว แต่สิ่งเหล่านี้อาจเป็นปัญหาที่เฉพาะเจาะจงมากขึ้น
pveentjer

ฉันรู้เรื่องมวย นี่เป็นเพียงมาตรฐานอย่างไม่เป็นทางการ ความคิดคือการมีจำนวนชกมวย / unboxing เดียวกันทั้งในคอลเลกชันและการทดสอบกระแส
มิสเตอร์สมิ ธ

ก่อนอื่นฉันต้องแน่ใจว่ามันไม่ได้วัดความผิดพลาด พยายามรันเกณฑ์มาตรฐานสองสามครั้งก่อนที่คุณจะทำการวัดประสิทธิภาพจริง อย่างน้อยคุณก็ต้องทำการ JVM warmup ให้พ้นทางและรหัสนั้นถูก JITTED อย่างถูกต้อง หากปราศจากสิ่งนี้คุณอาจทำข้อสรุปที่ผิด
pveentjer

ตกลงฉันจะโพสต์ผลลัพธ์ใหม่ตามคำแนะนำของคุณ ฉันได้ดู JMH แล้ว แต่ต้องใช้ Maven และใช้เวลาในการปรับแต่ง ขอบคุณอยู่ดี
มิสเตอร์สมิ ธ

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