가능하면 항상 병렬 스트림을 사용해야합니까?


514

Java 8 및 람다를 사용하면 컬렉션을 스트림으로 반복하고 병렬 스트림을 사용하기가 쉽습니다. docs의 두 가지 예 , parallelStream을 사용하는 두 번째 예 :

myShapesCollection.stream()
    .filter(e -> e.getColor() == Color.RED)
    .forEach(e -> System.out.println(e.getName()));

myShapesCollection.parallelStream() // <-- This one uses parallel
    .filter(e -> e.getColor() == Color.RED)
    .forEach(e -> System.out.println(e.getName()));

주문에 신경 쓰지 않는 한 항상 병렬을 사용하는 것이 유리합니까? 더 많은 코어에서 작업을 빠르게 나누는 것이 더 빠르다고 생각할 것입니다.

다른 고려 사항이 있습니까? 병렬 스트림은 언제 사용해야하고 비 병렬은 언제 사용해야합니까?

(이 질문은 항상 병렬 스트림을 사용하는 것이 좋은 생각이 아니라 병렬 스트림을 사용하는 방법과시기에 대한 토론을 시작하도록 요청됩니다.)

답변:


735

병렬 스트림은 순차 스트림에 비해 오버 헤드가 훨씬 높습니다. 스레드를 조정하는 데 상당한 시간이 걸립니다. 기본적으로 순차적 스트림을 사용하고 병렬 스트림 만 고려합니다.

  • 처리 할 대량의 항목이 있습니다 (또는 각 항목을 처리하는 데 시간이 걸리고 병렬화 가능)

  • 처음에 성능 문제가 있습니다

  • 이미 다중 스레드 환경에서 프로세스를 실행하지 않습니다 (예 : 웹 컨테이너에서 이미 병렬로 처리 할 요청이 많은 경우 각 요청 내에 병렬 계층을 추가하면 긍정적 효과보다 부정적인 영향을 줄 수 있음) )

귀하의 예에서에 대한 동기화 된 액세스에 의해 성능이 어쨌든 구동 되며이 System.out.println()프로세스를 병렬로 만들면 아무런 영향을 미치지 않거나 심지어 부정적인 영향을 미칩니다.

또한 병렬 스트림이 모든 동기화 문제를 마술처럼 해결하지는 않습니다. 프로세스에서 사용되는 술어 및 함수가 공유 자원을 사용하는 경우 모든 것이 스레드로부터 안전해야합니다. 특히, 부작용은 당신이 평행하게 갈 때 정말로 걱정해야 할 것들입니다.

어쨌든 측정하지 마십시오! 병렬 처리가 가치가 있는지 아닌지 측정 만 할 수 있습니다.


18
좋은 대답입니다. 처리 할 항목이 많으면 스레드 조정 문제 만 증가한다고 덧붙입니다. 각 항목을 처리하는 데 시간이 걸리고 병렬화가 유용 할 수있는 병렬화가 가능한 경우에만 해당됩니다.
Warren Dew

16
@WarrenDew 나는 동의하지 않는다. 포크 / 조인 시스템은 간단히 N 개의 항목을 예를 들어 4 개의 파트로 나누고이 4 개의 파트를 순차적으로 처리합니다. 그러면 4 개의 결과가 줄어 듭니다. 대규모 장치 처리 속도가 엄청나게 큰 경우에도 병렬 처리가 효과적 일 수 있습니다. 그러나 항상 그렇듯이 측정해야합니다.
JB 니 제트

나는 그것들을 사용하기 위해 Runnable호출하는 객체 모음을 가지고 있는데 , 병렬로 Java 8 스트림을 사용하도록 변경해도 괜찮 습니까? 그런 다음 클래스에서 스레드 코드를 제거 할 수 있습니다. 그러나 단점이 있습니까? start()Threads.forEach()
ycomp

1
@JBNizet 4 개의 파트가 순차적으로 처리되면 프로세스 병렬 또는 순차적으로 알고 있다는 차이가 없습니까? Pls 명확히
Harshana

3
@Harshana는 분명히 4 개의 각 요소가 순차적으로 처리 될 것임을 의미합니다. 그러나 부품 자체는 동시에 처리 될 수 있습니다. 즉, 사용 가능한 여러 CPU 코어가있는 경우 각 파트는 다른 요소와 독립적으로 자체 코어에서 실행될 수 있으며 자체 요소를 순차적으로 처리 할 수 ​​있습니다. (참고 : 이것이 병렬 Java 스트림이 작동하는 방식 인 경우 모르겠습니다. JBNizet의 의미를 명확하게하기 위해 노력하고 있습니다.)
내일

