자바 8 : 스트림과 컬렉션의 성능
저는 Java 8을 처음 사용합니다. 여전히 API에 대해 잘 모르지만 새로운 Streams API의 성능과 좋은 이전 컬렉션을 비교하기 위해 작은 비공식 벤치 마크를 만들었습니다.
테스트는의 목록을 필터링하고 Integer
각 짝수에 대해 제곱근을 계산하여 결과 List
로 저장합니다 Double
.
코드는 다음과 같습니다.
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)
이 특정 테스트의 경우 스트림이 컬렉션보다 약 두 배 느리며 병렬 처리가 도움이되지 않습니다 (또는 내가 잘못 사용하고 있습니까?).
질문 :
- 이 시험은 공정합니까? 내가 실수 했어?
- 스트림이 컬렉션보다 느립니까? 누구든지 이것에 대한 좋은 공식 벤치 마크를 만들었습니까?
- 어떤 접근법을 위해 노력해야합니까?
결과가 업데이트되었습니다.
I ran the test 1k times after JVM warmup (1k iterations) as advised by @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)
In this case streams are more performant. I wonder what would be observed in an app where the filtering function is only called once or twice during runtime.
Stop using
LinkedList
for anything but heavy removing from the middle of the list using iterator.Stop writing benchmarking code by hand, use JMH.
Proper benchmarks:
@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)));
}
}
Result:
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
Just as I expected stream implementation is fairly slower. JIT is able to inline all lambda stuff but doesn't produce as perfectly concise code as vanilla version.
Generally, Java 8 streams is not a magic. They couldn't speedup already well-implemented things (with, probably, plain iterations or Java 5's for-each statements replaced with Iterable.forEach()
and Collection.removeIf()
calls). Streams are more about coding convenience and safety. Convenience -- speed tradeoff is working here.
1) You see time less than 1 second using you benchmark. That means there can be strong influence of side effects on your results. So, I increased your task 10 times
int max = 10_000_000;
and ran your benchmark. My results:
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)
without edit (int max = 1_000_000
) results were
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)
It's like your results: stream is slower than collection. Conclusion: much time were spent for stream initialization/values transmitting.
2) After increasing task stream became faster (that's OK), but parallel stream remained too slow. What's wrong? Note: you have collect(Collectors.toList())
in you command. Collecting to single collection essentially introduces performance bottleneck and overhead in case of concurrent execution. It is possible to estimate the relative cost of overhead by replacing
collecting to collection -> counting the element count
For streams it can be done by collect(Collectors.counting())
. I got results:
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)
That' s for a big task! (int max = 10000000
) Conclusion: collecting items to collection took majority of time. The slowest part is adding to list. BTW, simple ArrayList
is used for Collectors.toList()
.
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);
}
I change the code a bit, ran on my mac book pro which has 8 cores, I got a reasonable result:
Collections: Elapsed time: 1522036826 ns (1.522037 seconds)
Streams: Elapsed time: 4315833719 ns (4.315834 seconds)
Parallel streams: Elapsed time: 261152901 ns (0.261153 seconds)
For what you are trying to do, I would not use regular java api's anyway. There is a ton of boxing/unboxing going on, so there is a huge performance overhead.
Personally I think that a lot of API designed are crap because they create a lot of object litter.
Try to use a primitive arrays of double/int and try to do it single threaded and see what the performance is.
PS: You might want to have a look at JMH to take care of doing the benchmark. It takes care of some of the typical pitfalls like warming up the JVM.
참고URL : https://stackoverflow.com/questions/22658322/java-8-performance-of-streams-vs-collections
'Programming' 카테고리의 다른 글
var_dump, var_export 및 print_r의 차이점 (0) | 2020.07.11 |
---|---|
내 리눅스 시스템에서 모든 사람을 위해 환경 변수를 설정하는 방법은 무엇입니까? (0) | 2020.07.11 |
$ _POST 대 $ _SERVER [ 'REQUEST_METHOD'] == 'POST' (0) | 2020.07.11 |
MySQL에서 빈 값을 허용하는 고유 제한 (0) | 2020.07.11 |
객체를 JSON 문자열로 변환 (0) | 2020.07.11 |