C#

내 코드가 느린 이유 - 최적화에 대한 고찰

kark 2025. 2. 10. 11:01
728x90

숙련된 게임 개발자는 타겟 플랫폼에 성능을 최적화 하는 작업이 개발 사이클 전체에 걸쳐 진행된다.

고사양 PC에서는 원활할 수 있지만, 저사양 모바일 플랫폼도 타겟팅 한다면 문제가 달라질 수 있다.

 

풍부한 환경 디테일, 스케일, 메카닉, 동작, 물리 와같은 많은 기능을 요구하지만,

그로인해 특정 구간, 프레임 중 눈에 띄는 성능 저하가 발생될 수 있다.

 

대부분의 프레젝트에서의 문제는 렌더링과 관련이 있다.

너무 큰 텍스쳐, 복잡한 메시, 비용이 큰 쉐이더, 배칭, 컬링, LOD 를 효율적으로 활용하지 못했을 경우이며

특별한 게임을 만들기 위해 작성한 C# 코드가 프레임당 CPU 시간을 지나치게 소모하고 있을 수 도 있다.

 

그렇다면 내 코드가 느린 이유를 따져보자

 

 

1. 가비지 컬렉션에 의해 발생되는 오버헤드와 일시정지

 

가비지 컬렉션은 프로그래밍에서 더이상 사용되지 않는 메모리를 자동으로 회수하는 기능이다.

이 기능덕에 개발자가 메모리를 직접 관리하지 않아도 되지만, GC 가 동작될 때 CPU 사용량이 증가하며

일시적으로 코드 실행이 멈추는 문제가 발생될 수 있다.

 

여기서 말하는 오버헤드란?

특정 작업을 수행하기 위해 추가적으로 필요한 비용(시간, 메모리, CPU 사용량 등)을 의미

즉, 프로그램이 원래 수행하려는 작업 외 추가적으로 발생되는 연산, 리소스 사용량을 뜻한다.

가비지 컬렉션에서는 CPU 사용 증가, 일시정지, 메모리관리 비용이 오버헤드로 작용될 수 있다.

 

가비지 컬렉션의 문제점

 - CPU 오버헤드 : GC가 실행될 때 많은 CPU 리소스를 사용할 수 있다. 특히 많은 객체를 할당하거나 제거해야 하는 경우 프레임 드랍 성능 저하가 발생

 

 - 일시정지 : GC 가 실행되는 동안 애플리케이션이 밀리초 동안 완전히 멈추며, 게임에서는 이로 인해 프레임 드랍이 발생된다

 

 - 메모리 단편화 : 메모리를 자주 할당/해제 하게 될경우 메모리 블록이 조각나며, 새로운 객체 할당할 공간을 찾기가 어려워 지므로 이는 간접적으로 추가 GC 실행을 유발하게 됨

 

 

2. 최적화되지 않은 컴파일러 생성 머신 코드

 

컴파일러 마다 코드 최적화 방식이 다르며, 어떤 컴파일러는 덜 최적화된 코드를 생성할 수 있음을 의미한다.

즉, 같은 소스 코드라도 사용하는 컴파일러, 실행 환경에 따라 성능이 다를 수 있음을 의미한다.

 

컴파일러란 ?

https://kark.tistory.com/101

 

컴파일러 Compiler

컴파일러는 프로그래밍 언어로 작성된 소스 코드를 기계어로 변환하는 프로그램이다.평소에 작성하는 코드는 사람이 이해하기 쉬운 고수준 언어이지만 컴퓨터는 기계어 0과 1로 된 코드만 이해

kark.tistory.com

 

최적화 하지않은 머신 코드가 발생되는 이유

 

 - 컴파일러마다 최적화 수준이 다름

같은 C++ 코드라도 GCC, Clang, MSVC 같은 C++ 컴파일러라도 서도 다르게 최적화가 가능하며

C#의 경우 .NET JIT 컴파일러와 AOT 컴파일러(Mono, IL2CPP) 도 서로 다른 최적화가 적용된다.

 

 - 실행 플랫폼이 다름

윈도우용 컴파일러가 생성한 머신코드와 ARM 모바일 기반 컴파일러가 생성한 머신 코드는 다를수 밖에 없다.

https://kark.tistory.com/102

 

실행 플랫폼 별 생성되는 기계어 코드가 다른 이유

컴파일러는 실행할 플랫폼에 맞는 기계어 코드를 생성해야한다.하지만 운영체제와 CPU 아키텍처마다 명령어 세트, 실행 방식이 다르기에 동일한 소스코드라도 각 플랫폼에 맞게 다르게 변환된

kark.tistory.com

 

 

x86 CPU 에서는 벡터화(SIMD) 최적화 = 한번에 여러 데이터 연산

ARM 에서는 다른방식의 최적화로 실행되는 플랫폼마다 성능차이가 발생된다.

 

 - 컴파일러 최적화 옵션 설정 차이

컴파일러 최적화 옵션 설정(O1, O2, O3, Ofast 등) 하면 더 빠른 머신 코드를 생성할 수 있음

하지만 최적화를 아예하지 않거나 기본 최적화만 적용하면 성능이 더 낮은 머신코드가 생성 될 수 있다.

 

3. 비효율적인 CPU 코어 사용

 

요즘은 저사양 기기에도 멀티코어 CPU 가 탑재 되지만, 멀티 스레드 코드를 작성하는것은 어렵고 오류가 발생되는 경우가 많다는 이유로 많은 게임 대부분의 로직을 메인스레드에서만 실행하는 경우가 허다함

 

 

4. 캐시 친화적이지 않은 데이터

 

CPU 캐시 ?

CPU는 데이터를 처리할 때 캐시 -> RAM -> HDD/SSD 순으로 데이터를 가져온다. 즉 캐시로부터 데이터를 가져오는것이 가장 빠른 방법이며, RAM 메인 메모리에서 데이터를 읽어오는것은 훨씬 느리다.

 

캐시 친화적이지 않은 데이터 ?

메모리에서 가져오는 방식이 비효율적이여서 CPU 캐시를 효과적으로 활용하지 못하는 데이터

CPU가 자주 사용하는 데이터를 캐시에서 유지하지 못하고 RAM 에서 가져와야할 때 성능 저하 발생

이를 방지하기 위해선 데이터가 캐시 친화적하게 배치되어야 한다

 

 

방법 1 연속적인 메모리 활용 - LinkedList 의 경우 노드가 메모리의 여러곳에 분산되어 있으므로 노드마다 접근할 경우 RAM 에 접근해야할 가능성이 높으며, 반면 기본 배열의 경우 연속된 메모리구조로 여러개의 데이터를 CPU 캐시에 로드하므로 빠르게 접근이 가능하다.

 

방법 2 데이터 접근 패턴 고려 - 이차원 배열을 순회 할때 행, 열 접근이 배열사이를 뛰어다니는 구조가 아닌지 확인한다.

 

방법 3 구조체 정렬 최적화 - char int char 순으로 선언될경우 1byte - 4byte - 1byte 순으로 섞이게 될경우 메모리 패딩이 발생되어 캐시 비효율적이다. 메모리를 연속적으로 배치하여 캐시 친화적으로 작성한다 int - char - char

 

방법 4 지역변수를 적극 활용