부트캠프

[원티드 포텐업] 컴파일러 동작

장 코범 2025. 7. 15. 16:05
728x90
반응형

앞부분에서 이것저것하고 이어서 컴파일러의 몇몇 동작을 정리할게요

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 -> 참조 (포인터) 
// 알아서 바꿔주는거지 뭐

 

 

 

 

 

 

 

728x90
반응형