programing

신속한 성능 : map () 및 reduce () 대 for 루프

sourcetip 2021. 1. 15. 20:25
반응형

신속한 성능 : map () 및 reduce () 대 for 루프


Swift로 성능에 중요한 코드를 작성하고 있습니다. 생각할 수있는 모든 최적화를 구현하고 Instruments에서 응용 프로그램을 프로파일 링 한 후 대부분의 CPU주기가 Floats 배열 에서 수행 map()되고 reduce()작업 하는 데 소비된다는 것을 깨달았습니다 . 그래서, 그냥 무슨 일이 일어날 지보고, 나는의 모든 인스턴스를 교체 map하고 reduce좋은 구식으로 for루프를. 그리고 놀랍게도 ... for루프가 훨씬 빨라졌습니다!

이것에 약간 당혹스러워서 대략적인 벤치 마크를 수행하기로 결정했습니다. 한 테스트에서 map다음과 같은 간단한 산술을 수행 한 후 Floats 배열을 반환했습니다.

// Populate array with 1,000,000,000 random numbers
var array = [Float](count: 1_000_000_000, repeatedValue: 0)
for i in 0..<array.count {
    array[i] = Float(random())
}
let start = NSDate()
// Construct a new array, with each element from the original multiplied by 5
let output = array.map({ (element) -> Float in
    return element * 5
})
// Log the elapsed time
let elapsed = NSDate().timeIntervalSinceDate(start)
print(elapsed)

그리고 동등한 for루프 구현 :

var output = [Float]()
for element in array {
    output.append(element * 5)
}

평균 실행 시간 map: 20.1 초. for루프의 평균 실행 시간 : 11.2 초. 결과는 Floats 대신 Integers를 사용하여 유사했습니다.

Swift의 성능을 테스트하기 위해 유사한 벤치 마크를 만들었습니다 reduce. 이번에 는 하나의 큰 배열의 요소를 합산 할 때 루프 reducefor거의 동일한 성능을 달성했습니다. 그러나 다음과 같이 테스트를 100,000 번 반복하면 :

// Populate array with 1,000,000 random numbers
var array = [Float](count: 1_000_000, repeatedValue: 0)
for i in 0..<array.count {
    array[i] = Float(random())
}
let start = NSDate()
// Perform operation 100,000 times
for _ in 0..<100_000 {
    let sum = array.reduce(0, combine: {$0 + $1})
}
// Log the elapsed time
let elapsed = NSDate().timeIntervalSinceDate(start)
print(elapsed)

대 :

for _ in 0..<100_000 {
    var sum: Float = 0
    for element in array {
        sum += element
    }
}

reduce방법은 29 초가 걸리고 for루프는 0.000003 초가 걸립니다.

당연히 컴파일러 최적화의 결과로 마지막 테스트를 무시할 준비가되었지만 컴파일러가 루프와 Swift의 내장 배열 방법을 다르게 최적화하는 방법에 대한 통찰력을 줄 수 있다고 생각합니다. 모든 테스트는 2.5GHz i7 MacBook Pro에서 -Os 최적화로 수행되었습니다. 결과는 배열 크기와 반복 횟수에 따라 for달랐지만 루프는 항상 다른 방법보다 1.5 배 이상, 때로는 최대 10 배까지 성능이 뛰어났습니다.

여기 Swift의 성능에 대해 약간 당황합니다. 기본 제공 Array 메서드가 이러한 작업을 수행하는 순진한 접근 방식보다 빠르지 않습니까? 저보다 낮은 수준의 지식을 가진 사람이 상황에 대해 밝힐 수 있습니다.


기본 제공 Array 메서드가 이러한 작업을 수행하는 순진한 접근 방식보다 빠르지 않습니까? 저보다 낮은 수준의 지식을 가진 사람이 상황에 대해 밝힐 수 있습니다.

나는 그저 질문의이 부분과 개념적 수준에서 더 많은 것을 다루고 싶다. (내가 Swift의 옵티마이 저의 본질을 거의 이해하지 못한 채로) "반드시 아니다". Swift의 옵티마이 저의 특성에 대한 뿌리 깊은 지식보다 컴파일러 설계 및 컴퓨터 아키텍처의 배경에서 더 많이 나옵니다.