258

Stream API는 실행 방식에서 추상화 된 방식으로 계산을 쉽게 작성하고 순차적 및 병렬 간을 쉽게 전환 할 수 있도록 설계되었습니다.

그러나 그것이 쉽다고해서 항상 좋은 생각을 의미하는 것은 아니며, 사실, 당신이 할 수 있기 때문에 모든 곳 을 버리는 것은 나쁜 생각 .parallel()입니다.

첫째, 병렬 처리는 더 많은 코어를 사용할 수있는 경우 빠른 실행 가능성 외에 다른 이점을 제공하지 않습니다. 병렬 실행에는 순차적 인 작업보다 항상 더 많은 작업이 필요합니다. 문제를 해결하는 것 외에도 하위 작업의 디스패치 및 조정도 수행해야하기 때문입니다. 여러 프로세서에서 작업을 분할하여 더 빨리 답변을 얻을 수 있기를 바랍니다. 이것이 실제로 발생하는지 여부는 데이터 세트의 크기, 각 요소에 대해 수행하는 계산량, 계산의 특성을 포함하여 많은 일에 달려 있습니다 (특히 한 요소의 처리가 다른 요소의 처리와 상호 작용합니까?) , 사용 가능한 프로세서 수 및 해당 프로세서와 경쟁하는 다른 작업 수입니다.

또한, 병렬 처리는 종종 순차적 구현에 의해 숨겨지는 계산에서 비결정론을 종종 노출한다는 점에 유의하십시오. 때때로 이것은 중요하지 않거나 관련된 작업을 제한하여 완화 할 수 있습니다 (즉, 축소 연산자는 상태 비 저장 및 연관이어야 함).

실제로 병렬 처리로 인해 계산 속도가 빨라지고 때로는 계산 속도가 느려지지 않으며 때로는 속도가 느려질 수도 있습니다. 순차적 실행을 사용하여 먼저 개발 한 다음 병렬 처리를 적용하는 것이 가장 좋습니다.

(A) 실제로 성능 향상에 이점이 있다는 것을 알고

(B) 실제로 향상된 성능을 제공 할 것입니다.

(A)는 기술적 문제가 아닌 비즈니스 문제입니다. 성능 전문가 인 경우 일반적으로 코드를보고 (B)를 결정할 수 있지만 현명한 길은 측정하는 것입니다. (그리고 (A)를 확신 할 때까지 귀찮게하지 마십시오. 코드가 충분히 빠르면 뇌주기를 다른 곳에 적용하는 것이 좋습니다.)

병렬 처리의 가장 간단한 성능 모델은 "NQ"모델입니다. 여기서 N은 요소 수이고 Q는 요소 당 계산입니다. 일반적으로 성능 이점을 얻으려면 제품 NQ가 일부 임계 값을 초과해야합니다. "1에서 N까지의 숫자 추가"와 같은 Q가 낮은 문제의 경우 일반적으로 N = 1000에서 N = 10000 사이의 손익분기 점이 표시됩니다. Q 문제가 높을수록 더 낮은 임계 값에서 손익분기 점이 발생합니다.

그러나 현실은 매우 복잡합니다. 따라서 전문 지식을 얻을 때까지 순차적 처리로 인해 실제로 비용이 드는 시점을 파악한 다음 병렬 처리가 도움이되는지 측정하십시오.


18
이 게시물은 NQ 모델에 대한 자세한 내용을 제공합니다. gee.cs.oswego.edu/dl/html/StreamParallelGuidance.html
Pino

4
@specializt : 스트림을 순차적에서 병렬로 전환하면 알고리즘 변경됩니다 (대부분의 경우). 여기에 언급 된 결정은 (임의의) 연산자 신뢰할 수있는 속성 ( 스트림 구현은 알 수 없음)과 관련이 있지만 물론 의존 해서는 안됩니다 . 이것이이 답변의 해당 섹션에서 말한 내용입니다. 당신이 규칙을 걱정하는 경우, 당신은 당신이 (그렇지 않으면 병렬 스트림은 아주 쓸모했다)라고하지만, 사용할 때와 같은 의도적으로 허용되지 않은 결정론의 가능성도있다처럼 결정적 결과를 가질 수 findAny대신 findFirst...
홀거

