[원티드 포텐업] 컴파일러 동작
앞부분에서 이것저것하고 이어서 컴파일러의 몇몇 동작을 정리할게요
ECS가 최고아님?!
x86, x64 왜 이렇게 네이밍이 되었을까?
x64: 여기는 이름에 맞게 64bit를 사용하는 아키텍처이다.
x86: 옛날에는 386, 486, 586 컴퓨터 시리즈가 출시되었다. 그 시절 시리즈를 샤라웃해서 x86이라는 이름이 붙은거고, 이름에 걸맞지 않게 32bit 아키텍처이다.
여러개의 CPU 아키텍처가 존재하지만 컴파일러가 아키텍처에 맞게 번역을 해주니
이제 cpu 아키텍처를 고려하지 않고 코딩을 자유롭게 가능하다.
int는 몇 바이트일까?
4바이트요!
100% 4바이트 맞아?!
사실 환경에 따라서 바뀔 수 있는 여지가 있고, 재정의 또한 가능하다.
그렇기 때문에 C++에서는 자료형마다 최소한의 byte수만 보장한다.
타입 재정의
using을 선호 하지만 레거시가 남아있으니까 typedef도 알고 있자
typedef __int32 int32;
using int32 = __int32;
//__int64 ... 64bit로 사용가능
객체지향과 cache hit이 서로 맞지 않는다.
Cache hit: 내가 필요한 데이터가 때마침 캐쉬에 있는 기분 좋은 상황 ( 컴퓨터 구조 시간이 아니니 간단하게...)
객체지향: 추상화/ 캡슐화 + 상속/다형성 + 데이터와 함수의 통합
과연 내가 설계한 class들이 보기좋게 한 곳에 잘 모여있을까?
예시를 봐보자
class A {
int x;
float y;
};
A arr[1000]; // 연속된 구조체 배열 => Cache friendly
cache hit의 관점으로만 보면 편안하다. 근데 객체 지향적으로 조금 더 코드를 만들어볼까?
class Base { virtual void f(); };
class Derived1 : public Base { int a[1000]; };
class Derived2 : public Base { float b[2000]; };
Base* arr[1000]; // 다양한 객체가 힙 메모리에 산재 => Cache unfriendly
오 설계를 잘했네~ 라고 생각할 수 있다.
그러나 cache 관점에서도 좋을까?
Base, Derive1, 2 들은 메모리에 산재되어있을 것이다. 그럼 cache hit과는 거리가 멀어진다.
근데 뭐 어쩔 수 있나,,,, 특히 게임은...
컴파일러 동작
문제들어갑니다. 저는 여기 수업을 들으면서 머리를 15번 정도 친 것 같아요 많은 도움이 될 것입니다.
아래와 같은 코드는 잘 동작할까요? 오류가 날까요?
Log.cpp
#include <iostream>
void Log(const char* message)
{
std::cout << "Hello World" << std::endl;
}
main.cpp
void Log(const char* message);
void main()
{
Log("Hello World");
}
정답
잘 동작합니다.
왜????!?!?!?!??! 아니 header가 없는데? 이게 왜 동작해? 라고 생각할 수 있습니다.
그럼 아래와 같은 코드는 잘 동작할까요?
Log.h
void Log(const char* message);
Log.cpp
#include <iostream>
void Log(const char* message)
{
std::cout << "Hello World" << std::endl;
}
main.cpp
#include "Log.h"
void main()
{
Log("Hello World");
}
그래! 이렇게 header를 추가해야지 잘 동작하지! 라고 생각할 겁니다.
네 맞아요 잘 동작합니다
근데 #include "Log.h"는 사실 전처리기가 코드 덩어리로 바꿔줍니다. (https://tithingbygame.tistory.com/216)
그럼 main.cpp는 컴파일러가 봤을 때 이렇게 보이는 것입니다.
void Log(const char* message);
void main()
{
Log("Hello World");
}
처음 제가 낸 문제와 동일한 상태가 되는거죠? 그러니까 처음 낸 문제가 잘 동작하는 것입니다.
여기서 머리를 몇번 쳤습니다.
우리가 지금까지 배운 header추가해서 선언부 구현부를 나눈건 저희 편하자고 한거였습니다 하하
자 그럼 연계해서 문제를 내겠습니다.
Log.cpp, Log.h, main.cpp까지 존재한 상태일 때
여기서 Log.cpp를 주석처리하면 어떻게 될까요??
LNK오류가 납니다.

