Swift String(Array) 성능 최적화

Array의 메모리 할당

배열은 메모리에서 특정한 영역에 연속적으로 저장됩니다. [1,2,3...]이라는 배열이 있다면 배열의 시작주소부터 연속적으로 요소들이 word단위로 저장됩니다.

       
       
  1 2 3
4 5 6  
       
       

 

이때 배열에 어떤 요소를 추가하면, 다음 주소에 다른 데이터가 저장 되 있을 수 있으므로, 메모리에서 배열을 저장 할 수 있는 공간을 찾아 할당하게 됩니다.

 

1 2 3 4
5 6 7  
       
      Cat
       
       

 

만약 어떤 요소를 추가할때마다, 이같이 메모리의 특정영역에서 저장할 공간을 찾는다면 성능적으로 문제가 있을 수 있습니다. 그래서 Swift에서 배열은 배열보다 더 큰 양의 메모리 공간을 예약합니다. 이때 예약된 공간도 다 사용하게 된다면 현재 배열의 배수의 크기만큼 공간을 찾아 예약을 하게 됩니다. 다만 무조건 배수의 크기만큼 예약하는게 아닌 배열의 크기에 따라 다른듯 합니다.

 

String의 성능

Swift String은 Character의 배열입니다. 따라서 +, +=, append, joined 같은 연산을 할 수 있고 그에따른 메모리 할당 매커니즘 역시 같습니다.

하지만 String은 struct이므로 어떤 연산을 하는지에 따라 성능의 차이가 나타납니다.

+ 연산

 public static func + (lhs: String, rhs: String) -> String {
    var result = lhs
    result.append(rhs)
    return result
  }

String에서 +연산을 할 시 COW로 동작을 하므로, 새 객체를 만들어지고 메모리에 할당되게 됩니다. 따라서 +를 반복적으로 많이 하게 될 경우 성능 하락(메모리 할당 및 해제에 대한 비용)이 발생할 수 있습니다.

+= 연산

public static func += (lhs: inout String, rhs: String) {
    lhs.append(rhs)
  }

+= 연산은 새로운 인스턴스가 아닌 참조로 문자열을 추가하기에 +연산처럼 메모리에서 공간을 찾지 않아도 됩니다.

joined 연산

  internal func _joined(separator: String) -> String {
    let underestimatedCap =
      (1 &+ separator._guts.count) &* self.underestimatedCount
    var result = ""
    result.reserveCapacity(underestimatedCap)
    if separator.isEmpty {
      for x in self {
        result.append(x._ephemeralString)
      }
      return result
    }

    var iter = makeIterator()
    if let first = iter.next() {
      result.append(first._ephemeralString)
      while let next = iter.next() {
        result.append(separator)
        result.append(next._ephemeralString)
      }
    }
    return result
  }
}

reserveCapacity()함수로 문자열의 크기만큼 메모리상에서 공간을 탐색한 후 문자열을 생성 및 할당합니다. 즉 한번만 메모리에서 공간을 찾는 작업을 하면 됩니다.

 

따라서 다음과 같은 코드가 존재한다면 

var value = "123"
let arr = ["4","5","6","7","8"]

for i in arr {
    value = value + i
    value.append(i)
    value += i
}

 

요로케 joined을 사용하는 방식이 성능적으로 최적화 된 코드라 할 수 있겠네요.

var value = "123"
let arr = ["4","5","6","7","8"]
value.append(arr.joined())

배열의 크기를 알기에 메모리에 공간을 할당을 하는 작업을 딱 한번만 하기 때문입니다. 반복문 코드에서는 반복적으로 배열에 값을 추가하기때문에 예약된 공간이 전부 다 차면 다시 메모리를 할당해야 되기 때문이죠.