Read Write Lock은 스레드의 작업 중 읽기와 쓰기 작업을 나누어서 lock으로 관리하는 것이다. 만약 한 스레드가 작업을 위해 공유 자원에 접근하여 읽고 있는 중에 다른 스레드가 공유 자원의 값을 업데이트 해버리면 어떻게 될까? 읽고 있던 스레드는 작업을 위해 얻은 데이터의 일관성이 깨지게 된다. 이를 막기 위해서 lock으로 문제를 해결할 수 있지만, 한 번에 하나의 스레드만 접근하는 방식보단, 될 수 있으면 많은 스레드가 공유 자원의 일관성을 깨지 않고 작업을 진행하는 것이 훨씬 효율적이라고 생각하지 않는가? 이런 아이디어에서 나온 lock 방식이다.

 

 Read Write Lock은 재귀적으로 작동할 수 있다. lock에 한 번 진입한(lock을 얻은) 스레드는 다른 lock에도 접근할 수 있다는 것이다. 모든 lock에 접근할 수 있는 것은 아니다. 예시로 들어보면 읽기 작업을 하기 위해 Read Lock을 얻은 후 다시 Read Lock을 얻을 수 있다. 그러나 Read Lock을 얻은 후에는 Write Lock을 얻을 수 없다. 이는 읽고 있는 락이 있으면서 쓰기 작업을 하게 된다면 데이터의 일관성이 깨지기 때문이다. 

 

 직접 구현하는 부분은 인프런의 [ C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 Part4: 게임 서버 ] 를 보며 작성하였다.

 

const int EMPTY_FLAG = 0x00000000;
const int WRITE_MASK = 0x7FFF0000;
const int READ_MASK = 0x0000FFFF;

 

 위 플래그는 Read - Write lock에서 lock을 얻는 과정에서 비트 마스크 연산을 위해 사용할 상수이다. 우리는 _flag라는 변수를 따로 선언하여 lock을 구현해볼 것이다.

 

int _flag = EMPTY_FLAG;
int writeCount = 0;

void WriteLock() {
        //같은 스레드가 재귀적으로 락에 접근했을 경우
        int lockThreadId = (_flag & WRITE_MASK) >> 16;
        if (Thread.CurrentThread.ManagerdId == lockThreadId) {
                writeCount++;
                return;
        }
        
        while (true) {
                int desired = (Thread.CurrentThread.ManageredThreadId << 16) & WRITE_MASK;
    	        // Write Lock을 얻기 위해서 현재 작동하는 스레드의 아이디를 Write 부분에 넣어준다.
    	        for (int i = 0; i < 5000; i++) {
    	        // 간단한 스핀락의 형태로 5000번 시도 후 못 얻었다면 양보한다.
    		        // 만약 _flag의 상태가 비어있다면 lock을 얻는다.
    		        if (InterLocked.CompareExchange(ref _flag, desired, EMPTY_FLAG) == EMPTY_FLAG)
        		        return;
    	        }
                Thread.Yield();
        }
}

void WriteUnlock() {
        int lockCount = --writeCount;
        if (lockCount == 0)
                InterLocked.Exchange(ref _flag, EMPTY_FLAG);
}

 

 Write Lock은 재귀적으로 작동할 수 없지만 Read Lock을 읽기만 하기 때문에 재귀적으로 작동할 수 있다. 따라서 카운트하는 변수를 두어 관리한다.

 

void ReadLock() {
        int lockThreadId = (_flag & WRITE_MASK) >> 16;
        if (lockThreadId == Thread.CurrentThread.ManagedThreadId) {
                Interlocked.Increment(ref _flag);
                return;
        }

        while (true) {
                for (int i = 0; i < 5000; i++) {
                        int expected = _flag & READ_MASK;
                        if (Interlocked.CompareExchange(ref _flag, expected+1, expected) == expected)
                                return;
                }
        }
        Thread.Yield();
}

