You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
2주전에 있었던 멘토님의 ThorVG C++강의에 나온 개념들을 공부 겸 정리해보았습니다.
아는 내용도 일부 있지만 그래도 다시 본다는 느낌으로 AI의 도움을 받아 작성해 보았습니다.
틀린 부분이나 보충할 부분이 있으면 지적해주시면 감사하겠습니다.
전부 작성한게 아니기 때문에 나중에 기회가 된다면 추가적으로 더 글을 작성할 예정이니다.
매크로
컴파일 전에 전처리기가 처리하는 텍스트 수준의 정의.
C++에서는 값, 상수나 함수를 정의할 수 있다.
// 전처리 전
#defineBUFFER_SIZE1024int buffer[BUFFER_SIZE];
// 전처리 후int buffer[1024];
ThorVG는 이러한 매크로를 사용해서 필요한 엔진만 빌드할 수 있게한다.
이 경우에는 #ifdef를 사용해서 원하는 부분의 코드만 남긴다.
// CPU 엔진을 켠 경우, config.h에서 생성되는 파일
#defineTHORVG_CPU_ENGINE_SUPPORT1
// tvgCanvas.cpp
SwCanvas* SwCanvas::gen(EngineOption op) noexcept
{
#ifdef THORVG_CPU_ENGINE_SUPPORT
// CPU 엔진을 빌드할 때만 이 코드가 남음if (engineInit > 0) {
auto renderer = newSwRenderer(TaskScheduler::threads(), op);
renderer->ref();
auto ret = new SwCanvas;
ret->pImpl->renderer = renderer;
return ret;
}
#endifTVGLOG("RENDERER", "SwCanvas is not supported");
returnnullptr;
}
Shared와 Static
라이브러리 코드를 실행 파일고 결합하는 방식의 차이
Shared
.dll, .so, .dylib등을 사용해서 실행파일과 별개의 파일로 라이브러리를 분리하는 형태.
실행파일에서는 어떤 라이브러리를 사용할 것인지만 기록해둔다.
Static
.lib, .a등을 사용해서 라이브러리 코드를 실행 파일 안으로 복사하는 형태.
실행 파일 하나만 배포하기 쉽지만 크기는 커질 수 있다.
Thorvg의 경우
ThorVG 예시에서는 이렇게 다른 방식으로 빌드된 라이브러리를 thorvg-1.pc파일을 통해서 인식하고 자동으로 처리한다.
이 파일은 ThorVG를 meson으로 빌드할때 자동으로 생성되며, 해당 파일을 통해서 ThorVG의 위치를 찾은 다음 실행하는 방식이다.
Struct, Class, 메모리 레이아웃
C++에서 Struct, Class의 차이
C++에서 Struct는 Class와 거의 차이가 없고, 동일하게 아래의 기능을 사용할 수 있다.
변수, 함수 선언
생성자, 소멸자
상속
가상 함수
public, protected, private
아래처럼 Struct 내부에서도 생성자와 소멸자를 선언하는게 가능하다.
structPoint {
int x;
int y;
Point(int x, int y)
: x(x), y(y)
{
}
voidmove(int dx, int dy)
{
x += dx;
y += dy;
}
};
다만 C++에서 Struct 내부에서 변수를 선언하면 기본적으로 public이지만, Class는 private이다.
ThorVG에서는 대부분의 API가 공개되어 있기 때문에 Strcut를 기본적으로 사용해서 별도의 표시 없이도 공개적으로 사용할 수 있게 만들어두었다.
다만, 사용자가 컨트롤 할 수 없게하거나, 특별한 경우에는 private이나 protected를 사용했다.
예를 들어, Paint Struct의 구조는 아래와 같다.
structTVG_API Paint
{
// 공개 함수들protected:virtual~Paint();
};
이때 virtual을 protected로 만든데에는 사용자가 delete paint;를 사용하지 못하게 함을 위해서이다.
C++에서 delete키워드를 사용하면 아래와 같은 작업이 실행된다.
paint가 가리키는 객체의 소멸자 ~Paint()실행
객체가 사용하던 메모리 해제
ThorVG에서는 사용자가 이를 직접 실행하게 하는 대신, ThorVG의 메서드인 Paint::rel(paint)를 사용해서 아래와 같은 추가 작업을 실행한 후 paint 객체를 삭제한다.
paint가 nullptr이 아닌가?
참조 횟수가 0인가?
두 조건을 만족하면 delete paint
virtual
C++에서는 virtual을 통해서 함수를 선언해야 실제 객체의 메서드가 실행되는 경우가 있다.
예를 들어 아래의 코드에서, 두 클래스를 선언하고 자식 클래스를 코드처럼 변수 선언을 하면 실제로 들어있는건 Shape 객체이지만 포인터의 타입은 Paint*가 된다.
그 상태에서 draw를 실행하면 Shape가 아닌 Paint의 draw함수가 실행되게 된다.
위와 같은 상황을 막기 위해서 메모리, 파일 자원을 안전하게 관리하기 위한 설계 지침으로 Rule of Three/Five/Zero가 있다.
Rule of Three: 아래 세 함수 중 하나를 직접 작성했다면, 나머지 두 함수도 필요할 가능성이 높다는 규칙
소멸자
복사 생성자
복사 대입 연산자
Rule of Five: 위의 세 함수에 추가해, 이동 생성자, 이동 대입 연산자도 추가된 규칙
Rule of Zero: 가능하면 자원을 직접 관리하지 말고, 이미 안전하게 관리해주는 타입을 사용해 특수 멤버 함수를 하나도 작성하지 말자는 규칙
Impl, pImpl 규칙
ThorVG에서는 아래처럼 Struct의 내부구현을 Impl로 별도로 분리해서 하고 있다.
// Paint.hstructPaint {
structImpl;
Impl* pImpl;
};
// Paint.cppstructPaint::Impl {
float opacity;
bool visible;
// 복잡한 내부 데이터와 함수
};
이때 struct 내부에서 직접 구현하지 않고 굳이 Impl로 나누어서 구현한데에는 아래와 같은 이유가 있다.
헤더파일의 간략화
만약 Paint클래스 내부에 여러 메서드를 구현한다면 아래와 같은 모습이 되는데, 이는 다른 의존성을 불러오고, 헤더파일에 새로운 #include문의 삽입을 유도해 파일을 복잡하게 한다.
또한, 사용자의 입장에서 Renderer은 무엇인지 또 알아야 하는 등, 구조 자체도 복잡해지게 된다.
그렇다고 해서 Impl을 분리한 다음 다른 파일에 작성하게 된다면 구조는 간단해지지만 컴파일시 문제가 생긴다.
C++은 컴파일시 객체의 정확한 크기를 알아야 되는데, 다른 파일에 Impl을 선언하면 그 크기를 정확하게 알 수 없기 때문에 결국 Impl은 같은 헤더파일에 선언되어야 하고, 이는 결국 복잡한 파일 구조의 원인이 된다.
따라서 먼저 Impl의 이름만 알려준다음, 해당 객체의 포인터(포인터의 크기는 일정함)를 선언하는 방식으로 이를 해결했다.
structImpl;
structPaint {
Impl* pImpl; // 가능
};
내부 구현을 바꿔도 struct의 크기 유지
처음과 달리 Impl에 기능이 추가된다고 하더라도, 아래의 코드에서 Paint는 여전히 포인터 하나를 가리키기 때문에 Paint의 크기는 유지된다.
크기가 유지되면 생기는 이점으로는 사용자 프로그램과 새 버전의 ThorVG라이버리의 호환될 가능성이 높아진다.
사용자 프로그램을 컴파일 하면 Paint의 전체 크기, 각 멤버의 위치 등을 저장하는데, 만약 struct내부에 직접 멤버를 구현했을 경우 업데이트를 하면 앞서 말한 사항이 달라져 사용자 프로그램이 잘못된 메모리를 읽거나 쓸 수 있다.
사용자가 내부 구현에 의존하기 어려움
다른 곳에 Impl을 구현해 두면서, 사용자는 공개 함수만 사용하게 되고, 내부 멤버를 직접 수정하지 않게 됨으로서 예상치 못한 동작을 방치할 수 없게된다.
paint->pImpl->opacity = 0.5f; // Impl 정의를 모르므로 접근 불가능
다만, 위의 방법을 사용하게 된다면 Impl에 대한 구현사항이 객체 내부가 아닌 별도의 구역에 할당되게 된다. 따라서 Impl을 위한 별도의 메모리 비용 같은 것들이 더 들게 되고, pImpl을 생성하고 삭제하는 과정에서 추가적인 비용도 발생하게 된다.
그렇기에 ThorVG에서는 이를 방지하기 위해 공개 API에서만 이러한 구조를 채택해서 사용하고 있다.
ThorVG의 코드 컨벤션과 메모리
아래는 ThorVG의 코드 컨벤션이다.
classMyClass : publicBase
{
staticconstexprfloatNAME_MAX = 100;
enum Type : uint8_t { Default = 0, Type1, Type2, Type3 };
Type type = Type::Default; // assign an initial value when essentialfloat length = 1.0f;
char* name = nullptr; // use nullptr, not NULL or 0float size; // omit initial value only when not logically needed// pack booleans together for optimal memory alignment (3 bytes here)bool initialized = false;
bool valid = false;
uint8_t refCount = 0;
};
이때 bool을 한 곳에 모아두는 이유는 메모리 패딩을 방지 하기 위해서이다.
CPU는 여러바이트로 구성된 데이터를 특정한 주소 배수에 배치할 때 효과적으로 접근할 수 있다.
bool → 1바이트 단위 주소
uint8_t → 1바이트 단위 주소
float → 4바이트 단위 주소
포인터 → 64비트 환경에서 보통 8바이트 단위 주소
만약 bool과 같은 1바이트 데이터를 float같은 요소들 중간에 끼워넣는다면 float를 4의 배수 메모리 주소에 배치하기 위해서 3바이트가 bool뒤에 추가로 배치되게 된다.
ThorVG에서는 타입마다 다르게 해석해야 하는 멤버들을 하나로 묶어서 메모리 낭비를 줄였다.
그 후 코드를 작성할 땐 해당 RenderSurface의 내부 타입을 통해서(여기에서는 ColorSpace cs를 통해서 판단) union 내부에서 어떤데이터를 어떻게 작업해야 할지 판단한다.
structRenderSurface
{
union {
pixel_t* data = nullptr; // 시스템 쪽 제네릭 포인터uint32_t* buf32; // 픽셀당 4B - RGBA 한번에uint8_t* buf8; // 픽셀당 1B - Grayscale8 마스크
};
Key key;
uint32_t stride = 0;
uint32_t w = 0;
uint32_t h = 0;
ColorSpace cs = ColorSpace::Unknown;
uint8_t channelSize = 0;
bool premultiplied = false;
bool alphaIgnored = false;
};
GPU 메모리 레이아웃
CPU에서 GPU로 메모리를 보낼때는 메모리의 배치가 더 중요해진다.
CPU에서 C++ 구조체를 보낸다고 할 때, GPU는 구조체의 의미를 모르고 그냥 그대로 메모리에 올려두기 때문에, 잘못된 값을 읽는 걸 방지하기 위해 프로그래머가 의도적으로 패딩을 통해서 배치해줘야한다.
예를 들어 Shader에서 사용하는 Uniform값들을 모아둔 Uniform Buffer를 GPU에 복사할 때, std140이라는 규칙에 의해서 uniform buffer의 데이터들은 16바이트의 경계에서 시작해야 된다고 요구 할 수 있다. (정확한 수치는 실제와 다를 수 있음)
이를 위해서 ThorVG는 alignas(16)이라는 키워드를 통해서 메모리의 16의 배수의 위치에 값을 맞춘다.
또한 WebGPU에서는 특정한 규칙에 따라서 각 데이터 블록의 시작하는 위치가 256의 배수여야하는 경우가 있는데, 이 경우에도 ThorVG는 각 데이터 블록을 256바이트의 크기로 만들고, static_assert(sizeof==256)과 같은 코드를 사용해서 데이터가 256바이트인지 검증한다.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
2주전에 있었던 멘토님의 ThorVG C++강의에 나온 개념들을 공부 겸 정리해보았습니다.
아는 내용도 일부 있지만 그래도 다시 본다는 느낌으로 AI의 도움을 받아 작성해 보았습니다.
틀린 부분이나 보충할 부분이 있으면 지적해주시면 감사하겠습니다.
전부 작성한게 아니기 때문에 나중에 기회가 된다면 추가적으로 더 글을 작성할 예정이니다.
매크로
컴파일 전에 전처리기가 처리하는 텍스트 수준의 정의.
C++에서는 값, 상수나 함수를 정의할 수 있다.
ThorVG는 이러한 매크로를 사용해서 필요한 엔진만 빌드할 수 있게한다.
이 경우에는
#ifdef를 사용해서 원하는 부분의 코드만 남긴다.Shared와 Static
라이브러리 코드를 실행 파일고 결합하는 방식의 차이
Shared
.dll,.so,.dylib등을 사용해서 실행파일과 별개의 파일로 라이브러리를 분리하는 형태.실행파일에서는 어떤 라이브러리를 사용할 것인지만 기록해둔다.
Static
.lib,.a등을 사용해서 라이브러리 코드를 실행 파일 안으로 복사하는 형태.실행 파일 하나만 배포하기 쉽지만 크기는 커질 수 있다.
Thorvg의 경우
ThorVG 예시에서는 이렇게 다른 방식으로 빌드된 라이브러리를
thorvg-1.pc파일을 통해서 인식하고 자동으로 처리한다.이 파일은 ThorVG를 meson으로 빌드할때 자동으로 생성되며, 해당 파일을 통해서 ThorVG의 위치를 찾은 다음 실행하는 방식이다.
Struct, Class, 메모리 레이아웃
C++에서 Struct, Class의 차이
C++에서 Struct는 Class와 거의 차이가 없고, 동일하게 아래의 기능을 사용할 수 있다.
아래처럼 Struct 내부에서도 생성자와 소멸자를 선언하는게 가능하다.
다만 C++에서 Struct 내부에서 변수를 선언하면 기본적으로
public이지만, Class는private이다.ThorVG에서는 대부분의 API가 공개되어 있기 때문에 Strcut를 기본적으로 사용해서 별도의 표시 없이도 공개적으로 사용할 수 있게 만들어두었다.
다만, 사용자가 컨트롤 할 수 없게하거나, 특별한 경우에는
private이나protected를 사용했다.예를 들어,
PaintStruct의 구조는 아래와 같다.이때
virtual을 protected로 만든데에는 사용자가delete paint;를 사용하지 못하게 함을 위해서이다.C++에서
delete키워드를 사용하면 아래와 같은 작업이 실행된다.paint가 가리키는 객체의 소멸자~Paint()실행ThorVG에서는 사용자가 이를 직접 실행하게 하는 대신, ThorVG의 메서드인
Paint::rel(paint)를 사용해서 아래와 같은 추가 작업을 실행한 후 paint 객체를 삭제한다.virtual
C++에서는 virtual을 통해서 함수를 선언해야 실제 객체의 메서드가 실행되는 경우가 있다.
예를 들어 아래의 코드에서, 두 클래스를 선언하고 자식 클래스를 코드처럼 변수 선언을 하면 실제로 들어있는건
Shape객체이지만 포인터의 타입은Paint*가 된다.그 상태에서
draw를 실행하면Shape가 아닌Paint의draw함수가 실행되게 된다.그러나
virtual을 사용해서 부모 클래스의 메서드를 선언하면 위와 같이 변수를 선언하고 실행해도 실제 객체 타입에 맞는 메서드가 실행된다.컴파일러가 만드는 특수 멤버 함수
컴파일러에서는 Struct에 대해서 6개의 특수 멤버 함수를 자동으로 만들어 준다.
이때 객체가 간단한 경우에는 컴파일러에서 이를 자동으로 만들어 줄 수 있으나, 포인터나 파일처럼 직접 관리해야하는 자원이 있으면 자동으로 복사나 이동 메서드를 잘못 만들어 줄 수 있다.
이를 방지하기 위해서 ThorVG에서는 매크로를 통해서 복사, 대입 생성자는 delete하고, 생성자는 protected로 바군다.
그리고 생성과 소멸을 각각
gen,rel이라는 2개의 함수에 전적으로 할당하면서 사용자가 API를 사용할 때 생기는 실수를 막고 있다.Rule of Three/Five/Zero
위와 같은 상황을 막기 위해서 메모리, 파일 자원을 안전하게 관리하기 위한 설계 지침으로 Rule of Three/Five/Zero가 있다.
Impl, pImpl 규칙
ThorVG에서는 아래처럼 Struct의 내부구현을
Impl로 별도로 분리해서 하고 있다.이때 struct 내부에서 직접 구현하지 않고 굳이
Impl로 나누어서 구현한데에는 아래와 같은 이유가 있다.만약
Paint클래스 내부에 여러 메서드를 구현한다면 아래와 같은 모습이 되는데, 이는 다른 의존성을 불러오고, 헤더파일에 새로운#include문의 삽입을 유도해 파일을 복잡하게 한다.또한, 사용자의 입장에서
Renderer은 무엇인지 또 알아야 하는 등, 구조 자체도 복잡해지게 된다.그렇다고 해서
Impl을 분리한 다음 다른 파일에 작성하게 된다면 구조는 간단해지지만 컴파일시 문제가 생긴다.C++은 컴파일시 객체의 정확한 크기를 알아야 되는데, 다른 파일에
Impl을 선언하면 그 크기를 정확하게 알 수 없기 때문에 결국Impl은 같은 헤더파일에 선언되어야 하고, 이는 결국 복잡한 파일 구조의 원인이 된다.따라서 먼저
Impl의 이름만 알려준다음, 해당 객체의 포인터(포인터의 크기는 일정함)를 선언하는 방식으로 이를 해결했다.처음과 달리
Impl에 기능이 추가된다고 하더라도, 아래의 코드에서Paint는 여전히 포인터 하나를 가리키기 때문에Paint의 크기는 유지된다.크기가 유지되면 생기는 이점으로는 사용자 프로그램과 새 버전의 ThorVG라이버리의 호환될 가능성이 높아진다.
사용자 프로그램을 컴파일 하면
Paint의 전체 크기, 각 멤버의 위치 등을 저장하는데, 만약 struct내부에 직접 멤버를 구현했을 경우 업데이트를 하면 앞서 말한 사항이 달라져 사용자 프로그램이 잘못된 메모리를 읽거나 쓸 수 있다.다른 곳에
Impl을 구현해 두면서, 사용자는 공개 함수만 사용하게 되고, 내부 멤버를 직접 수정하지 않게 됨으로서 예상치 못한 동작을 방치할 수 없게된다.다만, 위의 방법을 사용하게 된다면
Impl에 대한 구현사항이 객체 내부가 아닌 별도의 구역에 할당되게 된다. 따라서Impl을 위한 별도의 메모리 비용 같은 것들이 더 들게 되고,pImpl을 생성하고 삭제하는 과정에서 추가적인 비용도 발생하게 된다.그렇기에 ThorVG에서는 이를 방지하기 위해 공개 API에서만 이러한 구조를 채택해서 사용하고 있다.
ThorVG의 코드 컨벤션과 메모리
아래는 ThorVG의 코드 컨벤션이다.
이때 bool을 한 곳에 모아두는 이유는 메모리 패딩을 방지 하기 위해서이다.
CPU는 여러바이트로 구성된 데이터를 특정한 주소 배수에 배치할 때 효과적으로 접근할 수 있다.
만약
bool과 같은 1바이트 데이터를float같은 요소들 중간에 끼워넣는다면float를 4의 배수 메모리 주소에 배치하기 위해서 3바이트가bool뒤에 추가로 배치되게 된다.이를 빈 공간을 padding이라고 하고, 이를 방지하기 위해
bool은 한 곳에 모아서 배치한다.union의 활용
union은 각 멤버가 별개의 메모리를 가지는 일반적인 struct의 멤버들과 달리, 모든 멤버가 같은 공간을 사용함으로써 가장 큰 멤버의 크기만큼만 메모리를 차지하는 타입이다.ThorVG에서는 타입마다 다르게 해석해야 하는 멤버들을 하나로 묶어서 메모리 낭비를 줄였다.
그 후 코드를 작성할 땐 해당 RenderSurface의 내부 타입을 통해서(여기에서는
ColorSpace cs를 통해서 판단) union 내부에서 어떤데이터를 어떻게 작업해야 할지 판단한다.GPU 메모리 레이아웃
CPU에서 GPU로 메모리를 보낼때는 메모리의 배치가 더 중요해진다.
CPU에서 C++ 구조체를 보낸다고 할 때, GPU는 구조체의 의미를 모르고 그냥 그대로 메모리에 올려두기 때문에, 잘못된 값을 읽는 걸 방지하기 위해 프로그래머가 의도적으로 패딩을 통해서 배치해줘야한다.
예를 들어 Shader에서 사용하는 Uniform값들을 모아둔 Uniform Buffer를 GPU에 복사할 때, std140이라는 규칙에 의해서 uniform buffer의 데이터들은 16바이트의 경계에서 시작해야 된다고 요구 할 수 있다. (정확한 수치는 실제와 다를 수 있음)
이를 위해서 ThorVG는
alignas(16)이라는 키워드를 통해서 메모리의 16의 배수의 위치에 값을 맞춘다.또한 WebGPU에서는 특정한 규칙에 따라서 각 데이터 블록의 시작하는 위치가 256의 배수여야하는 경우가 있는데, 이 경우에도 ThorVG는 각 데이터 블록을 256바이트의 크기로 만들고,
static_assert(sizeof==256)과 같은 코드를 사용해서 데이터가 256바이트인지 검증한다.All reactions