String의 Small String Optimization
안녕하세요. 그린입니다 🍏
이번 포스팅에서는 Swift String의 Small String Optimization에 대해 학습해보겠습니다 🙋🏻
우선 오늘의 학습을 거치기전에 선행되어야 할 조건이 있어요!
바로 요 포스팅입니다!
왜냐하면 오늘 배워볼 부분이 String은 Collection 타입이라 Heap에 원본 데이터가 저장되고 복사가 일어난 후 값의 수정이 발생하면 서로 다른 메모리 영역을 가진다고 배웠어요.
그런데 실제로 작은 문자열에 대해 찍어보면 다른 메모리 영역을 갖지 않고 동일하게 나타나는것이 의심이 되어 알아본 학습이기 때문입니다 🤔
그러니 꼭! 위 포스팅을 먼저 선행하고 해당 포스팅을 보시는걸 권장합니다 🙏🏻
서로 다른 String 데이터들이 같은 Heap 주소를 가르킨다?
우선 아래와 같은 코드가 있다고 가정해봅시다!
import Foundation
var firstStr: String = "green"
var secondStr: String = "red"
// MARK: - Stack 메모리 영역 주소 출력
func printStackAddress(input: inout String) {
withUnsafePointer(to: &input) { pointer in
print("The stack memory address is \(pointer)")
}
}
// MARK: - Heap 메모리 영역 주소 출력
func printHeapAddress(address input: UnsafeRawPointer) {
print(String(format: "The heap memory address is %p", Int(bitPattern: input)))
}
printStackAddress(input: &firstStr)
printStackAddress(input: &secondStr)
printHeapAddress(address: firstStr)
printHeapAddress(address: secondStr)
// The stack memory address is 0x0000000100008018
// The stack memory address is 0x0000000100008028
// The heap memory address is 0x600000c08260
// The heap memory address is 0x600000c08260
명백히 서로 다른 String 데이터입니다.
복사를 하지도 않았구요!
그런데 같은 Heap 주소를 출력하고 있어요.
도대체 왜 그럴까요?
여기서 찾아보고 알아봤던건 두가지 가정들과 결론이였습니다.
Swift에선 직접적으로 Heap 메모리 주소에 접근을 지원하지 않는다!
Swift에서는 말 그대로 직접적으로 Heap 메모리 주소에 접근을 지원하지 않는다는 사실입니다.
그럼으로 위 코드에서도 String이 변환되는 과정에서 생기는 임시 메모리 주소를 활용해 Heap 메모리 주소를 대략적으로 알 수 있었던것이죠.
대략적일뿐 확실하고 정확한 주소인지는 판단할 수 없다고 생각합니다.
즉, String의 데이터가 실제로 저장된 Heap 메모리 주소와는 차이가 있을 수 있습니다.
그런데 이것만으론 납득하기에 충분하지 않았어요!
가끔은 다른 주소를 가르키게 나올때도 있고 큰 문자열이 들어가면 확실히 다르게 나타나니까요!
그래서 오늘 주제였던 Small String Optimization이라는 개념 때문이라는것을 알 수 있었어요 😃
그럼 Small String Optimization을 알아볼까요?
String Optimization?
우선 String은 CoW (Copy-on-Write) 최적화를 통해 메모리를 효율적으로 힙에서 관리합니다.
이는 변수가 복사될 때 실제 힙 데이터가 복사되는것이 아니라 참조 카운트만 증가하고 데이터가 변경될 때 실제로 힙 메모리에 데이터가 복사되는 방식이죠!
여기까지는 이전 포스팅을 보셨다면 충분히 쉽게 알 수 있는 사실이죠.
그럼 정말인지 한번 아래 코드를 통해 대략적인 Heap 메모리 주소를 찍어볼까요?
import Foundation
var firstStr: String = "green"
var secondStr: String = firstStr
// MARK: - Stack 메모리 영역 주소 출력
func printStackAddress(input: inout String) {
withUnsafePointer(to: &input) { pointer in
print("The stack memory address is \(pointer)")
}
}
// MARK: - Heap 메모리 영역 주소 출력
func printHeapAddress(address input: UnsafeRawPointer) {
print(String(format: "The heap memory address is %p", Int(bitPattern: input)))
}
🔥🔥🔥🔥🔥🔥🔥
secondStr = "red"
🔥🔥🔥🔥🔥🔥🔥
printStackAddress(input: &firstStr)
printStackAddress(input: &secondStr)
printHeapAddress(address: firstStr)
printHeapAddress(address: secondStr)
// The stack memory address is 0x0000000100008018
// The stack memory address is 0x0000000100008028
// The heap memory address is 0x600000c04bc0
// The heap memory address is 0x600000c04bc0
분명 secondStr은 firstStr을 복사하고 다른 문자열로 데이터를 수정해줬습니다.
그럼 예상되는 결과는 서로 다른 Heap 주소를 나타내야 맞을텐데 같은 주소를 가르키죠!?
그럼 CoW가 사실이 아니였어..!?
아닙니다.
이유는 Small String Optimization때문에 그렇습니다.
Swift의 String은 작은 문자열에 대해서는 Small String Optimization이라는 최적화를 수행해요!
이 최적화라는것은 작은 문자열 (길이가 작은)에 대해 Heap 메모리를 사용하지 않도록하면서 String 구조체 내에 직접적으로 저장하도록 합니다.
이유는 작은 문자열의 생성과 복사에 대한 오버헤드를 줄일 수 있는 장점때문이죠.
즉, secondStr이 복사되고 데이터가 변경되었어도 Heap 주소는 같게 나오게 되는것이죠.
사실상 red가 String 구조체 내에 직접 저장되어 있으니까요!
그럼 실제인지 두 문자열을 복사하지 않고 다른 값을 넣어 찍어보죠!
import Foundation
var firstStr: String = "green"
var secondStr: String = "red"
// MARK: - Stack 메모리 영역 주소 출력
func printStackAddress(input: inout String) {
withUnsafePointer(to: &input) { pointer in
print("The stack memory address is \(pointer)")
}
}
// MARK: - Heap 메모리 영역 주소 출력
func printHeapAddress(address input: UnsafeRawPointer) {
print(String(format: "The heap memory address is %p", Int(bitPattern: input)))
}
printStackAddress(input: &firstStr)
printStackAddress(input: &secondStr)
printHeapAddress(address: firstStr)
printHeapAddress(address: secondStr)
// The stack memory address is 0x0000000100008018
// The stack memory address is 0x0000000100008028
// The heap memory address is 0x600000c00200
// The heap memory address is 0x600000c00200
복사가 아닌 아예 다른 문자열을 넣어줘도 길이가 짧기에 String 구조체 내에 저장되죠.
그리고 임시 힙 메모리 주소가 같은걸 확인할 수 있습니다.
그런데 여기서 어떤 문자열 변수라도 길이가 긴 큰 문자열로 변경되면 이 문자열은 String 구조체 내에 직접 저장될 수 없기에 새로운 Heap 메모리 주소를 할당받게 됩니다.
한번 코드로 볼까요?
import Foundation
var firstStr: String = "greenredbluewhiteorangebrown"
var secondStr: String = firstStr
// MARK: - Stack 메모리 영역 주소 출력
func printStackAddress(input: inout String) {
withUnsafePointer(to: &input) { pointer in
print("The stack memory address is \(pointer)")
}
}
// MARK: - Heap 메모리 영역 주소 출력
func printHeapAddress(address input: UnsafeRawPointer) {
print(String(format: "The heap memory address is %p", Int(bitPattern: input)))
}
🔥🔥🔥🔥🔥🔥🔥
secondStr = "red"
🔥🔥🔥🔥🔥🔥🔥
printStackAddress(input: &firstStr)
printStackAddress(input: &secondStr)
printHeapAddress(address: firstStr)
printHeapAddress(address: secondStr)
// The stack memory address is 0x0000000100008018
// The stack memory address is 0x0000000100008028
// The heap memory address is 0x6000017080e0
// The heap memory address is 0x600000c08140
자 보시면 secondStr은 긴 문자열인 firsStr을 복사하고 값을 수정했습니다.
결과를 보면 서로 다른 힙 주소를 나타내고 있음을 확인할 수 있죠!?
즉, 작은 문자열이 되며 String 구조체 내에 저장되게 되는것이죠.
마무리
결과적으로 정리해보면 이렇습니다!
Swift에서는 직접적으로 Heap 메모리 주소에 접근하는 것을 지원하지 않습니다.
그렇기에 위 예시 코드에서 보여준 힙 메모리 주소를 알아보는 함수들은 명확하게 정확한 주소라기 보다는 String 인스턴스의 임시 메모리 주소를 출력하는것입니다.
그렇기에 100% 신뢰할 수 없을 수 있죠.
그 다음으로 작은 문자열에 대해서는 Small String Optimization을 통해 String 구조체 내에 저장됩니다.
레퍼런스
해당 Swift에서 Small String Optimization을 활용하는것은 아래 Swift 오픈 소스 이슈의 정보로부터 학습이 시작되었습니다 ☺️