void ReadUnlock() {
        Interlocked.Decrement(ref _flag);
}

 

 사용 방법은 읽는 작업 시 ReadLock 함수를 실행하고, 작업이 끝나면 ReadUnlock 함수를 호출하면 된다. 재귀적으로 실행하고 싶을 때는, ReadLock 함수를 2번 호출하였다면 ReadUnlock 함수를 2번 호출하면 된다.

 

 WriteLock -> WriteLock, WriteLock -> ReadLock, ReadLock -> ReadLock은 가능하지만 ReadLock -> WriteLock은 동작하지 않는다.

 멀티스레드 환경에서 공유자원의 접근을 위해서는 Lock을 사용한다. 스레드는 Lock을 얻기 위해서 여러가지 경쟁 방식을 사용할 수 있다. Lock을 얻을 때까지 계속 대기하는 방식, 랜덤한 시간을 기다렸다가 다시 시도하는 방식, 제 3자가 분배하는 방식이 있다.

 


Spin Lock

 스핀락은 Lock을 얻을 때까지 루프를 돌며 대기하는 방식이다. 제일 간단하지만, 계속 연산을 실행하므로 자원을 낭비한다고 할 수 있다. Interlocked의 CompareExchange는 컴파일러에서 여러번의 작업을 나누어 하는 것이 아닌 한번의 동작처럼 작동하므로 멀티스레드 환경에서 데이터 일관성이 깨지지 않기 때문에 사용하였다.

 

using System;

namespace Server {

    class SpinLock {
        
        volatile int _locked = 0;
        
            public void Aquire() {
                
                while (true) {
                       int expected = 0;
                       int desire = 1;
                       // CompareExchange는 _locked의 값과 expected의 값을 비교하고, 
                       // 값이 같다면 desire의 값을 넣어주고 바뀌기 전 값을 반환한다
                       if (Interlocked.CompareExchange(ref _locked, desire, expected) == expected)
                        	break; 
                }
                
            }
                
            public void Release() {
                
               _lock = 0;
                
            }
        
        }

    class Program {
        
        static void Main(string[] args) {
                
                static Spinlock _lock = new Spinlock();
                
                _lock.Aquire();
                // 공유 자원 작업
                _lock.Release();
                
        }
        
    }

}

 


 

Yield

 다음은 랜덤한 시간 또는 정해진 시간 대기 후 다시 lock 얻는 것을 시도한다. Thread의 Sleep과 Yield를 사용하여 구현한다. 대기하는 동안 다른 작업을 실행하여 무한정 대기하는 것보다 효율이 좋다. 그러나 대기하는 시간이 짧아서 lock을 확인하는 빈도가 잦아지면 Context Switching이 자주 발생하면서 성능이 떨어질 수 있다.

 

using System;

namespace Server {

	class Program {
    
    	    volatile int _locked = 0;
    
    	    static void Main(string[] args) {
            
        	    while (true) {
            
            	        int expected = 0;
                        int desire = 1;
                        if (Interlocked(ref _locked, desire, expected) == expected)
                	        break;
                
                        Thread.Sleep(1); // 1ms 동안 대기
                        Thread.Sleep(0); // 우선 순위가 더 높은 스레드에게 양보
                        Thread.Yield(); // 작업이 있는 모든 스레드에게 양보
            
                  }
        
            }
    
       }

}

 


AutoResetEvent

 AutoResetEvent는 제 3자가 스레드의 작업 순서를 정해주는 방법이다. 다른 스레드의 작업이 끝나고 새로 작업을 시작할 수 있으면 예약을 걸어놓은 스레드에게 이벤트로 알려주는 것이다. 사실 기존의 Lock이 잠겨있는지 확인하려면 커널영역까지 접근하여 확인을 해야하는데 이 과정이 매우 느리다. 따라서 다른 작업을 하고 있다가 제 3자에게 이벤트를 받는 방식이 효율적이라고 할 수 있다.

 

using System;

namespace Server {

    class Program {
    
        AutoResetEvent _AREvent = new AutoResetEvent(true);
        // 생성자에서 false면 잠긴상태, true면 열린상태
        
        public void Aquire() {
        
            _AREvent.Set();
            // Lock을 열린 상태로 만들어준다.
        
        }
        
        public void Release() {
        
            _AREvent.WaitOne();
            // Lock을 닫힌 상태로 만들어준다.
            // WaitOne안에는 Reset 메소드도 포함되어 있다. Reset 메소드가 닫힌 상태로 만든다.
        
        }
    
