개발/Swift
Swift에서 배열 초기화: 반복 append vs Array(repeating:count:)
까칠코더
2025. 11. 11. 13:36
반응형
Swift에서 배열 초기화: 반복 append vs Array(repeating:count:)
대량의 요소로 배열을 만들 때 흔히 두 가지를 고민합니다.
- 반복 append: var a:[T]=[]; a.append(x) 를 루프에서 N번 호출
- Array(repeating:count:): 동일한 값/인스턴스로 N개를 한 번에 생성
두 방식은 성능, 메모리 할당, 값/참조에서 중요한 차이가 있습니다. 실무 기준으로 선택 가이드와 예제를 정리했습니다.
1. 한눈에 요약
| 상황 | 권장 방법 | 이유 |
| 같은 스칼라 값 N개 (Int/Double/Bool 등) | Array(repeating: v, count: n) | 단일 할당 + 내부 최적화(빠름) |
| 같은 인스턴스를 N번 참조해도 됨 (클래스) | Array(repeating: obj, count: n) | 같은 객체 참조를 복제(메모리 절감) |
| 서로 다른 인스턴스가 N개 필요 (클래스) | (0..<n).map { _ in C() } | repeating은 같은 참조를 복제하므로 부적합 |
| 값 타입(Struct) 대량 초기화 + 전부 같은 값 | Array(repeating: v, count: n) | 카피‑온‑라이트(COW) + 단일 할당 유리 |
| 값/인스턴스를 계산해서 넣어야 함 | map 생성 또는 reserveCapacity + append | 각 원소 별로 계산/검증 필요 |
| 극한 성능, 크기만 알고 내용 나중에 채움 | Array(unsafeUninitializedCapacity:) | 초기화/복사 최소화(신중히) |
핵심: 동일 값으로 채울 때는 repeating이 가장 빠릅니다. 반대로 각 요소가 달라야 하면 map 또는 reserveCapacity 후 append가 정석입니다.
2. Array(repeating:count:) 기본
// 스칼라 값
let zeros = Array(repeating: 0, count: 5) // [0, 0, 0, 0, 0]
// 값 타입(Struct)도 OK
struct Pixel { var r, g, b: UInt8 }
let blackRow = Array(repeating: Pixel(r:0,g:0,b:0), count: 1920)
// 참조 타입(클래스) 주의!
final class Box { var v: Int = 0 }
let boxes = Array(repeating: Box(), count: 3)
// 같은 인스턴스 참조 3개 → 한쪽 변경 시 모두 보임
boxes[0].v = 42
print(boxes.map(\.v)) // [42, 42, 42]
- 값 타입(struct/enum)일 때는 “같은 값”이 복사되어 저장됩니다.
- 참조 타입(class)일 때는 같은 인스턴스의 참조가 N번 복제됩니다 → 서로 다른 인스턴스가 필요하면 아래 §3 참조.
3. 서로 다른 인스턴스로 N개 만들기
final class Node { let id: Int; init(_ id: Int) { self.id = id } }
let nodes = (0..<5).map { Node($0) } // 각각 다른 인스턴스
print(nodes.map(\.id)) // [0,1,2,3,4]
Array(repeating: Node(), count: n) 은 하나의 Node 인스턴스를 n번 복제 참조하므로 부적합입니다.
4. 반복 append 의 올바른 사용법 (용량 예약 필수)
let n = 10_000
var arr: [Int] = []
arr.reserveCapacity(n) // ✅ 용량 사전 확보로 재할당/복사 최소화
for i in 0..<n {
arr.append(i * 2) // 요소별 계산/검증이 있을 때 유용
}
- reserveCapacity(_:) 없이 매번 append 하면 버퍼 확장이 여러 번 일어나 재할당 + 복사 비용이 누적될 수 있습니다.
- 미리 크기를 아는 경우 반드시 예약하세요.
5. unsafeUninitializedCapacity — 극한 최적화
배열 크기를 알고 있고, 직접 버퍼를 채우고 싶다면 초기화 오버헤드를 줄일 수 있습니다.
let n = 10_000
let a = Array<Int>(unsafeUninitializedCapacity: n) { buffer, initializedCount in
for i in 0..<n { buffer[i] = i * 2 }
initializedCount = n
}
- 초기값 평가/복사 없이 곧장 메모리를 채웁니다.
- 잘못 쓰면 메모리 안전성을 해칠 수 있으니 로우레벨 성능이 꼭 필요한 경우에만 사용하세요.
6. 성능 관점 요약
- repeating 은 단일 할당 + 반복 초기화 최적화 덕분에 가장 빠릅니다(특히 스칼라/단순 struct).
- append 루프는 유연하지만, 미리 용량 예약하지 않으면 느려집니다.
- 각 요소가 계산/검증을 거쳐야 하면 map 또는 reserveCapacity + append가 자연스럽습니다.
- “내용을 나중에 채울” 대량 생성은 unsafeUninitializedCapacity 로 초기화 비용을 건너뛸 수 있습니다.
7. 예제 모음
7.1 같은 값으로 초기화(스칼라/Struct)
let ones = Array(repeating: 1, count: 8)
struct Point { var x, y: Int }
let grid = Array(repeating: Point(x: 0, y: 0), count: 4)
7.2 참조 타입 주의 + 올바른 생성
final class Cell { var alive = false }
// 🚫 모든 요소가 같은 인스턴스를 가리킴
let wrong = Array(repeating: Cell(), count: 3)
wrong[0].alive = true
print(wrong.map(\.alive)) // [true, true, true]
// ✅ 서로 다른 인스턴스 필요할 때
let ok = (0..<3).map { _ in Cell() }
ok[0].alive = true
print(ok.map(\.alive)) // [true, false, false]
7.3 계산값으로 채우기 (map vs append)
let squares1 = (0..<10).map { $0 * $0 } // 간결, 내부에서 용량 계산
var squares2: [Int] = []; squares2.reserveCapacity(10)
for i in 0..<10 { squares2.append(i*i) } // 유연(조건/분기/검사 포함 가능)
7.4 큰 배열을 빠르게 채우기 (unsafe)
let n = 1_000
let fast = Array<Double>(unsafeUninitializedCapacity: n) { buf, count in
for i in 0..<n { buf[i] = Double(i) * 0.5 }
count = n
}
7.5 2차원 배열 초기화(독립 행 보장)
// 🚫 모든 행이 같은 내부 배열을 공유하게 만들면 안 됨
let wrongGrid = Array(repeating: Array(repeating: 0, count: 3), count: 3)
// 위 케이스는 값 타입(Array)이므로 각 행이 복사되어 안전하지만,
// 참조 타입 2차원일 때는 주의 필요
// ✅ 일반적으로는 다음이 명확
let rows = 3, cols = 3
let grid2D = (0..<rows).map { _ in Array(repeating: 0, count: cols) }
참고: Swift의 Array는 값 타입이라 Array(repeating: innerArray, count: r) 도 각 행이 복사되어 독립적입니다. 하지만 클래스 인스턴스가 들어있는 2차원 구조라면 내부 원소 참조가 공유될 수 있으므로 주의하세요.
8. 벤치마크 스케치(개념 확인용)
import Foundation
let n = 2_000_00
func measure(_ name: String, _ body: () -> Void) {
let t1 = CFAbsoluteTimeGetCurrent(); body(); let t2 = CFAbsoluteTimeGetCurrent()
print(name, ":", t2 - t1, "sec")
}
measure("repeating") {
_ = Array(repeating: 0, count: n)
}
measure("append no reserve (anti)") {
var a: [Int] = []
for i in 0..<n { a.append(i) }
}
measure("append + reserve") {
var a: [Int] = []; a.reserveCapacity(n)
for i in 0..<n { a.append(i) }
}
measure("unsafeUninitialized") {
_ = Array<Int>(unsafeUninitializedCapacity: n) { buf, c in
for i in 0..<n { buf[i] = i }
c = n
}
}
실제 수치는 Release(-O) 빌드 + 실기기에서 측정하세요. 디버그 빌드는 최적화가 꺼져 있어 의미가 적습니다.
9. 선택 가이드
- 같은 값/패딩으로 채우나? → repeating
- 각 요소가 계산/검증을 거치나? → map 또는 reserve + append
- 서로 다른 인스턴스가 필요한 클래스 배열인가? → map { Class() }
- 크기를 알고 내용은 나중에 채우나? → unsafeUninitializedCapacity
- 반복 append가 느리다? → reserveCapacity 확인
10. 참조 링크
- Array.init(repeating:count:)
https://developer.apple.com/documentation/swift/array/1539124-init - Array.reserveCapacity(_:)
https://developer.apple.com/documentation/swift/array/1539128-reservecapacity - Array.init(unsafeUninitializedCapacity:_:)
https://developer.apple.com/documentation/swift/array/3126954-init
11. 결론
- 동일 값 채우기는 Array(repeating:count:)가 가장 간단하고 빠릅니다.
- 서로 다른 값/인스턴스가 필요하면 map 또는 reserveCapacity + append를 사용하세요.
- 성능이 매우 중요한 경로에서는 unsafeUninitializedCapacity를 고려하되, 안전성과 가독성을 우선하십시오.
반응형