개발/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. 참조 링크

 

11. 결론

  • 동일 값 채우기 Array(repeating:count:)가 가장 간단하고 빠릅니다.
  • 서로 다른 값/인스턴스가 필요하면 map 또는 reserveCapacity + append를 사용하세요.
  • 성능이 매우 중요한 경로에서는 unsafeUninitializedCapacity를 고려하되, 안전성과 가독성을 우선하십시오.
반응형