        static void Main(string[] args) {
        
            
            Aquire();
            // 작업
            Release();
        
        }
    
    }

}

 

 AutoResetLock 말고도 ManualResetLock이 존재하는데, 이는 WaitOne 함수에 Reset 메소드가 들어있지 않아서 직접 Reset 함수를 호출하여 Lock을 닫아주어야 한다. 한 번에 하나의 스레드가 아닌 한 번에 여러 개의 스레드가 작업을 할 때 이용한다. 

'OS' 카테고리의 다른 글

OS - Read Write Lock / Reader Writer Lock  (1) 2024.07.23
OS - Context Switching (문맥 교환)  (1) 2024.07.16
OS - Lock, InterLocked - C#  (0) 2024.07.15
OS - 메모리 배리어 - Memory Barrier  (0) 2024.07.11
OS - 캐시(Cache)  (0) 2024.07.09

 Context Switching은 스레드가 프로세스를 교환하는 작업을 의미한다. 멀티태스킹이 동작하는 원리를 보면 스레드는 정해진 시간, 즉 타임 슬라이스 동안 프로세스를 작업 후 다른 프로세스의 작업을 실행한다. 하나의 스레드가 시간을 쪼개서 여러 개의 프로세스에서 작업을 진행하는 것이다. 이를 시분할 시스템이라고 한다. 이 과정에서 작업을 교체할 때 문맥 교환이 이루어진다. 프로그램의 입장에서 오버헤드가 발생한다고 표현한다. 따라서 타임 슬라이스가 짧게 설정된다면 문맥 교환이 자주 일어나면서 프로그램의 성능이 떨어지게 되는 것이다. 

 

 문맥 교환의 대부분은 멀티태스킹 환경에서 발생하지만 입출력 인터럽트에서도 발생한다. 인터럽트가 발생하면 입력/출력을 할 때까지 대기를 한다. 이 과정에서도 문맥 교환이 발생한다. 이러한 대기 시간이 다른 작업에 문제가 되어 해결할 수 있는 비동기 입출력도 존재하지만 다음에 다뤄보도록 할 것이다. 다음으로는 사용자/커널 모드 전환이 있다. 운영체제에 따라 다르다.

 멀티스레드 환경에서 공유 자원에 접근 시, 동시에 접근하게 된다면 데이터의 일관성이 깨질 것이다. 따라서 한 번에 하나의 연산이 이루어져야 하는데, 이것을 가능하게 해주는 것이 lock이다. 하나의 스레드가 공유 자원에 대해 lock을 가지게 되면 모든 작업 완료 후 lock을 다시 반납한다. 그 동안 다른 스레드는 lock을 얻을 때까지 대기를 한다. 

 

 임계구역(Critical Section)은 lock을 잡은 상태로, 하나의 스레드만 진입할 수 있는 공간이다. 이 공간에 진입을 해야만 공유 자원에 대한 접근이 가능해진다. 이는 상호배제(Mutual Exclusion) 원칙을 충족한다.

 

 Lock을 얻고, 반납하는 과정에서 지연시간, 즉 오버헤드가 발생하게 된다. 따라서 Lock이 너무 많이 있으면 오버헤드 때문에 효율이 떨어진다. 그렇다고 Lock의 개수를 줄이고 임계구역의 길이를 늘리게 된다면, 다른 스레드들이 작업을 하지 못하기 때문에 효율이 떨어진다. 적당히 조절해야 한다.

 

 사실 Lock은 경쟁을 통해 얻게 되는데, 경쟁에서 밀려 Lock을 얻지 못한 스레드는 계속 멈춰있는다. 이를 기아상태라고 한다. 해결을 위해 CPU 스케줄링으로 우선순위를 부여하여 공정하게 Lock을 얻을 수 있게 하는 방법이 있다. 여러 방식이 있지만 이 글에서는 다루지 않을 것이다.

 

 Lock을 얻는 과정에서 문제가 발생할 수 있는데 교착상태(Dead Lock)이라고 하는 문제가 발생한다. 이건 예시를 들어 설명하겠다. A 프로세스는 실행 중 A라는 공유자원을 들고있고, B라는 자원이 필요한 상황이다. 그리고 B 프로세스는 B라는 공유자원을 들고 있고, A라는 자원이 필요한 상황이다. A와 B 프로세스는 서로가 필요한 자원을 들고있으며, 누구 하나가 Lock을 해제할 때까지 계속 기다리는 상황이 될 것이다. 이런 상황을 미리 방지하는 것이 좋다.

 


 

