안녕하세요. 개발하는 정주입니다.
오늘은 "for-in과 고차함수(forEach, map, filter, reduce) 시간 비교"에 대해 알아보겠습니다.
이미 많은 자료가 있으나 직접 테스트 해보면 좋을 것 같아 진행하였습니다.
하지만 아직 부족한 실력으론 시간 차이가 발생하는 이유까지는 알아내지 못했습니다 ㅠㅠ
혹시 아시는 분이 계시다면 댓글로 알려주시면 감사하겠습니다!
테스트 방법
테스트 환경 : Xcode 13.3.1 / Swift 5 / MacBook Pro(16형, 2021년 모델)
반복 횟수 : 1000만 * 10회 (총 1억 회)
출력 방법 : 1000만 회를 반복할 때마다 수행 시간을 출력, 마지막에 전체 수행 시간을 출력하였습니다.
시간 측정 메서드
public func measureTime(_ closure: () -> ()) -> TimeInterval {
let startDate = Date()
closure()
return Date().timeIntervalSince(startDate)
}
변수
let time = 10000000
var forResult: TimeInterval = 0.0, highResult: TimeInterval = 0.0
time : 반복 횟수입니다.
forResult : for문의 전체 실행 시간입니다.
highResult : 고차함수의 전체 실행 시간입니다.
for-in과 forEach
첫 번째로 for-in과 forEach 비교입니다.
forEach도 고차함수 중에 하나로 반복문과는 차이가 있습니다.
for-in과 forEach의 차이점에 대해 다루는 포스팅은 아니므로...
for-in은 반복문, forEach는 반복 메서드정도로 생각하시면 될 것 같습니다.
암튼 수행시간을 바로 비교해보죠!
테스트 코드
for i in 1...10 {
let result = measureTime {
for i in 0..<time { }
}
forResult += result
print("[\(i)] for 1000만: \(result)(s)")
}
for i in 1...10 {
let result = measureTime {
(0..<time).forEach { _ in }
}
highResult += result
print("[\(i)] forEach 1000만: \(result)(s)")
}
반복문 안에서 어떠한 작업도 하지 않는 순수한 반복 시간을 측정해보았습니다.
결과
for-in과 forEach 모두 1000만 회당 3.4초정도 소요되었습니다.
1억 회를 반복한 시간은 for-in은 33.78초, forEach는 34.28초입니다.
for-in이 forEach보다 1.01배 더 빨랐지만 유의미한 차이는 없었습니다.
테스트 전에는 아무래도 고차함수인 forEach가 더 빠르지 않을까? 생각을 했었는데요.
오히려 for-in이 빠른 것을 보고 좀 놀랐습니다. 메서드를 호출하는 과정이 추가가 되서 그럴까요?
흥미로운 결과였습니다.
for-in과 map - 1
for-in과 map을 비교했습니다.
map과의 비교는 두 가지 케이스로 나눠 테스트 하였습니다.
아마 map은 가장 많이 쓰이는 고차함수가 아닐까 싶은데요! 개인적으로 가장 결과가 궁금했던 테스트였습니다.
테스트 코드
for i in 0..<10 {
var arr: [Int] = []
let result = measureTime {
for i in 0..<time {
arr.append(i)
}
}
forResult += result
print("[\(i)] for 1000만: \(result)(s)")
}
for i in 0..<10 {
let result = measureTime {
let arr = (0..<time).map { $0 }
}
highResult += result
print("[\(i)] map 1000만: \(result)(s)")
}
0 ~ 1000만-1까지 반복하였을 때 index를 원소로 갖는 배열을 생성하도록 하였습니다.
결과
for-in은 1000만 회당 3.4초, map은 3.1초가 소요되었습니다.
총 소요시간은 for-in은 34.69초, map은 31.42초이며 map이 1.1배 더 빨랐습니다.
forEach와는 다르게 충분히 유의미한 속도 차이를 보여주었습니다.
이 결과를 통해 단순한 배열 생성은 for-in보다 map이
코드 길이, 가독성, 속도면에서 더 효율적이라는 것을 알 수 있습니다.
for-in과 map - 2
하지만 1번 테스트처럼 단순히 배열 생성만 하는 것은 실제 사용법과는 거리가 있습니다.
map은 기존 배열에 어떠한 연산 처리를 하여 새로운 배열을 만들 때 사용하기 때문입니다.
따라서 이러한 시나리오도 테스트 해보았습니다.
테스트 코드
var testArray: [Int] = Array(repeating: 1, count: time)
for i in 0..<10 {
var arr: [Int] = []
let result = measureTime {
for i in testArray {
arr.append(i * 2 / 2 + 10)
}
}
forResult += result
print("[\(i)] for 1000만: \(result)(s)")
}
for i in 0..<10 {
let result = measureTime {
let arr = testArray.map { $0 * 2 / 2 + 10 }
}
highResult += result
print("[\(i)] map 1000만: \(result)(s)")
}
1로 채워진 1000만 길이짜리 배열을 미리 생성하였습니다.
이 배열에서 값을 읽어 연산을 처리한 결과를 새로운 배열로 만듭니다.
괜히 연산 좀 넣어보고 싶어서 의미는 없지만 2를 곱하고 2를 나눠주었습니다.
결과
for-in은 1000만 회당 1초, map은 1000만 회당 0.8초가 소요되었습니다.
총 소요시간은 for-in은 10.32초, map은 7.95초로 무려 1.3배나 차이가 났습니다.
그보다 더 놀란 점은 range로 반복한 것과 특정 배열에서 원소를 뽑아낸 것의 시간 차이가 상당히 난다는 점입니다.
제가 테스트를 잘못 했나? 하는 마음에 여러 번 시도해보았는데요.
모든 결과에서 for-in은 3배, map은 약 4.5배 차이가 났습니다.
이 결과는 흥미로워서 좀 더 확인을 해보려고 합니다.
만약 항상 0~1000만 회 반복한다면 1000만 짜리 배열 하나 만들어서 반복문을 돌리는 것이 시간 비용이 훨씬 싸겠구나 생각이 들었습니다. 물론 공간 비용은 훨씬 비싸겠지만요... ㅎ
for-in과 map의 시간 차이 외에도 귀한 지식을 얻게 해준 테스트였네요.
for-in과 filter
for-in과 filter를 비교해보았습니다.
filter도 특정 조건에 맞는 원소를 뽑아내는 데 유용하여 자주 썼던 고차 함수입니다.
지금까지는 가독성을 기준으로 for-in을 쓸지 filter를 쓸지 결정했었는데요.
과연 시간 차이는 얼마나 날지 궁금합니다.
테스트 코드
var testArray: [Int] = (0..<time).map { $0 }
for i in 0..<10 {
var arr: [Int] = []
let result = measureTime {
for i in testArray {
if i % 2 == 0 {
arr.append(i)
}
}
}
forResult += result
print("[\(i)] for 1000만: \(result)(s)")
}
for i in 0..<10 {
let result = measureTime {
let arr = testArray.filter { $0 % 2 == 0 }
}
highResult += result
print("[\(i)] filter 1000만: \(result)(s)")
}
이번에도 테스트 배열을 하나 미리 생성하였습니다.
이 테스트 배열에서 짝수인 원소만 뽑아낸 배열을 생성하였습니다.
for-in으로 filter를 구현할 때 중요한 점은 remove가 아닌 새로운 배열에 append를 해주어야 한다는 점입니다!
filter 자체가 새로운 배열을 return 하기도 하고 중간 index의 remove는 O(n)으로 상당히 느린 연산이기 때문입니다.
결과
for-in은 스크린샷이 조금 잘렸습니다 ㅠㅠ
for-in은 1000만 회당 1초, filter도 1000만 회당 1초가 소요되었습니다.
총 소요시간은 for-in은 10.44초, filter는 10.68초로 for-in이 1.02배 더 빨랐습니다.
같은 조건으로 4회정도 더 반복했을 때 항상 for-in이 더 빠른 결과가 나왔습니다.
지금까지는 filter를 애용했는데 오히려 for-in이 더 빠르다니 무척 놀랍습니다.
하지만 시간 차이는 유의미할 정도는 아니므로 가독성을 고려하여 코드를 작성하는 것이 좋겠습니다.
일반적으로 조건이 간단할 경우 filter가 훨씬 가독성이 좋고 조건이 복잡하다면 for-in을 사용하는 게 더 좋겠네요!
for-in과 reduce
reduce는 배열의 합을 구할 때 유용한 고차 함수이죠?
저도 간단한 합 연산은 reduce를 애용하고 있는데요. 시간 차이가 얼마나 날지 확인해 봅시다.
테스트 코드
var testArray: [Int] = (0..<time).map { $0 }
for i in 0..<10 {
var sum = 0
let result = measureTime {
for i in testArray {
sum += i
}
}
forResult += result
print("[\(i)] for 1000만: \(result)(s)")
}
for i in 0..<10 {
let result = measureTime {
var sum = testArray.reduce(0, +)
}
highResult += result
print("[\(i)] reduce 1000만: \(result)(s)")
}
for-in은 sum이라는 변수에 testArray 원소를 더해주었고 reduce는 reduce의 결과를 sum에 대입해주었습니다.
sum은 모든 반복에 대해 동일한 값을 출력한 것을 확인하였습니다.
결과
for-in은 1000만 회당 0.9초, reduce는 1000만 회당 0.9초가 소요되었습니다.
for-in은 총 9.08초, reduce는 총 9.26초 소요되었으며 for-in이 1.02배 더 빨랐습니다.
for-in과 reduce의 시간 차이도 유의미하지는 않았습니다.
하지만 위 코드만 하더라도 reduce의 코드가 훨씬 짧고 직관적이라는 것을 알 수 있습니다.
따라서 for-in과 reduce도 코드나 조건 상황에 맞춰 사용하면 적절하겠습니다.
여기까지의 결과에서 흥미로운 점은 filter와 reduce 모두 1.02배 차이가 난다는 것인데요.
단순 우연인지 아니면 어떤 이유가 있는지 궁금하네요.
더 찾아보고 알게되면 보충하여 포스팅 작성하겠습니다 ^^!
for-in과 고차 함수 종합 테스트
그렇다면 for-in에서 append, 조건부 append, 합 연산을 하는 것과
map, filter, reduce를 따로 호출하는 것 중 어떤 것이 유리할까? 하는 궁금증이 생깁니다.
바로 확인해보자구요.
테스트 코드
var testArray: [Int] = (0..<time).map { $0 }
for i in 0..<10 {
var sum = 0
var arr: [Int] = [], arr2: [Int] = []
let result = measureTime {
for i in testArray {
arr.append(i)
if i % 2 == 0 {
arr2.append(i)
}
sum += i
}
}
forResult += result
print("[\(i)] for 1000만: \(result)(s)")
}
for i in 0..<10 {
let result = measureTime {
var arr = testArray.map { $0 }
var arr2 = testArray.filter { $0 % 2 == 0 }
var sum = testArray.reduce(0, +)
}
highResult += result
print("[\(i)] All 1000만: \(result)(s)")
}
for-in은 테스트 배열의 원소를 arr에 append, arr2에 짝수만 append, sum에 합 연산을 해주었습니다.
고차함수에는 map과 filter, reduce를 순서대로 진행했습니다.
map과 filter, reduce는 함께 사용되는 경우가 많으므로 실제 사용과 동 떨어진 테스트가 아니었기에
두근 두근하며 실행 시켜보았습니다.
결과
for-in은 1000만 회당 1.1초로 타 결과와 별반 다르지 않았습니다.
고차함수 종합은 1000만 회당 2.7초로 map + filter + reduce의 수행 속도를 더한 결과가 나왔습니다.
결국, for-in은 별반 다르지 않은 11.48초, 고차함수들은 27.20초로 for-in이 2.37배 더 빨랐습니다.
어쩌면 당연한 결과로 보이지만 지금까지 생각을 못했다는 것이 민망한 결과였습니다.
고차함수가 훨씬 빠르겠지? 하며 반복문을 하찮게 여긴 과거가 부끄러웠네요...
마무리
이번 테스트를 통해 반복문과 고차함수는 적절히 섞어 사용해야 한다는 점을 배웠습니다.
지금까지는 웬만하면 고차함수를 써야지! 했는데요.
map은 예상대로 더 빨랐지만 filter는 비슷한 속도였고 reduce는 오히려 느렸습니다.
1000만 회를 반복할 일이 많진 않겠지만 스노우볼처럼 쌓이고 쌓이는 것이라 생각하면 무시할 정도는 아닙니다.
특히 마지막처럼 고차함수를 여러 번 사용하는 것보다 코드가 좀 길어져도 for-in 하나로 처리하는 것이
훨씬 효율적이라는 것을 배웠습니다.
가독성과 속도를 고려하여 문제에 적절한 것을 선택하는 연습을 해야겠네요!
감사합니다!
아직은 초보 개발자입니다.
더 효율적인 코드 훈수 환영합니다!
공감과 댓글 부탁드립니다.