4
"먼저 병렬 처리는 더 많은 코어를 사용할 수있을 때 더 빠른 실행 가능성 외에 다른 이점을 제공하지 않습니다."또는 IO와 관련된 작업을 적용하는 경우 (예 :) myListOfURLs.stream().map((url) -> downloadPage(url))....
Jules

6
@Pacerier 훌륭한 이론이지만 슬프게도 순진합니다 (처음으로 자동 병렬화 컴파일러를 구축하려는 30 년의 역사를보십시오). 필연적으로 잘못 될 때 사용자를 성가 시게하지 않을만큼 충분한 시간을 추측하는 것은 실용적이지 않기 때문에 사용자가해야 할 일은 사용자가 원하는 것을 말하게하는 것이 었습니다. 대부분의 상황에서 기본값 (순차적)이 정확하고 더 예측 가능합니다.
Brian Goetz

2
@ 줄 : IO에 병렬 스트림을 사용하지 마십시오. 이는 CPU 집약적 인 작업만을위한 것입니다. 병렬 스트림을 사용하므로 ForkJoinPool.commonPool()차단 작업을 원하지 않습니다.
R2C2

68

나는 중 하나 지켜 프리젠 테이션브라이언 게츠 (람다 표현식에 대한 Java 언어 설계자 및 사양 리드) . 그는 병렬화를 진행하기 전에 고려해야 할 다음 4 가지 사항을 자세히 설명합니다.

분할 / 분해 비용
– 때로는 분할 작업보다 단순히 분할 비용 이 더 비쌉니다!
작업 파견 / 관리 비용
– 다른 스레드로 작업을 처리하는 데 많은 시간을 할애 할 수 있습니다.
결과 조합 비용
– 때로는 많은 양의 데이터를 복사해야합니다. 예를 들어, 숫자를 추가하는 것은 저렴하지만 세트를 병합하는 것은 비쌉니다.
지역
– 방에있는 코끼리. 이것은 모두가 놓칠 수있는 중요한 포인트입니다. 캐시 미스로 인해 CPU가 데이터를 기다리는 경우 캐시 미스를 고려해야합니다. 그러면 병렬화를 통해 아무것도 얻지 못합니다. 그렇기 때문에 다음 인덱스 (현재 인덱스 근처)가 캐시되고 CPU가 캐시 미스를 경험할 가능성이 적어짐에 따라 어레이 기반 소스가 최상의 병렬 처리를하는 이유입니다.

또한 병렬 속도 향상의 가능성을 결정하는 비교적 간단한 공식에 대해서도 언급합니다.

NQ 모델 :

N x Q > 10000

여기서
N = 데이터 항목 수
Q = 항목 당 작업량


13

JB가 머리에 못을 박았다. 내가 추가 할 수있는 유일한 것은 Java 8이 순수한 병렬 처리를 수행하지 않는다는 것 입니다. 예, 기사를 쓰고 30 년 동안 F / J를 해왔으므로 문제를 이해하고 있습니다.


10
스트림은 외부 대신 내부 반복을 수행하므로 스트림을 반복 할 수 없습니다. 그것이 어쨌든 스트림의 전체 이유입니다. 학업에 문제가 있다면 기능적 프로그래밍이 적합하지 않을 수 있습니다. 기능 프로그래밍 === 수학 === 학문. 그리고 J8-FJ가 고장 나지 않았습니다. 단지 대부분의 사람들이 f ****** 매뉴얼을 읽지 않는 것입니다. 자바 문서는 병렬 실행 프레임 워크가 아니라고 매우 분명합니다. 이것이 모든 스플리터의 모든 이유입니다. 예, 학업 적이며, 사용 방법을 알고 있으면 작동합니다. 예. 커스텀 실행기를 사용하는 것이 더
쉬워야합니다

1
Stream에는 iterator () 메서드가 있으므로 원하는 경우 외부에서 반복 할 수 있습니다. 내 이해는 반복자를 한 번만 사용할 수 있고 아무도 괜찮은지를 결정할 수 없기 때문에 Iterable을 구현하지 않는다는 것입니다.
Trejkaz 2016 년

14
솔직히 말해서 : 전체 용지가 대규모, 정교한 호언 장담처럼 읽고 - 그리고 거의 그것의 신뢰성을 부정하는 것이 ... 나는 그것을 재 - 일을 권 해드립니다 훨씬 덜 공격적 저음 그렇지 않으면 많은 사람들이 실제로 완전히 읽어 귀찮게하지 않습니다 ... im 그냥 sayan
specializt

