trivial을 알아보기 이전에 한 번 생각해 보자.
💬 trivial을 왜 알아야 할까?🤔
내가 구현한 객체가 trivial 하지 않으면 복사 생성자나 복사 대입 연산자가 호출되어 복사가 된다. 너무 당연한 얘기이다.
그런데 trivial 하면 컴파일러가 복사 생성자나 복사 대입 연산자를 호출하지 않고 메모리 블록을 통째로 복사를 한다.(bitwise copy) 다시 말해 코드가 최적화되어 복사 속도가 빨라지기도 하고 이를 고려하여 구현할 수도 있다.
💬 trivial의 첫인상은 마치 내가 산 주식이 100원 올라 자랑하고 있는데, 친구가 훨씬 더 많이 올랐다는 소릴듣는 느낌이랄까? 다시 말해 이상하게 성능을 손해 본 느낌이 들게 하였었다.
하찮은 녀석이, 하찮지 않기 때문에 알아볼 필요가 있다.🧐
trivial 뜻 : 하찮은
trivial 한 객체일 경우, 복사
bitwise copy가 되는 예시
vector는 사용하는 타입이 trivial 한 객체일 경우, 복사 생성 시 bitwise copy를 하니 내부 동작을 한번 확인해 보자.
메모리 동작 원리를 설명하면서 vector를 예로 들었었는데
vector 내부 구현 동작을 잘 모른다면 아래 글에서 이해를 우선하고 다음으로 넘어가자.
vector 사용 시
Example
약식으로 vector의 복사 생성자만 표현하면 아래와 같이 된다.
vector(const vector& other): size(other.size), capacity(other.capacity)
{
//메모리만 할당
buff = static_cast<T*>(operator new(sizeof(T) * size));
//갯수만큼 복사 생성자 호출
for (int i = 0; i < size; i++)
{
new(&buff[i]) T(other.buff[i]);
}
}
입력 T가 trivial 하면 아래와 같이 변경할 수 있다.
Example
vector(const vector& other): size(other.size), capacity(other.capacity)
{
//메모리만 할당
buff = static_cast<T*>(operator new(sizeof(T) * size));
if (std::is_trivially_copy_constructible_v<T>)
{
memcpy(buff, other.buff, sizeof(T) * size); // 1.
}
else
{
for (int i = 0; i < size; i++)
{
//갯수만큼 복사 생성자 호출
new(&buff[i]) T(other.buff[i]);
}
}
}
💡 참고
c++11부터 trivial 유무를 체크할 수 있는 traits 기술이 추가 되었는데 아래와 같이 사용할 수 있다.
#include <type_traits> //헤더에 정의 됨
// trivial 여부를 조사하는 방법
bool b = std::is_trivially_default_constructible_v<T>;
주석 1
T 객체가 trivial 하다면 개수만큼 복사 생성자를 호출하는 것이 아닌 객체 메모리를 그냥 memcpy 한다.
💬 오~~~ 😤 생각만 해도 빨라지는 느낌이다. 👍
✔ 실제 std::vector는 다음과 같이 처리하고 있다.
_Bitcopy_constructible 로 tirivial 유무를 체크하여 bitcopy 조건을 체크한다.
trivial 한 객체란 뭘까❓
생성자/소멸자에서 아무것도 하지 않는 객체라면 trivial 한 객체이다.
예제를 통해 trivial 한 객체를 정리해 보자.
Example
아래 Derived class를 통해 trivial 한 조건을 정리해 보자.
class base {
}
class Derived : base { // 1.
int x;
int y;
Base obj; // 2.
public:
//Base(){} //3.
//~Base(){} //4.
//virtual void foo() {} // 5.
}
주석된 내용을 하나씩 확인해 보면
주석 1. 상속받았다면 기반 클래스(base)의 생성자가 trivial 해야 한다.
주석 2. 객체형 멤버(Base obj)가 있다면 멤버의 객체가 trivial 해야 한다.
주석 3.4. 직접 만든 생성자/소멸자가 없어야 한다.
L 생성자나 소멸자가 호출이 필요한 객체라면 bitwise copy시 문제가 되는 객체이기에 이러한 객체는 trivial 할 수 없다.
주석 5. 가상함수가 없어야 한다.
L bitwise copy를 하면 가상 함수 테이블을 덮어버리기에 객체의 의미가 망가진다. 그렇기 때문에 가상함수가 있는 객체는 trivial 할 수 없다.
📜 동일한 이유로 trivial 하지 않은 객체는 bitwise copy를 하지 말라고 C++핵심 가이드라인에서 권고하고 있으니 참고해 보자.
SL.con.4: trivially-copyable하지 않은 객체들을 memset이나 memcpy의 인자로 사용하지 말라.
추가로 알아두면 좋을 non-trivial 케이스
흔하지 않겠지만 아래의 코드는 trivial 하지 않다 왜 그런지 한번 생각해 보자.
Example
#include<iostream>
#include<type_traits>
class Test
{
int data = 0;
public:
Test() = default;
};
int main()
{
bool b = std::is_trivially_default_constructible_v<Test>;
std::cout << b << std::endl;
}
result
0
얼핏 보면 생성자가 default 생성자이고 소멸자도 컴파일러가 생성되니 Test 객체는 trivial 한 것 같지만 왜 trivial 하지 않은 결과가 나왔을까? 🤔
그이유는 컴파일러는 코드를 아래와 같이 변경한다.
int data;// = 0;
public:
Test() : data(0){}
int data = 0;
바로 인라인 초기화 코드 때문에 생성자가 변경되어 trivial 하지 않은 객체가 된 것이다.
💬 설계시 성능과 최적화에 대해 고민하고 있다면 trivial 하게 제공하는 방안도 고려해보면 좋을 것 같다.
참고
C++ 핵심 가이드라인 한글화 프로젝트 링크
C++ Core Guidelines 원문 링크
'L C++' 카테고리의 다른 글
남들 쓰듯이 쓰는 virtual이라면 고민할 필요 없는 알쓸 virtual 정보 📖 (0) | 2024.10.06 |
---|---|
직접선언! 낄낄빠빠하자. (0) | 2024.09.09 |
컴파일러와 페어 프로그래밍 - 특별 멤버 함수 편 🚀 (0) | 2024.08.15 |
'0' 대신 'nullptr'를 써야 하는 이유 (0) | 2024.08.04 |
new/delete 동작 원리 이해하기 (4) | 2024.07.22 |