Java 8 Streams: beberapa filter vs. kondisi kompleks

235

Terkadang Anda ingin memfilter Streamdengan lebih dari satu kondisi:

myList.stream().filter(x -> x.size() > 10).filter(x -> x.isCool()) ...

atau Anda dapat melakukan hal yang sama dengan kondisi kompleks dan satu filter :

myList.stream().filter(x -> x.size() > 10 && x -> x.isCool()) ...

Dugaan saya adalah bahwa pendekatan kedua memiliki karakteristik kinerja yang lebih baik, tetapi saya tidak mengetahuinya .

Pendekatan pertama menang dalam keterbacaan, tetapi apa yang lebih baik untuk kinerja?

deamon
sumber
57
Tulis kode mana saja yang lebih mudah dibaca dalam situasi tersebut. Perbedaan kinerja minimal (dan sangat situasional).
Brian Goetz
5
Lupakan nano-optimasi dan gunakan kode yang sangat mudah dibaca & dipelihara. dengan stream, kita harus selalu menggunakan setiap operasi secara terpisah termasuk filter.
Diablo

Jawaban:

151

Kode yang harus dieksekusi untuk kedua alternatif ini sangat mirip sehingga Anda tidak dapat memprediksi hasilnya dengan andal. Struktur objek yang mendasarinya mungkin berbeda tetapi itu bukan tantangan bagi pengoptimal hotspot. Jadi itu tergantung pada kondisi lain di sekitarnya yang akan menghasilkan eksekusi yang lebih cepat, jika ada perbedaan.

Menggabungkan dua instance filter menciptakan lebih banyak objek dan karenanya lebih banyak mendelegasikan kode tetapi ini dapat berubah jika Anda menggunakan referensi metode daripada ekspresi lambda, misalnya ganti filter(x -> x.isCool())dengan filter(ItemType::isCool). Dengan begitu Anda telah menghilangkan metode pendelegasian sintetis yang dibuat untuk ekspresi lambda Anda. Jadi, menggabungkan dua filter menggunakan dua metode referensi dapat membuat kode delegasi yang sama atau lebih kecil dari satu filterpermintaan tunggal menggunakan ekspresi lambda &&.

Tetapi, seperti dikatakan, overhead semacam ini akan dihilangkan oleh pengoptimal HotSpot dan dapat diabaikan.

Secara teori, dua filter bisa lebih mudah diparalelkan daripada satu filter tetapi itu hanya relevan untuk tugas-tugas yang agak komputasional¹.

Jadi tidak ada jawaban sederhana.

Intinya adalah, jangan berpikir tentang perbedaan kinerja seperti di bawah ambang batas deteksi bau. Gunakan apa yang lebih mudah dibaca.


¹ ... dan akan membutuhkan implementasi yang melakukan pemrosesan paralel dari tahapan selanjutnya, jalan yang saat ini tidak diambil oleh implementasi Stream standar

Holger
sumber
4
bukankah kode harus mengulangi aliran yang dihasilkan setelah setiap filter?
jucardi
13
@Juan Carlos Diaz: tidak, aliran tidak bekerja seperti itu. Baca tentang "evaluasi malas"; operasi perantara tidak melakukan apa-apa, mereka hanya mengubah hasil operasi terminal.
Holger
34

Kondisi filter yang kompleks lebih baik dalam perspektif kinerja, tetapi kinerja terbaik akan menunjukkan mode lama untuk loop dengan standar if clauseadalah opsi terbaik. Perbedaan pada array 10 elemen perbedaan kecil mungkin ~ 2 kali, untuk array besar perbedaannya tidak begitu besar.
Anda dapat melihat pada proyek GitHub saya , di mana saya melakukan tes kinerja untuk opsi iterasi berbagai array

Untuk ops / throughput elemen array 10 kecil: 10 elemen array Untuk ops / s throughput elemen 10.000 sedang : Untuk ops / masukkan deskripsi gambar di sini array throughput elemen yang besar 1.000.000: 1M elemen

CATATAN: tes berjalan

  • 8 CPU
  • RAM 1 GB
  • Versi OS: 16.04.1 LTS (Xenial Xerus)
  • versi java: 1.8.0_121
  • jvm: -XX: + UseG1GC -server -Xmx1024m -Xms1024m

UPDATE: Java 11 memiliki beberapa kemajuan pada kinerja, tetapi dinamika tetap sama

Mode benchmark: Throughput, ops / waktu Java 8vs11

Serge
sumber
22

Tes ini menunjukkan bahwa opsi kedua Anda dapat bekerja lebih baik secara signifikan. Temuan pertama, lalu kodenya:

one filter with predicate of form u -> exp1 && exp2, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=4142, min=29, average=41.420000, max=82}
two filters with predicates of form u -> exp1, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=13315, min=117, average=133.150000, max=153}
one filter with predicate of form predOne.and(pred2), list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=10320, min=82, average=103.200000, max=127}

sekarang kodenya:

enum Gender {
    FEMALE,
    MALE
}

static class User {
    Gender gender;
    int age;

    public User(Gender gender, int age){
        this.gender = gender;
        this.age = age;
    }

    public Gender getGender() {
        return gender;
    }