기사에 대한 몇 가지 질문 ... 우선, 왜 왜 균형 잡힌 나무 구조를 방향성 비순환 그래프와 동일시합니까? 예, 균형 나무 입니다 연결리스트이며, 거의 모든 객체 지향 데이터 구조 다른 배열에 비해 너무 DAG를하지만. 또한 재귀 적 분해는 균형 잡힌 나무 구조에서만 작동하므로 상업적으로 관련이 없다고 주장 할 때 어설 션을 어떻게 정당화합니까? 배열 기반 데이터 구조 (예 : /) 에서도 잘 작동하는 것으로 보입니다 (실제로 심도있는 문제를 조사하지 않고 있음) . ArrayListHashMap
Jules

1
이 스레드는 2013 년부터 시작되었으며 그 이후로 많은 변화가있었습니다. 이 섹션은 자세한 답변이 아닌 설명입니다.
14:15에

3

다른 답변은 병렬 처리에서 조기 최적화 및 오버 헤드 비용을 피하기 위해 이미 프로파일 링을 다루었습니다. 이 답변에서는 병렬 스트리밍을위한 이상적인 데이터 구조 선택에 대해 설명합니다.

원칙적으로, 병렬 처리의 성능 향상을 통해 스트림에 가장 적합합니다 ArrayList, HashMap, HashSet, 및 ConcurrentHashMap인스턴스; 배열; int범위; 그리고 long범위. 이러한 데이터 구조의 공통점은 모두 원하는 크기의 하위 범위로 정확하고 저렴하게 분할 할 수있어 병렬 스레드간에 작업을 쉽게 분할 할 수 있다는 것입니다. 이 작업을 수행하기 위해 스트림 라이브러리가 사용하는 추상화는 spliterator이며 spliteratoron Stream및 메소드에서 리턴됩니다 Iterable.

이러한 모든 데이터 구조가 공통적으로 갖는 또 다른 중요한 요소는 순차적으로 처리 될 때 우수한 참조 위치를 제공한다는 점입니다. 순차 요소 참조는 메모리에 함께 저장됩니다. 이러한 참조에 의해 참조되는 객체는 메모리에서 서로 가까이 있지 않을 수 있으며, 이는 참조의 지역성을 감소시킵니다. 참조 영역은 대량 작업을 병렬화하는 데 매우 중요합니다. 스레드가 없으면 스레드가 많은 시간을 유휴 상태로 보내고 메모리에서 프로세서의 캐시로 데이터가 전송 될 때까지 대기합니다. 최상의 참조 위치를 가진 데이터 구조는 데이터 자체가 연속적으로 메모리에 저장되므로 기본 배열입니다.

출처 : 항목 # 48 Joshua Bloch의 스트림을 병렬, 효과적인 Java 3e로 만들 때주의 사용


2

무한 스트림을 제한과 병렬화하지 마십시오. 다음과 같은 일이 발생합니다.

    public static void main(String[] args) {
        // let's count to 1 in parallel
        System.out.println(
            IntStream.iterate(0, i -> i + 1)
                .parallel()
                .skip(1)
                .findFirst()
                .getAsInt());
    }

결과

    Exception in thread "main" java.lang.OutOfMemoryError
        at ...
        at java.base/java.util.stream.IntPipeline.findFirst(IntPipeline.java:528)
        at InfiniteTest.main(InfiniteTest.java:24)
    Caused by: java.lang.OutOfMemoryError: Java heap space
        at java.base/java.util.stream.SpinedBuffer$OfInt.newArray(SpinedBuffer.java:750)
        at ...

사용하면 동일 .limit(...)

설명 : Java 8, 스트림에서 .parallel을 사용하면 OOM 오류가 발생합니다.

마찬가지로 스트림이 정렬되어 있고 처리하려는 것보다 훨씬 많은 요소가있는 경우 병렬을 사용하지 마십시오.

public static void main(String[] args) {
    // let's count to 1 in parallel
    System.out.println(
            IntStream.range(1, 1000_000_000)
                    .parallel()
                    .skip(100)
                    .findFirst()
                    .getAsInt());
}

병렬 스레드가 중요한 0-100 대신 많은 수의 범위에서 작동하여 시간이 오래 걸리기 때문에 훨씬 오래 실행될 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.