InterLocked

 위에서 언급했던 데이터의 일관성이 깨지는 부분에 관한 이야기다. 여러 개의 스레드가 동시에 하나의 공유 자원에 접근을 하여 수정하였다면, 내부적으로 어떤 과정이 일어날까? 보통 컴파일러의 작동 과정은 변수의 값을 불러오고, 값을 수정하고, 변수에 수정한 값을 넣어주는 과정을 거친다. 동작이 여러 번 나누어져 실행되기 때문에 일관성이 깨지는데, 이를 해결하는 것이 C#의 InterLocked이다. InterLocked를 사용하면 동작이 한 번에 실행되는 효과를 가진다.

 

InterLocked.Increment(ref number);
InterLocked.Decrement(ref number);

 


Lock / Monitor

lock 문은 임계구역을 직접 설정해줄 수 있다. 따라서 한 번에 하나의 스레드만 접근이 가능하다.

 

object obj = new object();

lock(obj) {

	~~~

}

 

 Monitor 클래스는 lock과 비슷한 역할을 한다. 명시적으로 락을 얻고 반납하는 것을 설정한다.

 

object obj = new object();

Monitor(obj).Enter();
~~~
Monitor(obj).Exit();

 


 메모리 배리어는 코드의 실행 순서를 강제하는 코드라고 볼 수 있다. 컴파일러 최적화 과정 중 코드가 원하는 순서에 맞지 않게 최적화 되는 경우가 있는 것을 방지해준다. 컴파일러 최적화가 궁금하다면 전에 포스팅 한 글을 읽어보는 것을 추천한다.

 

 

[OS] 컴파일러 최적화

굳이 따지자면 컴파일러 최적화는 운영체제와 관련이 없다. 그러나 이후 멀티스레드 프로그래밍과 관련하여 원하는 대로 동작하지 않는 경우가 존재한다. 이때 컴파일러가 스스로 코드를 최적

mainsdev.tistory.com

 

 사용 방법은 매우 간단하다. 꼭 순서가 지켜졌으면 하는 코드 아래 부분에 한 줄만 추가하면 된다.

 

using System;

namesapce Server {

		class Program {
        
        		static int x = 0;
                	static int y = 0;
                	static int r1, r2;
        
        		public void Function1() {
                		x = 1
                        	//Thread.MemoryBarrier();
                        	r1 = y;
                	}
                
                	public void Function2() {
                		y = 1
                        	//Thread.MemoryBarrier();
                		r2 = x;
                	}
        
        		static void Main(string[] args) {
                
                		while (true) {
                        		Task t1 = new Task(Function1);
                        		Task t2 = new Task(Function2);
                                
                                	t1.Start();
                                	t2.Start();
                                
                                	Thread.WaitAll(t1, t2);
                                
                                	if (r1 == 0 && r2 == 0) 
                                			break;
                        	}
                	}
        	}
}

 

 위 코드는 순서대로 실행이 된다고 가정하면 영원히 while문을 탈출하지 못할 것이다. 그러나 최적화로 인해 r1, r2에 x와 y의 값을 대입한 후 x와 y에 1을 넣는 작업을 하여 순서가 뒤바뀌게 되어 탈출하게 된다. 주석처리를 한 부분을 주석 해제하고 다시 돌려보면 프로그램이 종료되지 않는다. 이는 변수의 값을 업데이트 해주는 작업인데, 메모리 배리어를 만나는 순간 변수의 값을 확실하게 업데이트 해줌으로써 순서를 지켜준다. 데이터베이스의 COMMIT 명령어를 실행 후 DB를 검색하는 것이라고 생각이 들었다.

 

 volatile과 메모리 배리어는 비슷한 기능을 하고 있으나, 메모리 배리어는 가시성을 확보해준다는 점에서 차이가 있다. 프로그램의 흐름을 알기 쉬운 것이다. 

 

'OS' 카테고리의 다른 글