    public void setGender(Gender gender) {
        this.gender = gender;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

static long test1(List<User> users){
    long time1 = System.currentTimeMillis();
    users.stream()
            .filter((u) -> u.getGender() == Gender.FEMALE && u.getAge() % 2 == 0)
            .allMatch(u -> true);                   // least overhead terminal function I can think of
    long time2 = System.currentTimeMillis();
    return time2 - time1;
}

static long test2(List<User> users){
    long time1 = System.currentTimeMillis();
    users.stream()
            .filter(u -> u.getGender() == Gender.FEMALE)
            .filter(u -> u.getAge() % 2 == 0)
            .allMatch(u -> true);                   // least overhead terminal function I can think of
    long time2 = System.currentTimeMillis();
    return time2 - time1;
}

static long test3(List<User> users){
    long time1 = System.currentTimeMillis();
    users.stream()
            .filter(((Predicate<User>) u -> u.getGender() == Gender.FEMALE).and(u -> u.getAge() % 2 == 0))
            .allMatch(u -> true);                   // least overhead terminal function I can think of
    long time2 = System.currentTimeMillis();
    return time2 - time1;
}

public static void main(String... args) {
    int size = 10000000;
    List<User> users =
    IntStream.range(0,size)
            .mapToObj(i -> i % 2 == 0 ? new User(Gender.MALE, i % 100) : new User(Gender.FEMALE, i % 100))
            .collect(Collectors.toCollection(()->new ArrayList<>(size)));
    repeat("one filter with predicate of form u -> exp1 && exp2", users, Temp::test1, 100);
    repeat("two filters with predicates of form u -> exp1", users, Temp::test2, 100);
    repeat("one filter with predicate of form predOne.and(pred2)", users, Temp::test3, 100);
}

private static void repeat(String name, List<User> users, ToLongFunction<List<User>> test, int iterations) {
    System.out.println(name + ", list size " + users.size() + ", averaged over " + iterations + " runs: " + IntStream.range(0, iterations)
            .mapToLong(i -> test.applyAsLong(users))
            .summaryStatistics());
}
Hank D
sumber
3
Menarik - ketika saya mengubah urutan untuk menjalankan test2 SEBELUM test1, test1 berjalan sedikit lebih lambat. Hanya ketika test1 berjalan pertama yang tampaknya lebih cepat. Adakah yang bisa mereproduksi ini atau memiliki wawasan?
Sperr
5
Mungkin karena biaya kompilasi HotSpot dikeluarkan oleh tes apa pun yang dijalankan terlebih dahulu.
DaBlick
@Sperr Anda benar, ketika pesanan berubah, hasilnya tidak dapat diprediksi. Tetapi, ketika saya menjalankan ini dengan tiga utas yang berbeda, selalu filter yang rumit memberikan hasil yang lebih baik, terlepas dari utas mana yang dimulai lebih dulu. Di bawah ini adalah hasilnya. Test #1: {count=100, sum=7207, min=65, average=72.070000, max=91} Test #3: {count=100, sum=7959, min=72, average=79.590000, max=97} Test #2: {count=100, sum=8869, min=79, average=88.690000, max=110}
Paramesh Korrakuti
2

Ini adalah hasil dari 6 kombinasi berbeda dari uji sampel yang dibagikan oleh @Hank D. Jelas bahwa predikat formulir u -> exp1 && exp2sangat berkinerja dalam semua kasus.

one filter with predicate of form u -> exp1 && exp2, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=3372, min=31, average=33.720000, max=47}
two filters with predicates of form u -> exp1, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=9150, min=85, average=91.500000, max=118}
one filter with predicate of form predOne.and(pred2), list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=9046, min=81, average=90.460000, max=150}

one filter with predicate of form u -> exp1 && exp2, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=8336, min=77, average=83.360000, max=189}
one filter with predicate of form predOne.and(pred2), list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=9094, min=84, average=90.940000, max=176}
two filters with predicates of form u -> exp1, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=10501, min=99, average=105.010000, max=136}

two filters with predicates of form u -> exp1, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=11117, min=98, average=111.170000, max=238}
one filter with predicate of form u -> exp1 && exp2, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=8346, min=77, average=83.460000, max=113}
one filter with predicate of form predOne.and(pred2), list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=9089, min=81, average=90.890000, max=137}

two filters with predicates of form u -> exp1, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=10434, min=98, average=104.340000, max=132}
one filter with predicate of form predOne.and(pred2), list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=9113, min=81, average=91.130000, max=179}
one filter with predicate of form u -> exp1 && exp2, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=8258, min=77, average=82.580000, max=100}

one filter with predicate of form predOne.and(pred2), list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=9131, min=81, average=91.310000, max=139}
two filters with predicates of form u -> exp1, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=10265, min=97, average=102.650000, max=131}
one filter with predicate of form u -> exp1 && exp2, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=8442, min=77, average=84.420000, max=156}

one filter with predicate of form predOne.and(pred2), list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=8553, min=81, average=85.530000, max=125}
one filter with predicate of form u -> exp1 && exp2, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=8219, min=77, average=82.190000, max=142}
two filters with predicates of form u -> exp1, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=10305, min=97, average=103.050000, max=132}
Venkat Madhav
sumber