배경
- 스터디 도중 구조체가 클래스에 비해 가지는 가장 두드러지는 강점은 무엇일까? 라는 이야기가 나왔다.
- 즉, 클래스와 구조체 중 구조체를 선택한다면, 그 주요 이유는 무엇일까?
- WWDC16 - Understanding Swift Performance 비디오를 모두 시청하고 모이는 자리였기 때문에 깊은 이야기들이 오고갔다.
구조체와 클래스의 차이점
- 값 의미론 / 참조 의미론
- 스택 할당 / 힙 할당
- 레퍼런스 카운트 없음 / 레퍼런스 카운트 있음
- 정적 디스패치 / 동적 디스패치
- 스터디에서 정리한 주요 차이점은 위와 같았다.
구조체는 언제나 정적 디스패치로 동작할까?
- 우리는 구조체가 정적 디스패치로 동작하기 때문에, 컴파일 단계에서 inlining 적용을 통해 함수 호출 스택을 쌓지 않는 최적화가 가능해 성능이 좋다고 알고 있었다.
- 그러나 동시에 프로토콜 타입으로 선언된 프로퍼티는 동적 디스패치로 동작한다는 새로운 사실을 알게 되었다.
- 그러면 프로토콜의 조합을 통해 다형성을 관리하는 POP 아래에서, 클래스 대신 구조체를 사용한다고 해도 메소드 디스패치에서 성능적인 이점을 가져오기는 힘들지 않을까? 라는 의견들이 있었다.
구조체는 언제나 레퍼런스 카운트가 없을까?
- 또한 우리는 구조체는 힙에 할당되지 않기 때문에 레퍼런스 카운트가 없어, 레퍼런스 카운트 연산 및 연산에 필요한 동기화 메커니즘을 필요치 않기 때문에 성능이 좋다고 알고 있었다.
- 그러나 이번 영상에서 구조체 내부에 참조 타입을 가질 경우엔, 구조체를 복사할 경우 해당 참조 타입들의 레퍼런스 카운트가 증가한다는 사실을 알게 되었다.
- 따라서 클래스 내부에 참조 타입을 가질 때보다 더 많은 레퍼런스 카운트 연산 오버헤드를 가질 수도 있고, 또한 구조체 복사 시 참조 타입의 인스턴스를 가리키는 포인터가 복사되기 때문에 구조체가 가지는 값 의미론이 깨진다.
구조체는 언제나 스택에 할당될까?
- 그리고 우리는 구조체는 스택에 할당되기 때문에 메모리 할당 연산이 빨라 성능적 이점이 있음을 알고 있다.
- 그러나 이번 영상에서 프로토콜 타입으로 선언된 프로퍼티는 existential container를 사용, 해당 컨테이너가 담을 수 있는 3word의 크기를 넘어서면 구조체임에도 불구하고 힙 공간에 할당된다는 사실을 알게 되었다.
- 따라서 프로토콜 타입 프로퍼티에 구조체 인스턴스를 넣어 사용할 경우엔 대부분의 경우 3word 크기를 넘길 것이기 때문에 힙에 할당되어 존재할 것이다.
구조체의 선택 기준
- 위에서 다룬 여러 예외들로 인해... 스터디에서 구조체와 클래스의 근본적인, 가장 큰 차이는 값 의미론 / 참조 의미론이고, 해당 의미론들이 필요한 경우 각각 구조체 / 클래스를 사용하는 것이 적절해 보인다는 결론을 내렸다.
- 물론 구조체 내부에서 참조 타입을 사용할 경우 값 의미론이 깨지긴 하지만, 이는 참조 타입이 가지는 인스턴스 주소 자체가 복사되기 때문이지, 구조체의 동작이 변경되는 것이 아니다.
- 따라서 디스패치, 레퍼런스 카운트, 메모리 할당 등 여러 요인에 따라 변경될 수 있는 성능적인 부분과 달리 구조체가 가지는 값 의미론은 불변한다고 볼 수 있다고 생각한다.
결론
- 성능적인 측면도 구조체를 선택하는 주요한 이유들 중 하나지만, 이는 프로토콜 타입의 사용 및 참조 타입을 구조체 안에 사용하는 등의 행위로 성능적 이점이 사라질 수 있다.
- 구조체가 가지는 값 의미론만이 유일하게 불변하기 때문에, 구조체와 클래스의 근본적인 차이는 그 동작 방식인 값 의미론과 참조 의미론이 아닐까?