OS - Context Switching (문맥 교환)  (1) 2024.07.16
OS - Lock, InterLocked - C#  (0) 2024.07.15
OS - 캐시(Cache)  (0) 2024.07.09
OS - 컴파일러 최적화  (0) 2024.07.09
OS - 스레드 생성(Task) - C#  (0) 2024.07.08

 컴퓨터 프로그램의 동작과정을 생각해보자. 아주 간단하게 생각해보자면 프로세스로 바뀐 프로그램은 메모리(RAM)으로 올라가서 CPU에 의해 실행된다. 작업 과정에서 CPU는 작업에 필요한 정보들을 불러와야 한다. 이러한 정보들은 프로그램이 저장되어 있는 저장소(HDD, SSD)에 저장이 되어있다. 작업을 할 때마다 저장소에서 정보를 불러온다면 어떤 일이 일어날까? 사실 프로그램 실행에 있어서는 아무 문제가 되지 않을 것이다. 그러나 반복 작업에 있어서 정보를 불러오는 작업을 계속한다면 매우 오래 걸릴 것이다. 이를 해결하기 위해 자주 사용하는 데이터는 캐시(Cache)라는 공간에 저장하여 빠르게 불러오는 것이다.

 

 캐시(Cache)는 CPU와 메모리 사이에 존재하는 작은 저장공간이다. 속도가 빠른 메모리를 사용하기도 하고, 물리적으로 가까이 붙어있어서 저장공간에서 불러오는 것에 비해 빠르다. 보통 L1, L2 등 캐시를 분류하기도 한다. 더 빠르고, 더 가까이 있는 차이다. 다른 블로그에서 본 글인데, L1은 CPU의 코어마다 할당되어 IL1(Instructure), DL1(Data)으로 분류된다고 한다. L2는 모든 코어가 공유하는 데이터가 들어간다고 한다. 


Cache Hit / Cache Miss

CPU는 작업을 하며 정보가 필요할 때 캐시(Cache)에서 원하는 정보가 있는지 찾아본다. 이때 원하는 정보가 존재하여 바로 가져오는 상황을 Cache Hit라고 하며, 정보가 없어서 저장소에서 찾는 상황을 Cache Miss라고 한다. 


 Locality

 지역성은 데이터의 모든 부분을 균일하게 참조하는 것이 아닌 특정 부분을 집중하여 참조하는 특성이다. 캐시(Cache)에는 2가지 지역성이 있다. Temporal Locality와 Spatical Locality이다. Temporal Locality는 최근에 사용되었던 데이터는 다음에도 사용될 것이라고 가정하는 것이다. 따라서 한 번이라도 사용된 데이터는 캐시에 저장해놓는다. Spatical Locality는 최근에 사용된 데이터의 주변 데이터도 사용될 것이라고 가정하는 것이다. 

 

 코드로 예시를 들어보겠다. 

 

int[10000][10000] arr = new int[10000, 10000];

for (int i = 0; i < 10000; i++) {
	for (int j = 0; j < 10000; j++) {
    		arr[i, j] = 1;
    	}
}

for (int i = 0; i < 10000; i++) {
	for (int j = 0; j < 10000; j++) {
    		arr[j, i] = 1;
    	}
}

 

 위 코드에서 반복문을 보면 배열에 접근하는 순서가 다르다. [i, j] 와 [j, i]의 차이이다. 반복되는 횟수는 동일하다. 어느 반복문이 더 빨리 실행될까? 정답은 위의 반복문이 더 빠르다. 그 이유에는 메모리의 구조와 캐시 지역성의 영향이 있다. 메모리의 구조는 아래 코드 블럭을 보면 [i, j]로 접근 후, [i, j+1]에 접근하는 순서로 진행하게 된다. 이때 주변 메모리의 값도 미리 가져오는 Spatical Locality에 의해 미리 배열의 값을 캐시에 저장하였기 때문에 빠르게 처리할 수 있었던 것이다.

 

[0, 0][0, 1][0, 2][0, 3][0, 4] / [1, 0][1, 1][1, 2][...][] / [][][][][] / [][][][][]...

 


 캐시(Cache)는 단일 스레드 환경에서는 정상적으로 작동을 잘 하지만, 멀티 스레드 환경에서는 코어마다 들고 있는 캐시 값이 서로 맞지 않는 상황이 발생할 수도 있다. 이후 프로그래밍을 할 때, 이 문제를 해결할 방법들을 공부할 예정이다.