처음 뵙는 친구들인게 무슨 의미죠 ?
void __cdecl Log(char const *) 블라블라라며
cdecl 이라는 처음 뵙는 친구가 보입니다.
그럼 cdecl이 뭘까요?
__cdecl 는 Calling Convention (호출규약) 중 하나입니다.
호출 규약은 함수 호출 시 스택에 인자를 어떻게 전달하고, 반환값을 어떻게 처리하며, 호출이 끝난 후 스택을 누가 정리하는 지를 정의하는 규칙입니다.

__cdecl를 기준으로 설명을 하면
Pushes parameteers on the stack, in reverse order(right to left)라고 적혀있다.
순서를 반대로 해서 오른쪽에서 왼쪽이라는데,
이 말은 void func( int a, int b, int c)인 함수가 메모리에 잡혔을 때
메모리에는 int c, int b, int a 순으로 올라간다는 의미이다.
그림으로 보면 이렇다고 볼 수 있음

그리고 cleanup은 caller가 해주는 것을 볼 수 있다.
Caller | 함수를 호출한 쪽이 스택을 정리함. |
Callee | 함수를 정의한 쪽 (함수 내부)에서 스택을 정리함. |
그리고 다음은도 머리를 10번 정도 쳤던 내용이다.
일단 우리가 위의 문제와 이전 시간을 통해서
전처리기가 코드 뭉치로 바꾸고, 컴파일러가 구문확인하고, Linker가 링킹해준다고 이해를 했을 것이다.
그럼 이렇게 링킹되는 것도 당연하다.
저 이제 머리칠 준비하시구요
이렇게 header 파일 하나를 만들어봅시다.
EndBrace.h
}
그리고
main.cpp를 이렇게 만들어보세요
int main()
{
Log("Hello World");
std::cin.get();
#include "EndBrace.h"
어떻게 될까요??
잘 될까요? 안될까요?
우리가 배운 개념으로 보면 #include는 그냥 코드 뭉치로 바꿀뿐이잖아요
그럼 전처리기를 지나고 나서는 main.cpp가 이렇게 바뀔 것입니다.
int main()
{
Log("Hello World");
std::cin.get();
}
그리고 컴파일러는 구문을 확인하겠죠? 이상있나요? 없습니다
그래서 위의 코드는 잘 실행됩니다...!
기억해둘 것
1.
__decl
2.
전처리기는 코드 뭉치로 바꾸고,
컴파일러는 구문해석을 할 뿐..!
Header 추가 == 파일 입출력
3.
컴퓨터가 소수를 어떻게 저장하는 지 정리하기
4.
gpu는 데이터 크기를 안맞춰주면 지가 알아서 2의 제곱수로 만들어서 계산함
근데 그걸 매프레임하기 때문에 프레임이 뚝뚝 떨어진다.
5.
cache hit
#include에서 파일 입출력이 일어난다 -> SSD에서 찾아옴 -> 굉장히 느림
6.
문자열은 가변인가 불변인가?
-> 불변입니다.
string 의 값을 많이 바꿔본 적이 있지 않나요? 그럼 내 값을 바꿀 때 마다 주소가 바뀌는 건가? 라는 생각이 들어서 string의 값을 바꾸면서 주소를 찍어봤는데 주소는 동일하더라구요
그 이유는 이러합니다. 이렇게 string 변수는 메모리에 이렇게 잡혀져있고, 문자열은 어떤 공간에 만들어져있는겁니다.
이 어떤 공간에 만들어진 문자열이 불변이라는 의미입니다.
"a"인 string에 "b"를 추가해서 "ab"를 만들었다고, "a"가 사라지는게 아니라 그냥 "ab"가 추가로 만들어지는겁니다.
// 어셈블러 단계로 가면, 상속 등등 모든 문법이 그냥 함수 콜로 바뀐다.
// 어셈블러에서 포인터와 레퍼런스는 같나 똑같나?
// -> same
// C# / Java에는 pointer가 없나?
// struct -> 값, class -> 참조 (포인터)
// 알아서 바꿔주는거지 뭐