String
String은 일련의 Character들로 이루어진 데이터 타입으로, Copy-on-Write(CoW) 기법을 통해 메모리 성능 최적화합니다.
추가적으로 String을 이루는 Character들은 Extended Grapheme Cluster(확장된 문자소 클러스터)로 이루어져 있으며, 또 이 Extended Grapheme Cluster는 Unicode Scalar Value(유니코드 스칼라 값)들로 이루어져 있습니다.
Copy-on-Write(CoW)
Copy-on-Write 기법은 말 그대로 쓰기 작업이 일어날 때 복사가 된다는 의미입니다.
String에서는 CoW 전략을 사용할 때 버퍼(Buffer)에 데이터를 임시로 저장합니다. 만약 이 버퍼를 여러 다른 문자열 인스턴스가 참조하고 있을 경우 문자열 데이터가 변형(Mutation)된다면 이 버퍼가 복사되어 새로운 버퍼를 할당해줌으로써 메모리 효율성을 높입니다.
예시를 한번 들어보겠습니다.
아래와 같이 "안녕하세요"라는 문자열을 가진 str1 변수를 생성해줍니다. 그러면 Heap 영역에 "안녕하세요" 문자열 데이터를 가진 버퍼가 할당되고 Stack 영역에 있는 str1 변수가 이 Heap 영역에 있는 문자열 데이터의 주소를 가지게 됩니다.
var str1 = "안녕하세요" // 버퍼 생성
그 다음 str2이라는 새로운 변수를 생성하여 str1의 값을 str2에 할당해주면 아래 그림에서 보실 수 있듯이 str2도 str1과 같은 버퍼를 가리키게 됩니다.
var str2 = str1 // str2에 str1가 가리키는 참조를 할당
마지막으로 str2 변수에 "새로운 문자열"을 할당해주면 아래 그림에서 보실 수 있듯이 Heap영역에 있던 기존 버퍼 0xEEEE가 복사되어 새로운 독립적인 0xDDDD가 할당되고, 변수 str2가 이 복사된 메모리 0xDDDD를 가리키게 됩니다.
/**
str1과 같은 버퍼를 가리키던 str2에 새로운 문자열 데이터가 수정되면
새로운 독립적인 버퍼가 메모리에 할당되어 str2가 이 버퍼를 가르킨다.
*/
str2 = "새로운 문자열" // str2에 새로운 문자열 데이터 할당
CoW 전략 요약
동일한 문자열 데이터가 여러 변수에 할당되고 있는 경우, 이중 첫번째 문자열 데이터 변경이 발생할 때만 새로운 버퍼를 할당하여 데이터를 복사하여 메모리 성능을 최적화시켜주는 전략입니다.
CoW 성능
Cow 전략이 진행되는 동안(메모리 복사가 진행되는 동안) 문자열 데이터 길이 n만큼 시간이 걸리기 때문에 시간복잡도는 O(n)이고 메모리 공간도 문자열 데이터 크기만큼 차지되기 때문에 공간복잡도도 O(n)입니다.
하지만 시간복잡도 O(n)은 맨 처음 복사시에만 걸리는 시간입니다. 이후에는 지수적 성장 전략(Exponential Growth Strategy)을 사용해 버퍼의 확장 빈도를 줄여, 문자열을 여러 번 추가할 때도 평균적으로 상수 시간 O(1)에 문자열을 추가할 수 있습니다. 이를 통해 String의 성능 최적화와 효율적인 메모리 관리를 달성하게 됩니다.
Exponential Growth Strategy
지수적 성장 전략에 대해 더 자세히 알아보자면 새로 할당되는 버퍼의 크기가 기존 버퍼의 크기보다 지수적으로 증가하는 전략을 의미하며, 현재 버퍼가 가득 차게되면 기존 크기의 두 배 혹은 그 이상의 공간을 새로 할당(Doubling)하게 됩니다. 이 전략을 통해 문자열 데이터를 추가할 때마다 매번 버퍼를 할당하지 않고, 더 큰 공간을 미리 확보해 놓습니다.
count 속성 vs isEmty 속성, 어떤 속성이 더 효율적일까?
count에 대한 애플 공식 문서에 따르면 컬렉션이 비어있는지 확인할 때, count 속성은 해당 컬렉션이 RandomAccessCollection 프로토콜을 준수하지 않는 한 O(n)의 시간복잡도가 걸리므로 isEmpty 속성을 사용하는 것이 더 효율적이라고 합니다.
예를 들어, 아래와 같이 빈 문자열을 가진 string 변수가 정의되어 있습니다.
var string: String = ""
아래와 같이 count 속성을 이용해 string 변수가 빈 문자열을 가지고 있는지 확인하면 문자열을 처음부터 끝까지 탐색해야하기 때문에 문자열의 길이 n만큼 시간이 걸립니다.(O(n))
if string.count == 0 {
// 어떤 작업 처리
}
하지만 아래처럼 isEmpty 속성을 사용하면 바로 반환 처리되기 때문에 O(1)만큼의 시간이 걸려 빈 문자열 여부를 확인할 때는 isEmpty 속성이 대부분의 경우 성능상 이점을 가져갈 수 있다고 합니다.
if string.isEmpty {
// 어떤 작업 처리
}
Int
Swift에서 Int 타입은 부호가 있는 정수값(Signed Integer)으로 구조체(Structure)로 정의되어 있으며, 32bit CPU에서는 Int32로, 64bit CPU에서는 Int64와 동일한 크기가 같습니다.
여기서 일반적으로 CPU는 요즘 64bit를 기본으로 사용되기 때문에 일반적인 Int의 크기는 Int64와 동일한 크기인 64bit(=8byte)와 동일합니다.
Swift에서는 고정된 크기의 Int 타입인 Int8, Int16, Int32, Int64 타입들을 지원하고 있으며, 각각의 범위는 다음과 같습니다.
- Int8: -128 ~ 127
- Int16: -32768 ~ 32767
- Int32: -2147483648 ~ 2147483647
- Int64: -9223372036854775808 ~ 9223372036854775807
Note
Int8, Int16, Int32, Int64 타입 중 범위를 벗어나는 값은 저장될 수 없습니다.
예를들어, 아래 코드와 같이 Int8이 지정할 수 있는 -128~127보다 더 큰 범위를 넘어가는 200이라는 값을 할당하려고 하면 컴파일 에러가 발생하게 됩니다.
var smallNumber: Int8 = 200 // 에러 발생!
그외 Int 타입과 비슷하게 부호가 없는 UInt라는 데이터 타입도 있습니다.
엑셀 셀의 개수가 최대 65,536개이었던 이유
과거 엑셀 셀의 개수가 최대 65,586개까지였던 이유는 과거의 시스템 아키텍처 제약때문이었습니다.
Excel 2003년 이전 버전에서는 16비트 아키텍처를 기준으로 설계되었기 때문에 행의 개수를 2의 16제곱, 즉 65,536개로 제한되었습니다. 이는 16비트 시스템에서 주소 공간을 효율적으로 활용하기 위해 행 수를 이 숫자로 제한한 것이었습니다.
Double vs Float
Double과 Float 타입 실수를 표현하기 위한 데이터 타입입니다. 이때 Double 타입은 8byte까지 저장할 수 있고, Float 타입은 4byte까지 저장할 수 있습니다. 그 이유는 Double 타입은 64비트 부동소수점 방식을 따르고, Float 타입은 32bit 부동소수점 방식을 따르기 때문입니다. 여기서 부동소수점이란 실수를 표현하기 위한 방법을 의미하는데, 이 부동소수점은 크게 부호 비트, 지수 비트, 가수 비트로 이루어져 있습니다. Double 타입은 64비트 부동소수점 방식을 따르는 만큼 부호 비트, 지수 비트, 가수 비트가 각각 1비트, 11비트, 52비트로 할당되어 있으며, 가수 비트가 52비트로 할당되어 있으므로 소수점 15~16자리까지 표현될 수 있습니다. 반면 Float 타입은 32비트 부소수점 방식을 따르고 있으므로 부호 비트, 지수 비트, 가수 비트가 각각 1비트, 8비트, 23비트로 할당되어 있습니다. 여기서 Float 타입은 가수 비트가 23비트로 할당되어 있으므로 최대 소수점 6~7자리까지 표현할 수 있습니다. 그리고 이러한 Double 타입과 Float 타입의 소수점 자리 표현 정밀도 차이로 인해 일반적으론 Double 타입이 Float 타입보다 더 많이 사용됩니다.
Collection
Array / Dictionary / Set와 Tuple의 차이
Array / Dictionary / Set은 Mutable 하지만 Set은 Immutable입니다.
Array / Dictionary / Set
Array / Dictionary / Set도 String과 마찬가지로 Copy-on-Write (CoW) 메커니즘를 따릅니다.
Dictionary
해시는 해시함수를 이용해 데이터를 고정된 크기의 값으로 매핑하는 기술로 주로 데이터를 빠르게 저장하고 검색하기 위해 사용됩니다. 그리고 해시를 사용하다보면 해시 함수가 무한한 가짓수의 입력값을 받아 유한한 가짓수의 출력값을 생성하기 때문에, 필연적으로 충돌이 발생 수 있습니다.(비둘기 집의 원리) 따라서 이 충돌을 피하기 위해 Chaining 방식과 Open Addressing 방식을 사용합니다. Chaining이란 해시 테이블의 각 버킷에 연결 리스트나 동적 배열을 사용하여 충돌을 해결하는 방식을 의미합니다. 이 방식으로 잘 구현된 경우 탐색은 O(1)이지만 최악의 경우, 즉 모든 해시 충돌이 발생했다고 가정할 경우에는 O(n)이 됩니다. 그리고 해시 충돌 회피의 또다른 방법인 Open Addressing은 충돌 발생 시 탐사를 통해 빈 공간을 찾아나서는 방식을 의미합니다. 사실상 무한정 저장할 수 있는 Chaining 방식과 달리, Open Addressing은 전체 슬롯의 개수 이상은 저장할 수 없다는 단점이 있습니다.
Bool
참고사이트
https://developer.apple.com/documentation/swift/string#Accessing-a-Strings-Unicode-Representation (Apple 공식 문서 - String)
https://developer.apple.com/documentation/swift/collection/count-4l4qk (Apple 공식 문서 - count 속성)
https://developer.apple.com/documentation/swift/randomaccesscollection (Apple 공식 문서 - RandomAccessCollection)
https://developer.apple.com/documentation/swift/bidirectionalcollection (Apple 공식 문서 - BidirectionalCollection)
'Swift 문서 탐방' 카테고리의 다른 글
[Swift 문서 탐방] Structures and Classes (0) | 2024.11.15 |
---|---|
[Swift 문서 탐방] Enumerations (1) | 2024.11.14 |
[Swift 문서 탐방] Swift 기본 타입(2) - String과 Character (0) | 2024.11.09 |
[Swift 문서 탐방] Swift 기본 타입(1) (0) | 2024.11.05 |
[Swift 문서 탐방] ARC(Automatic Reference Counting) 정리 (8) | 2024.02.22 |