'OS' 카테고리의 다른 글

OS - Lock, InterLocked - C#  (0) 2024.07.15
OS - 메모리 배리어 - Memory Barrier  (0) 2024.07.11
OS - 컴파일러 최적화  (0) 2024.07.09
OS - 스레드 생성(Task) - C#  (0) 2024.07.08
OS - 스레드 생성(Thread, ThreadPool) - C#  (0) 2024.07.08

 굳이 따지자면 컴파일러 최적화는 운영체제와 관련이 없다. 그러나 이후 멀티스레드 프로그래밍과 관련하여 원하는 대로 동작하지 않는 경우가 존재한다. 이때 컴파일러가 스스로 코드를 최적화하여 돌리기 때문인데, 어떤 이유에서 최적화를 하고 돌리는지 알아보자.

 

 컴파일러 최적화? 프로그래밍을 해봤다면 대부분 컴파일러라는 단어와 최적화라는 단어는 많이 들어봤을 것이다. 그러나 둘이 합쳐지니 어떤 것을 뜻하는지 전혀 예상이 가지 않는다. 컴파일러 최적화는 컴파일러가 스스로 코드를 최적화하여 더욱 성능이 좋게 코드를 돌리게 해주는 기능이다. 겉으로만 들었을 때는 정말 좋은 기능이라고 생각이 들 수 있다. 물론 싱글스레드 환경에서는 좋은 기능이 맞다. 그러나 멀티스레드 환경에서는 의도치 않게 실행될 수 있기 때문에 어떤 식으로 최적화가 이루어지는지 알아야 문제점을 찾을 수 있다.

 

 사실 인프런에서 듣던 강의에서는 간단한 예제만 보여주고 컴파일러에서 최적화 하는 과정을 어셈블리를 통해서 보여주었는데, 찾다보니 더 많은 경우가 있었다. 

 


강의 예제

using System;

namespace Server {

		class Program {
        
        		static bool _stop = false;
        
        		static void ThreadFunction() {
                
                		while (_stop == false) {
                        		
                        	}
                
                	}
                
                	static void Main(string[] args) {
                
                		Task t = new Task(ThreadFunction);
                        	t.Start();
                        
                        	Thread.Sleep(1000);
                        
                        	_stop = true;
                
                	}
        
        	}

}

 

 위의 코드를 실행해본다고 가정해보자. 코드의 흐름대로 설명을 하자면 Task가 실행되어 ThreadFunction이 실행된 후, 1초를 대기한다. 그 이후 전역 변수인 _stop의 값을 변경하면 ThreadFunction의 while문을 탈출하여 프로그램이 종료되어야 할 것이다. 그러나 컴파일러는 코드를 최적화하여 돌린다. 프로그램 입장에서 지속적으로 _stop의 값을 확인하는 것은 매우 부담되는 일이다. 그 이유는 어셈블리 관점에서 값을 불러오는 LDA 연산이 가장 시간이 오래걸린다고 알고있다. 따라서 컴파일러는 빠른 실행을 위해 아래 코드와 같이 최적화를 진행한다.

 

using System;

namespace Server {

		class Program {
        
        		static bool _stop = false;
        
        		static void ThreadFunction() {
                
                		if (_stop == false) { // 변경된 부분
                        
                        		while (true) {
                                
                                	}
                        	}
                
                	}
                
                	static void Main(string[] args) {
                
                		Task t = new Task(ThreadFunction);
                        	t.Start();
                        
                        	Thread.Sleep(1000);
                        
                        	_stop = true;
                
                	}
        
        	}

}

 

 위와 같이 최적화 됨에 따라서 프로그램은 지속적으로 _stop을 참조하지 않고 while문을 반복하게 된다. 이를 해결할 수 있는 방법이 있는데, 바로 volatile을 이용하는 것이다. 타입 앞에 volatile 키워드만 붙여주면 컴파일러의 최적화를 막아주어 원하는 대로 프로그램을 돌릴 수 있다.

 


 