오버 헤드 호출

With functions like map and reduce accepting functions as inputs, it places a greater strain on the optimizer to put it one way. The natural temptation in such a case short of some very aggressive optimization is to constantly branch back and forth between the implementation of, say, map, and the closure you provided, and likewise transmit data across these disparate branches of code (through registers and stack, typically).

이러한 종류의 분기 / 호출 오버 헤드는 특히 Swift 클로저의 유연성을 고려할 때 옵티마이 저가 제거하기 매우 어렵습니다 (불가능하지는 않지만 개념적으로 매우 어렵습니다). C ++ 최적화 프로그램은 함수 개체 호출을 인라인 할 수 있지만 컴파일러가 map전달하는 각 함수 개체 유형에 대해 완전히 새로운 명령 집합을 효과적으로 생성해야하는 경우에 훨씬 더 많은 제한 사항과 코드 생성 기술이 필요합니다. 코드 생성에 사용되는 함수 템플릿을 나타내는 프로그래머).

따라서 손으로 구르는 루프가 더 빠르게 수행 될 수 있다는 사실은 그리 놀라운 일이 아닙니다. 최적화 프로그램에 많은 부담을주지 않습니다. 일부 사람들은 벤더가 루프를 병렬화하는 것과 같은 작업을 수행 할 수 있기 때문에 이러한 고차 함수가 더 빨리 진행될 수 있어야한다고 언급하는 것을 보았습니다. 그러나 루프를 효과적으로 병렬화하려면 먼저 일반적으로 다음과 같은 종류의 정보가 필요합니다. 옵티마이 저가 중첩 된 함수 호출을 손으로 굴린 루프만큼 저렴 해지는 지점까지 인라인 할 수 있습니다. 그렇지 않으면 전달하는 함수 / 클로저 구현이 다음과 같은 함수에 대해 효과적으로 불투명해질 것입니다.map/reduce: 그들은 단지 그것을 호출하고 그렇게하는 오버 헤드를 지불 할 수 있으며, 그렇게함으로써 부작용의 본질과 쓰레드 안전성에 대해 아무것도 가정 할 수 없기 때문에 그것을 병렬화 할 수 없습니다.

물론 이것은 모두 개념적입니다 .Swift는 미래에 이러한 경우를 최적화 할 수 있거나 이미 지금 그렇게 할 수있을 수 있습니다 ( -Ofast일부 안전을 희생하면서 Swift를 더 빠르게 진행할 수 있도록 일반적으로 인용되는 방법 참조 ) . 그러나 최소한 손으로 굴린 루프에서 이러한 종류의 기능을 사용하는 것은 옵티 마이저에 더 큰 부담을줍니다. 그리고 첫 번째 벤치 마크에서보고있는 시간 차이는 사람이 할 수있는 차이의 종류를 반영하는 것 같습니다. 이 추가 호출 오버 헤드로 예상됩니다. 알아내는 가장 좋은 방법은 어셈블리를보고 다양한 최적화 플래그를 시도하는 것입니다.

표준 기능

그것은 그러한 기능의 사용을 방해하는 것이 아닙니다. 의도를 더 간결하게 표현하고 생산성을 높일 수 있습니다. 그리고 그것들에 의존하면 여러분의 개입없이 Swift의 향후 버전에서 여러분의 코드베이스가 더 빨라질 수 있습니다. 하지만 항상 더 빠를 필요는 없습니다. 원하는 것을 더 직접적으로 표현하는 상위 수준 라이브러리 함수가 더 빨라질 것이라고 생각하는 것이 좋은 일반적인 규칙이지만 항상 예외가 있습니다. (그러나 여기에서 불신보다 신뢰의 측면에서 실수하는 것이 훨씬 낫기 때문에 프로파일 러를 손에 들고 돌이켜 보면 가장 잘 발견되었습니다).

인공 벤치 마크

