Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
Tags
- 리펙터링
- 클린 코드
- 리펙토링
- uitableview
- rxswift
- map
- collectionview
- Observable
- RxCocoa
- swift documentation
- 스위프트
- UICollectionView
- Human interface guide
- 리팩토링
- ribs
- tableView
- combine
- swiftUI
- Clean Code
- Protocol
- 애니메이션
- Xcode
- MVVM
- ios
- SWIFT
- uiscrollview
- clean architecture
- Refactoring
- HIG
- UITextView
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - swift] [오픈소스 까보기] swift-benchmark - 벤치마크 방법 본문
swift-benchmark 오픈소스
- git repo
- 특정 기능에 대해서 속도 최적화해보고 싶은 경우, 어떤 것이 더욱 최적화 되는 작업인지 확인해야하는데 이 때 벤치마크를 통해 의사결정이 가능
- benchmark 사용하기 위해서 아래 URL을 사용하여 SPM으로 추가
- 사용하는 것은 매우 간단하게, benchmark 함수의 클로저로 전달하면 완료
import Benchmark
benchmark("add string no capacity") {
var x1: String = ""
for _ in 1...1000 {
x1 += "hi"
}
}
benchmark("add string reserved capacity") {
var x2: String = ""
x2.reserveCapacity(2000)
for _ in 1...1000 {
x2 += "hi"
}
}
Benchmark.main()
(빌드 최적화 상태에서 benchmark를 알아야하므로, 해당 스킴의 Run 설정을 변경)
- Build Configuration을 Release로 변경
- Debug excutable체크해제
(실행 후 콘솔을 확인)
- 얼마나 시간이 걸리는지와, iterations 개수 파악이 가능
running add string no capacity... done! (1555.03 ms)
running add string reserved capacity... done! (1638.20 ms)
name time std iterations
---------------------------------------------------------------
add string no capacity 18333.000 ns ± 5.91 % 71320
add string reserved capacity 18166.000 ns ± 12.70 % 77317
BenchmarkSuite 개념
- Suite는 관련된 테이스 케이스들을 묶은 하나의 집합
- BenchmarkSuite라는것을 정의하여 benchmark에 하나씩 추가할 때 이 인스턴스로 저장하여, run() 시키면 벤치마크가 실행
public class BenchmarkSuite {
public let name: String
public let settings: [BenchmarkSetting]
public var benchmarks: [AnyBenchmark] = []
...
}
BenchmarkSuite가 사용되는 플로우
- benchmark 함수를 보면 클로저로 벤치마크할 코드를 주입
benchmark("add string no capacity") {
var x1: String = ""
for _ in 1...1000 {
x1 += "hi"
}
}
- 내부 코드
- 클로저를 defaultBenchmarkSuite라는 인스턴스의 benchmark 함수에 전달
// Benchmark.swift
public func benchmark(_ name: String, function: @escaping () throws -> Void) {
defaultBenchmarkSuite.benchmark(name, function: function)
}
- 타고 들어가면 이 인스턴스가 관리하고 있는 benchmarks 배열에 추가
// BenchmarkSuite.swift
public var benchmarks: [AnyBenchmark] = []
public func benchmark(_ name: String, function f: @escaping () throws -> Void) {
let benchmark = ClosureBenchmark(name, settings: [], closure: f)
register(benchmark: benchmark)
}
public func register(benchmark: AnyBenchmark) {
benchmarks.append(benchmark)
}
BenchmarkRunner 개념
- 위에서 추가하여 BenchmarkSuite로 관리되던 클로저들을 실행시켜야하는데, 이 실행시키는 역할을 BenchmarkRunner가 담당
- BenchmarkRunner는 suites 배열을 가지고 있고, 이를 단순히 run 시킬수도 있고 밖으로부터 suite를 주입받아서 run 시킬 수 있는 기능 둘 다 제공
- 결국 run(benchmark: AnyBenchmark, suite: BenchmarkSuite)을 실행
// BenchmarkRunner.swift
public struct BenchmarkRunner {
let suites: [BenchmarkSuite]
let settings: [BenchmarkSetting]
let customDefaults: [BenchmarkSetting]
var progress: ProgressReporter
var reporter: BenchmarkReporter
...
public mutating func run() throws {
for suite in suites {
try run(suite: suite)
}
reporter.report(results: results)
}
mutating func run(suite: BenchmarkSuite) throws {
for benchmark in suite.benchmarks {
try run(benchmark: benchmark, suite: suite)
}
}
mutating func run(benchmark: AnyBenchmark, suite: BenchmarkSuite) throws {
let settings = BenchmarkSettings([
defaultSettings,
self.customDefaults,
suite.settings,
benchmark.settings,
self.settings,
])
...
}
...
}
BenchmarkSettings 개념
- BenchmarkRunner에서 run(benchmark: AnyBenchmark, suite: BenchmarkSuite)에서 benchmark 핵심 로직이 있는데, 가장 첫번째로 benchmarkSettings를 생성
mutating func run(benchmark: AnyBenchmark, suite: BenchmarkSuite) throws {
let settings = BenchmarkSettings([
defaultSettings,
self.customDefaults,
suite.settings,
benchmark.settings,
self.settings,
])
...
}
- Setting은 value type이므로 settings값을 변경하는 처리보다는 config처럼 하나로 묶어서, 관련 코드들을 처리하기 쉽도록 instance만드는 것 (내부 코드를 보면 computed property위주로 존재)
// BenchmarkSetting.swift
public struct BenchmarkSettings {
...
/// Convenience accessor for WarmupIterations setting.
public var warmupIterations: Int {
if let value = self[WarmupIterations.self]?.value {
return value
} else {
return 0
}
}
public var filter: String? {
return self[Filter.self]?.value
}
public var filterNot: String? {
return self[FilterNot.self]?.value
}
public var minTime: Double {
if let value = self[MinTime.self]?.value {
return value
} else {
fatalError("minTime must have a default.")
}
}
public var timeUnit: TimeUnit.Value {
if let value = self[TimeUnit.self]?.value {
return value
} else {
fatalError("timeUnit must have a default.")
}
}
...
}
(밑에 전역변수로 defaultSettings도 존재)
public let defaultSettings: [BenchmarkSetting] = [
MaxIterations(1_000_000),
MinTime(seconds: 1.0),
TimeUnit(.ns),
InverseTimeUnit(.s),
Format(.console),
Quiet(false),
]
Benchmark 핵심 로직
- BenchmarkRunner.swift에서 run 로직이 핵심이고, settings를 만든 후 계속 확인해보면 settings 인스턴스에서 제공해주는 filter를 통해 지금 테스트 하려는 것들을 대상만 동작하도록 필터링 작업 수행
// BenchmarkRunner.swift
mutating func run(benchmark: AnyBenchmark, suite: BenchmarkSuite) throws {
let settings = BenchmarkSettings([
defaultSettings,
self.customDefaults,
suite.settings,
benchmark.settings,
self.settings,
])
let filter = try BenchmarkFilter(settings.filter, negate: false)
if !filter.matches(suiteName: suite.name, benchmarkName: benchmark.name) {
return
}
let filterNot = try BenchmarkFilter(settings.filterNot, negate: true)
if !filterNot.matches(suiteName: suite.name, benchmarkName: benchmark.name) {
return
}
...
}
- 필터링 작업이 끝나면 progress에 report 후 현재 시간을 기록
- 주의) 현재 시간을 기록하는데, 이건 벤치마크할 대상 뿐만이 아닌, 아래에서 알아볼 warmupState하는 시간도 포함될 것
mutating func run(benchmark: AnyBenchmark, suite: BenchmarkSuite) throws {
...
progress.report(running: benchmark.name, suite: suite.name)
let totalStart = now()
...
}
@inline(__always)
func now() -> UInt64 {
return DispatchTime.now().uptimeNanoseconds
}
- 이어서 warmupState라는 것을 만드는데, 원하는 타겟의 벤치마크를 수행하기 전에 준비하는 단계
- 벤치마크 warmup은 실제 벤치마크 실행 전에 몇 번의 연습 루프(iteration)를 돌려 하드웨어나 컴파일러 등이 최적화를 수행할 수 있도록 하는 과정
mutating func run(benchmark: AnyBenchmark, suite: BenchmarkSuite) throws {
...
var warmupState: BenchmarkState? = nil
if settings.warmupIterations > 0 {
warmupState = doNIterations(
settings.warmupIterations, benchmark: benchmark, suite: suite, settings: settings)
}
...
}
func doNIterations(
_ n: Int, benchmark: AnyBenchmark, suite: BenchmarkSuite, settings: BenchmarkSettings
) -> BenchmarkState {
var state = BenchmarkState(iterations: n, settings: settings)
do {
try state.loop(benchmark)
} catch is BenchmarkTermination {
} catch {
fatalError("Unexpected error: \(error).")
}
return state
}
- warmup이 끝나면 이어서 n번 iternation을 돌면서 benchmark 수행
- benchmark 수행 완료 후 progress에 report한 후 result를 저장하면 완료
mutating func run(benchmark: AnyBenchmark, suite: BenchmarkSuite) throws {
...
var state: BenchmarkState
if let n = settings.iterations {
state = doNIterations(n, benchmark: benchmark, suite: suite, settings: settings)
} else {
state = doAdaptiveIterations(
benchmark: benchmark, suite: suite, settings: settings)
}
let totalEnd = now()
let totalElapsed = totalEnd - totalStart
progress.report(
finishedRunning: benchmark.name, suite: suite.name, nanosTaken: totalElapsed)
let result = BenchmarkResult(
benchmarkName: benchmark.name,
suiteName: suite.name,
settings: settings,
measurements: state.measurements,
warmupMeasurements: warmupState != nil ? warmupState!.measurements : [],
counters: state.counters)
results.append(result)
}
- 중간에 doNIternations과 doAdaptiveIterations이 있는데, doNIternations은 N번 수행하는 것이고 doAdaptiveIterations은 구현부에서 정한 적당한 값으로 반복문을 수행
- while문을 돌면서 n값을 증가시켜주는데, predicNumberOfInterationsNeeded()메소드를 사용
// BenchmarkRunner.swift
func doAdaptiveIterations(
benchmark: AnyBenchmark, suite: BenchmarkSuite, settings: BenchmarkSettings
) -> BenchmarkState {
var n: Int = 1
var state: BenchmarkState = BenchmarkState()
while true {
state = doNIterations(n, benchmark: benchmark, suite: suite, settings: settings)
if n != 1 && hasCollectedEnoughData(state.measurements, settings: settings) { break }
n = predictNumberOfIterationsNeeded(state.measurements, settings: settings)
assert(
n > state.measurements.count,
"Number of iterations should increase with every retry.")
}
return state
}
- predicNumberOfInterationsNeeded()메소드
- 이전에 벤치마크를 통해 얼마나 걸렸는지 시간값(measurements)을 가지고 휴리스틱하게 적당한 값을 곱하여 구한 것
// BenchmarkRunner.swift
/// Heuristic for finding good next number of iterations to try, ported from google/benchmark.
func predictNumberOfIterationsNeeded(_ measurements: [Double], settings: BenchmarkSettings)
-> Int
{
let minTime = settings.minTime
let iters = measurements.count
// See how much iterations should be increased by.
// Note: Avoid division by zero with max(timeInSeconds, 1ns)
let timeInSeconds = measurements.reduce(0, +) / 1000000000.0
var multiplier: Double = minTime * 1.4 / max(timeInSeconds, 1e-9)
// If our last run was at least 10% of --min-time then we
// use the multiplier directly.
// Otherwise we use at most 10 times expansion.
// NOTE: When the last run was at least 10% of the min time the max
// expansion should be 14x.
let isSignificant = (timeInSeconds / minTime) > 0.1
multiplier = isSignificant ? multiplier : min(10.0, multiplier)
if multiplier < 1.0 {
multiplier = 2.0
}
// So what seems to be the sufficiently-large iteration count? Round up.
let maxNextIters: Int = Int(max(multiplier * Double(iters), Double(iters) + 1.0).rounded())
// But we do have *some* sanity limits though..
let nextIters = min(maxNextIters, settings.maxIterations)
return nextIters
}
- 이렇게 쌓인 결과들은 results 배열에 삽입
// BenchmarkRunner.swift
public var results: [BenchmarkResult] = []
...
mutating func run(benchmark: AnyBenchmark, suite: BenchmarkSuite) throws {
...
let result = BenchmarkResult(
benchmarkName: benchmark.name,
suiteName: suite.name,
settings: settings,
measurements: state.measurements,
warmupMeasurements: warmupState != nil ? warmupState!.measurements : [],
counters: state.counters)
results.append(result)
}
- 이 메소드, mutating func run(benchmark: AnyBenchmark, suite: BenchmarkSuite)를 호출했던 곳으로 다시 되돌아가보면, 이 결과들을 reporter에 report하는 코드가 존재
// BenchmarkRunner.swift
public mutating func run() throws {
for suite in suites {
try run(suite: suite)
}
reporter.report(results: results)
}
- reporter의 report 메소드에서 결과로 나온 result들을 보기 좋게 print()를 수행하여 완료
// BenchmarkReporter.swift
mutating func report(results: [BenchmarkResult]) {
let (rows, columns) = BenchmarkColumn.evaluate(results: results, pretty: true)
let widths: [BenchmarkColumn: Int] = Dictionary(
uniqueKeysWithValues:
columns.map { column in
(
column,
rows.compactMap {
row in row[column]?.count
}.max() ?? 0
)
}
)
print("", to: &output)
for (index, row) in rows.enumerated() {
let components: [String] = columns.compactMap { column in
var string: String
if let value = row[column] {
string = value
} else {
string = ""
}
let width = widths[column]!
let alignment = index == 0 ? .left : column.alignment
switch alignment {
case .left:
return string.rightPadding(toLength: width, withPad: " ")
case .right:
return string.leftPadding(toLength: width, withPad: " ")
}
}
let line = components.joined(separator: " ")
print(line, to: &output)
if index == 0 {
print(String(repeating: "-", count: line.count), to: &output)
}
}
}
(완료)
running add string no capacity... done! (1555.03 ms)
running add string reserved capacity... done! (1638.20 ms)
name time std iterations
---------------------------------------------------------------
add string no capacity 18333.000 ns ± 5.91 % 71320
add string reserved capacity 18166.000 ns ± 12.70 % 77317
정리
- benchmark() { 테스트영역 }: 클로저에 벤치마크를 수행할 코드를 넣으면 BenchmarkSuite클로저를 형태로 저장
- BenchmarkRunner의 메소드인 Benchmark.run()을 호출하면 시간을 기록하여 warmup하고 benchmark를 수행
- benchmark는 반복문을 돌면서 수행
- 테스트가 끝나면 results 배열에 저장하고, reporter 인스턴스에게 report()를 요청하면 result를 토대로 쉽게 알아볼 수 있도록 print를 수행
* 참고
'오픈소스 까보기' 카테고리의 다른 글
Comments