Luồng Java 8: nhiều bộ lọc so với điều kiện phức tạp


235

Đôi khi bạn muốn lọc a Streamvới nhiều điều kiện:

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

hoặc bạn có thể làm tương tự với một điều kiện phức tạp và một điều kiện duy nhất filter :

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

Tôi đoán là cách tiếp cận thứ hai có đặc điểm hiệu suất tốt hơn, nhưng tôi không biết điều đó.

Cách tiếp cận đầu tiên chiến thắng trong khả năng đọc, nhưng điều gì là tốt hơn cho hiệu suất?


57
Viết mã nào dễ đọc hơn trong tình huống. Sự khác biệt hiệu suất là tối thiểu (và có tính tình huống cao).
Brian Goetz

5
Hãy quên đi tối ưu hóa nano và sử dụng mã có thể đọc và bảo trì cao. với các luồng, người ta phải luôn luôn sử dụng từng thao tác riêng biệt bao gồm các bộ lọc.
Diablo

Câu trả lời:


151

Mã phải được thực thi cho cả hai lựa chọn thay thế giống nhau đến mức bạn không thể dự đoán được kết quả một cách đáng tin cậy. Cấu trúc đối tượng cơ bản có thể khác nhau nhưng đó không phải là thách thức đối với trình tối ưu hóa điểm nóng. Vì vậy, nó phụ thuộc vào các điều kiện xung quanh khác sẽ mang lại sự thực thi nhanh hơn, nếu có bất kỳ sự khác biệt nào.

Kết hợp hai trường hợp bộ lọc sẽ tạo ra nhiều đối tượng hơn và do đó nhiều mã ủy nhiệm hơn nhưng điều này có thể thay đổi nếu bạn sử dụng tham chiếu phương thức thay vì biểu thức lambda, ví dụ thay thế filter(x -> x.isCool())bằng filter(ItemType::isCool). Bằng cách đó, bạn đã loại bỏ phương thức ủy nhiệm tổng hợp được tạo cho biểu thức lambda của bạn. Vì vậy, việc kết hợp hai bộ lọc bằng hai tham chiếu phương thức có thể tạo ra mã ủy nhiệm giống nhau hoặc ít hơn so với một lệnh filtergọi sử dụng biểu thức lambda với &&.

Nhưng, như đã nói, loại chi phí này sẽ bị loại bỏ bởi trình tối ưu hóa HotSpot và không đáng kể.

Về lý thuyết, hai bộ lọc có thể dễ dàng song song hơn một bộ lọc nhưng điều đó chỉ phù hợp với các tác vụ cường độ khá tính toán.

Vì vậy, không có câu trả lời đơn giản.

Điểm mấu chốt là, đừng nghĩ về sự khác biệt hiệu suất như vậy dưới ngưỡng phát hiện mùi. Sử dụng những gì dễ đọc hơn.


¹ Khắc và sẽ yêu cầu triển khai thực hiện xử lý song song các giai đoạn tiếp theo, một con đường hiện không được thực hiện bởi triển khai Luồng tiêu chuẩn


4
không phải mã phải lặp lại luồng kết quả sau mỗi bộ lọc?
jucardi

13
@Juan Carlos Diaz: không, các luồng không hoạt động theo cách đó. Đọc về đánh giá lười biếng của người Viking; hoạt động trung gian không làm gì cả, chúng chỉ làm thay đổi kết quả của hoạt động đầu cuối.
Holger

34

Một điều kiện lọc phức tạp là tốt hơn trong phối cảnh hiệu suất, nhưng hiệu suất tốt nhất sẽ hiển thị thời trang cũ cho vòng lặp với một tiêu chuẩn if clauselà lựa chọn tốt nhất. Sự khác biệt trên một mảng nhỏ 10 phần tử chênh lệch có thể ~ 2 lần, đối với một mảng lớn, sự khác biệt không lớn.
Bạn có thể xem dự án GitHub của tôi , nơi tôi đã thực hiện các bài kiểm tra hiệu năng cho nhiều tùy chọn lặp mảng

Đối với mảng nhỏ 10 phần tử thông lượng ops / s: Mảng 10 phần tử Đối với trung bình 10.000 phần tử thông lượng ops / s: nhập mô tả hình ảnh ở đây Đối với mảng lớn 1.000.000 phần tử thông lượng ops / s: Yếu tố 1M

LƯU Ý: các bài kiểm tra chạy trên

  • 8 CPU
  • RAM 1 GB
  • Phiên bản hệ điều hành: 16.04.1 LTS (Xenial Xerus)
  • phiên bản java: 1.8.0_121
  • jvm: -XX: + Sử dụngG1GC -server -Xmx1024m -Xms1024m

CẬP NHẬT: Java 11 có một số tiến bộ về hiệu năng, nhưng tính năng động vẫn giữ nguyên

Chế độ điểm chuẩn: Thông lượng, ops / time Java 8vs11


22

Thử nghiệm này cho thấy tùy chọn thứ hai của bạn có thể thực hiện tốt hơn đáng kể. Phát hiện đầu tiên, sau đó là mã:

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}

bây giờ mã

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());
}

3
Thú vị - khi tôi thay đổi thứ tự để chạy test2 TRƯỚC test1, test1 chạy chậm hơn một chút. Chỉ khi test1 chạy đầu tiên thì nó có vẻ nhanh hơn. Bất cứ ai có thể tái tạo điều này hoặc có bất kỳ hiểu biết?
Sperr

5
Có thể là do chi phí biên dịch HotSpot phải chịu bởi bất kỳ thử nghiệm nào được chạy trước.
DaBlick

@Sperr bạn đúng, khi đơn hàng thay đổi, kết quả không thể đoán trước. Nhưng, khi tôi chạy cái này với ba luồng khác nhau, bộ lọc luôn phức tạp sẽ cho kết quả tốt hơn, bất kể luồng nào bắt đầu trước. Dưới đây là kết quả. 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

Đây là kết quả của 6 kết hợp thử nghiệm mẫu khác nhau được chia sẻ bởi @Hank D Rõ ràng là vị ngữ của hình thức u -> exp1 && exp2có hiệu suất cao trong tất cả các trường hợp.

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}
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.