두 번째 벤치 마크는 거의 확실하게 컴파일러가 사용자 출력에 영향을 미치는 부작용이없는 코드를 최적화 한 결과입니다. 인공 벤치 마크는 최적화 프로그램이 관련없는 부작용 (기본적으로 사용자 출력에 영향을주지 않는 부작용)을 제거하기 위해 수행하는 작업의 결과로 오해의 소지가있는 것으로 악명 높은 경향이 있습니다. 따라서 실제로 벤치마킹하려는 작업을 모두 건너 뛰는 최적화 프로그램의 결과가 아니라는 사실이 너무 좋아 보이는 시간으로 벤치 마크를 구성 할 때주의해야합니다. 최소한 계산에서 수집 된 최종 결과를 테스트에서 출력하기를 원합니다.


나는 (당신의 첫 번째 테스트에 대해 많은 것을 말할 수 없다 map()append()루프에서)하지만 난 결과를 확인할 수 있습니다. 추가하면 추가 루프가 더욱 빨라집니다.

output.reserveCapacity(array.count)

어레이 생성 후. Apple이 여기에서 일을 개선 할 수 있으며 버그 보고서를 제출할 수 있습니다.

for _ in 0..<100_000 {
    var sum: Float = 0
    for element in array {
        sum += element
    }
}

컴파일러는 (아마도) 계산 된 결과가 전혀 사용되지 않기 때문에 전체 루프를 제거합니다. 유사한 최적화가 왜 발생하지 않는지 추측 할 수 있습니다.

for _ in 0..<100_000 {
    let sum = array.reduce(0, combine: {$0 + $1})
}

그러나 reduce()클로저로 호출 하면 부작용이 있는지 여부를 결정하기가 더 어려울 것 입니다.

합계 를 계산 하고 인쇄 하기 위해 테스트 코드가 약간 변경된 경우

do {
    var total = Float(0.0)
    let start = NSDate()
    for _ in 0..<100_000 {
        total += array.reduce(0, combine: {$0 + $1})
    }
    let elapsed = NSDate().timeIntervalSinceDate(start)
    print("sum with reduce:", elapsed)
    print(total)
}

do {
    var total = Float(0.0)
    let start = NSDate()
    for _ in 0..<100_000 {
        var sum = Float(0.0)
        for element in array {
            sum += element
        }
        total += sum
    }
    let elapsed = NSDate().timeIntervalSinceDate(start)
    print("sum with loop:", elapsed)
    print(total)
}

두 변형 모두 내 테스트에서 약 10 초가 걸립니다.


문자열 배열에서 반복되는 변환의 성능을 측정하는 빠른 성능 테스트 세트를 수행 한 결과 .mapfor 루프보다 약 10 배의 성능이 훨씬 더 우수한 것으로 나타났습니다 .

아래 스크린 샷의 결과는 단일 map블록의 체인 map변환이 각각 단일 변환에서 여러 s를 능가 map하고 for 루프를 사용하면 성능이 뛰어남을 보여줍니다.

맵 대 루프 성능 시연

플레이 그라운드에서 사용한 코드 :

import Foundation
import XCTest

class MapPerfTests: XCTestCase {
	var array =
		[
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString",
			"MyString"
	]

	func testForLoopAllInOnePerf() {
		measure {
			var newArray: [String] = []
			for item in array {
				newArray.append(item.uppercased().lowercased().uppercased().lowercased())
			}
		}
	}

	func testForLoopMultipleStagesPerf() {
		measure {
			var newArray: [String] = []
			for item in array {
				let t1 = item.uppercased()
				let t2 = item.lowercased()
				let t3 = item.uppercased()
				let t4 = item.lowercased()
				newArray.append(t4)
			}
		}
	}

	func testMultipleMapPerf() {
		measure {
			let newArray = array
				.map( { $0.uppercased() } )
				.map( { $0.lowercased() } )
				.map( { $0.uppercased() } )
				.map( { $0.lowercased() } )
		}
	}

	func testSingleMapPerf() {
		measure {
			let newArray = array
				.map( { $0.uppercased().lowercased().uppercased().lowercased() } )
		}
	}
}

MapPerfTests.defaultTestSuite.run()

참조 URL : https://stackoverflow.com/questions/33750636/swift-performance-map-and-reduce-vs-for-loops

반응형