Constant Folding

 상수로 구성된 식을 미리 계산하여 프로그램을 실행한다. 

int number = 1 + 2 * 3 - 4;

////////////////////////////////

int number = 3;

 

 위의 코드를 아래 코드로 바꾸어 실행한다.

 


Dead Code Elimination

 불필요한 계산/코드를 제거하여 프로그램의 크기를 줄인다.

int x = 1;
int y = 2;

int result = x + y;

Console.WriteLine(result);

///////////////////////////////////////////

int x = 1;
int y = 2;

Console.WriteLine((x + y));

 

해당 변수 부분에 직접 연산하는 방법을 사용하여 불필요한 변수의 사용을 줄인다.

 


Loop Optimization

 반복문을 효율적으로 실행하기 위해 반복횟수를 줄이거나 내부 계산을 최적화 한다.

int n = 10;
int sum = 0;

for (int i = 0; i < n; i++) {
	sum += i;
}

//////////////////////////////

int n = 0;
int sum = (n * (n - 1)) / 2;

 

 간단한 수식으로 바꾸어 연산을 10번에서 1번으로 바꾸었다.

 


Memory Optimization

메모리 사용을 최적화하여 프로그램 성능 향상한다.불필요한 메모리 할당을 제거하거나, 데이터를 캐시에 저장하여 사용한다.

int[] array = new int[10];

for (int i = 0; i < array.length; i++) {
	array[i] = i;
}

/////////////////////////////////////

int[] array = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

 


Inline Function Expansion

 함수 호출을 함수의 본문으로 대체하여 호출 오버헤드를 줄인다.

int x = 1;
int y = 2;
int z = addNumber(a, b);

int addNumber(int x, int y) {
	return x + y;
}

////////////////////////////////////

int x = 1;
int y = 2;
int z = x + y;

 


Data Flow Analysis

변수의 사용과 변화를 추적하여 불필요한 계산을 제거한다.

int x = 1;
int y = x * 2;
int z = y + 3;

Console.WriteLine(z);

////////////////////////

int x = 1;
int z = (x * 2) + 3;

Console.WriteLine(z);

 


 이 글은 인프런 "[C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버" 강의와 아래 블로그를 참고하여 작성하였다.

 

컴파일러 최적화

컴파일러 최적화

velog.io

'OS' 카테고리의 다른 글

OS - 메모리 배리어 - Memory Barrier  (0) 2024.07.11
OS - 캐시(Cache)  (0) 2024.07.09
OS - 스레드 생성(Task) - C#  (0) 2024.07.08
OS - 스레드 생성(Thread, ThreadPool) - C#  (0) 2024.07.08
OS - 멀티스레드 개요  (1) 2024.07.05

 직전에 쓴 Thread와 관련된 글이다. 

 

[OS] 스레드 생성(Thread, ThreadPool) - C#

C#을 이용하여 스레드를 생성해보자. 기본 쓰레드 생성using System;namespace Server { class Program { static void newFunction() { Console.WriteLine("Thread Running..."); } static void Main(string[] args) { Thread t1 = new Thread(newFunct

mainsdev.tistory.com

 

 이 글은 C#에서 지원하는 Task에 대하여 작성할 예정이다. Task는 Thread와 ThreadPool을 보완하여 만들어졌다. 특수한 상황이 아니라면 대부분 Task를 사용한다. Task는 ThreadPool의 스레드를 공유한다. 

 


Task

using System;

namespace Server {
    
        class Program {
    
                static void ThreadFunction() {
                        Console.WriteLine("Thread");
                }
    
                static void Main(string[] args) {
                        Task t = new Task(ThreadFunction);
                        t.Start();
                }
    
        }
}

 

사용방법은 Thread와 비슷하다. 스레드가 끝날 때까지 기다리는 Wait() 함수와 WaitAll() 함수가 존재한다. 

'OS' 카테고리의 다른 글

OS - 메모리 배리어 - Memory Barrier  (0) 2024.07.11
OS - 캐시(Cache)  (0) 2024.07.09
OS - 컴파일러 최적화  (0) 2024.07.09
OS - 스레드 생성(Thread, ThreadPool) - C#  (0) 2024.07.08
OS - 멀티스레드 개요  (1) 2024.07.05

+ Recent posts