기존에 티스토리에서 운영 중이던 프로그래밍 관련 블로그(devnote.tistory.com)를 Octopress로 이사합니다. 사실 운영이라고 말하기도 뭣할 만큼 오랫동안 방치되어 있었는데, 다시금 분위기를 쇄신하고자 환경을 바꿔볼까 합니다.
- +기존 블로그를 feedburner 주소로 구독중이었다면 새로운 블로그로 자동으로 넘어갑니다. 하지만 티스토리 기본 rss 주소를 사용중이었다면, 이참에 feed-burner로 갈아타 주세요 ‘ㅁ’)/
feed burder address : http://feeds.feedburner.com/florist_devnote
diff --git a/2014/07/16/the-benz-programmer/index.html b/2014/07/16/the-benz-programmer/index.html index 21265422..fe069f98 100644 --- a/2014/07/16/the-benz-programmer/index.html +++ b/2014/07/16/the-benz-programmer/index.html @@ -3,7 +3,7 @@ - + @@ -125,12 +125,12 @@ @@ -218,7 +218,7 @@요 며칠간 이 책을 읽었습니다. 회사 도서관에 갔다가 제목이 끌려서 한 번 읽어봤어요.
누가 정한건지 모르겠지만 책 제목 참 멋지게 지었습니다. 주변에서 제가 이 책 읽는 것 보면 모두들 제목에 대해 관심을 보이더군요 ㅎ
진로를 고민중인 학생이나 일을 시작한지 얼마 되지 않는 신입 개발자들을 주 대상으로 삼은 책입니다. 다소 주관적이긴 하지만 선배 개발자 입장에서 들려주는 이런 저런 이야기들이 적혀 있어요.
저자는 자기관리를 잘 하는 분이신 듯 합니다. 구체적인 개인 목표를 세우고 달성을 위해 노력하는 점이라든지, 꾸준한 자기계발에 관심을 두는 점 같은 좋은 습관을 많이 만들어두신 것 같네요.
저는 작업 도중에 빌드 걸어놓고 잠깐씩 기다리는 동안에 주로 읽었습니다.
빌드시간에 조금 난이도가 있는 기술서적을 읽을 때는, 내용을 좀 따라가려다 보면 빌드가 끝나서 흐름이 끊기고, 이게 계속 반복되다보니 책에 제대로 집중할 수가 없었습니다.
그래서 빌드시간에 책읽는 것은 거의 포기를 하고 있었는데, 이런 책은 부담없이 읽을 수 있어서 빌드 중에 읽어도 괜찮더군요.
그래서 앞으로는 빌드하는 중에 이런 가벼운 책들 읽으면 되겠구나 하는 생각을 해봤습니다.
저는 책을 읽다가 조금 엉뚱한 구절에 눈길이 확 쏠렸는데,
diff --git a/2014/07/19/google-c-plus-plus-style-guide/index.html b/2014/07/19/google-c-plus-plus-style-guide/index.html index 7021a6e2..96275439 100644 --- a/2014/07/19/google-c-plus-plus-style-guide/index.html +++ b/2014/07/19/google-c-plus-plus-style-guide/index.html @@ -3,7 +3,7 @@ - + @@ -126,12 +126,12 @@ @@ -217,7 +217,7 @@
지금 참여중인 프로젝트에서 얼마전에 코딩 컨벤션을 통일하는 작업이 있었습니다.
본격적으로 컨벤션을 통일하고 이제 한 서너달? 정도 지난 것 같네요.
처음에는 팀원 대다수가 많이 혼란스러워 했지만 이제 어느 정도 시간이 지나고 나니 팀 내 프로그래머 모두가 거의 유사한 스타일의 코드를 작성하게 됐습니다. 이렇게 되니 전보다 코드 가독성이 좋아지고 협업을 할 때 이런 저런 많은 도움이 됩니다.
-
+
사실 컨벤션이 통일되면 좋다는 것은 아주 상식적인 말입니다만, 개개인이 선호하는 스타일이 다 다르기 때문에 통일을 하기가 쉽지 않다는 것이 문제입니다. 팀에서도 그동안 몇 차례 시도 했었지만 잘 안되었다가, 이번에서야 겨우 성공했어요.
이번에 컨벤션의 통일을 성공한 주된 요인 중의 하나는 구글 내부에서 사용하는 컨벤션을 정리해서 공개한 구글 C++ 스타일 가이드라고 볼 수 있습니다. 이 문서의 내용을 가져와 몇 가지 사항만 프로젝트에 맞게 조정하여 적용 하였지요. 구글 컨벤션의 코드들은 처음 볼 땐 좀 낮설었지만 적응하고 나니 이젠 괜찮군요.
diff --git a/2014/07/21/octopress-tips-on-windows/index.html b/2014/07/21/octopress-tips-on-windows/index.html
index b369ca3c..81633968 100644
--- a/2014/07/21/octopress-tips-on-windows/index.html
+++ b/2014/07/21/octopress-tips-on-windows/index.html
@@ -3,7 +3,7 @@
-
+
@@ -128,12 +128,12 @@
@@ -218,7 +218,7 @@
개인적으로 Octopress를 윈도우에서 사용하도록 구성하면서 도움이 되었던 팁들을 몇가지 정리해 보려고 합니다.
앞으로 계속 사용해 가면서 추가적인 팁이 생길 때에도 이 포스팅에 업데이트 할 생각이예요.
-
+
윈도우 실행 (Windows + R) 창에서 블로그 패스로 바로 이동 하기
@@ -256,6 +256,7 @@ 1
2
3
4
5
6
7
8
cd %blogpath%
rmdir /s /q _deploy
mkdir _deploy
cd _deploy
git init
git remote add origin https://....
git pull
git check --track origin/gh-pages
+
diff --git a/2014/08/19/yoda-notation/index.html b/2014/08/19/yoda-notation/index.html
index ac16d676..845362e4 100644
--- a/2014/08/19/yoda-notation/index.html
+++ b/2014/08/19/yoda-notation/index.html
@@ -3,7 +3,7 @@
-
+
@@ -125,12 +125,12 @@
@@ -220,7 +220,7 @@
이 책을 읽다가 ‘Yoda Notation’이란 표현을 처음 접했습니다. 표현이 재미있어서 블로그에 한 번 적어봅니다. 구글링해보면 Yoda Conditions 라고도 부르는 것 같네요. 프로그램 코드 상에서 조건문에 값 비교 구문을 적을 때 변수와 상수의 위치를 바꾸어 적는 것을 말합니다.
May the force be with you. 1
2
3
4
int val = 20;
if(20 == val) { // <- yoda notation here.
...
}
-
+
조건문을 val == 20
으로 적는 것이 일반적인 언어 어순과 같아서 읽기가 좋지만
프로그래머의 실수로 val = 20
과 같이 잘못된 코드가 만들어지고 컴파일 에러 없이 그대로 실행되는 것을 막기 위해서
일부러 변수와 상수의 위치를 서로 바꾸는 거죠.
요다는 영화 스타워즈에서 영문권 사람들도 이해하기 어려울 정도로 꼬인 문법의 말을 사용합니다. 이를 빗대어 위와 같은 조건문 표기 방식을 Yoda Notation이라고 부르는군요. 재미있는 네이밍입니다 :)
diff --git a/2014/09/12/claenup-cpp-project-1st/index.html b/2014/09/12/claenup-cpp-project-1st/index.html
index beb6816d..eefd426c 100644
--- a/2014/09/12/claenup-cpp-project-1st/index.html
+++ b/2014/09/12/claenup-cpp-project-1st/index.html
@@ -3,7 +3,7 @@
-
+
@@ -125,12 +125,12 @@
@@ -219,7 +219,7 @@ Whole Tomato의 Spaghetti 처럼 인클루드간의 관계를 그래프로 보여주는 툴은 몇 번 본 적 있다. 조낸 멋지게 그래프까지 보여주었지만 정작 불필요한 놈이 무언지 콕 짚어주는 녀석은 없음. 참으로 척박한 현실이다.
그래서 한 번 직접 만들어보기로 했다.
-
+
프로젝트 내의 cpp 파일을 개별 컴파일 하기
일단은 만들려는 툴에서, 입력으로 받은 vc 프로젝트에 포함된 cpp 파일을 개별로 컴파일 할 수 있어야 한다.
그렇게 되면 cpp 파일마다 돌면서 코드 안에 있는 #include를 직접 하나씩 제거해보면서 컴파일이 성공하는지를 확인할거다. 그러면 불필요할 것이라 예상되는 #include의 후보를 만들 수 있다.
무식한 방법이다. cpu를 많이 먹을거고 시간도 오래 걸릴거다. 하지만 저렇게라도 알 수 있다면 새벽에 실행해서 리포트 뽑아놓도록 CI에 물려놓으면 그만이다.
diff --git a/2014/09/17/cleanup-cpp-project-2nd/index.html b/2014/09/17/cleanup-cpp-project-2nd/index.html
index 2844494a..f02e445b 100644
--- a/2014/09/17/cleanup-cpp-project-2nd/index.html
+++ b/2014/09/17/cleanup-cpp-project-2nd/index.html
@@ -3,7 +3,7 @@
-
+
@@ -125,12 +125,12 @@
@@ -218,12 +218,14 @@
지워도 되는 인클루드를 찾아냈다
개별 파일 하나씩을 컴파일 할 수 있다면 이제 모든 인클루드를 하나씩 삭제하면서 컴파일 가능 여부를 확인해보면 된다. 이 부분은 간단한 file seeking과 string 처리 작업일 뿐이니 굳이 부연 설명은 필요 없다. 카페에서 여유롭게 음악을 들으며 즐겁게 툴을 만들자. 뚝딱뚝딱.
이정도 하고 나니 이제 vcxproj파일 경로를 주면 해당 프로젝트에 들어있는 소스코드에서 불필요한 인클루드를 색출해 위치정보를 출력해주는 물건이 만들어졌다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
작업 대상으로 1개의 프로젝트가 입력 되었습니다.
-------------------------------------------------
Service : 프로젝트 정리.
Service : PCH 생성.
컴파일 : stdafx.cpp ... 성공. 걸린 시간 : 1.04초
Client.cpp의 인클루드를 검사합니다.
- process #1 Client.cpp (1/2) ... X
- process #1 Client.cpp (2/2) ... X
ClientAcceptor.cpp의 인클루드를 검사합니다.
- process #1 ClientAcceptor.cpp (1/2) ... 컴파일 가능!
- process #1 ClientAcceptor.cpp (2/2) ... X
ClientConnection.cpp의 인클루드를 검사합니다.
- process #1 ClientConnection.cpp (1/3) ... X
- process #1 ClientConnection.cpp (2/3) ... X
- process #1 ClientConnection.cpp (3/3) ... X
Start.cpp의 인클루드를 검사합니다.
- process #1 Start.cpp (1/4) ... X
- process #1 Start.cpp (2/4) ... X
- process #1 Start.cpp (3/4) ... X
- process #1 Start.cpp (4/4) ... X
ThreadEntry.cpp의 인클루드를 검사합니다.
- process #1 ThreadEntry.cpp (1/1) ... X
-------------------------------------------------
Project : Service 모두 1개의 인클루드가 불필요한 것으로 의심됩니다.
D:\Dev\uni\World\Service\ClientAcceptor.cpp
- 2 line : #include "World/Service/Client.h"
총 소요 시간 : 13.289 sec
-
+
+
이 정도 만들어서 회사에서 만들고 있는 프로젝트에 조금 돌려 보았는데, 덕분에 꽤나 많은 불필요 인클루드를 색출해 내었다. 회사 프로젝트는 덩치가 제법 크고, 아직 서비스 중이지 않은 코드여서 용감무쌍한 리팩토링이 자주 일어나기 때문에 관리가 잘 안되는 파일이 제법 있더라. 아무튼 덕을 톡톡히 보았다.
튜닝 : 솔루션 단위로 검사할 수 있게 만들자
프로젝트 파일 단위로 어느 정도 돌아가니까, 솔루션 파일 단위로도 돌릴수 있게 확장했다. sln 파일을 파싱해서 프로젝트 리스트만 얻어오면 끝나는 일이다.
하지만 sln 파일은 vcxproj 파일처럼 쉽게 파싱할 수는 없다. 이녀석은 xml 포맷이 아니라, 자체적인 포맷을 가지고 있다. 사실 sln 파일을 파싱해 본 게 이번이 처음이 아닌데, 예전에는 lua를 써서 직접 노가다 파싱을 했더니 별로 재미도 없고 잘 돌아가지도 않고 코딩하는 재미도 별로 없더라.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 솔루션 파일은 이렇게 생겼다. 왜죠...
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2013
VisualStudioVersion = 12.0.30723.0
MinimumVisualStudioVersion = 10.0.40219.1
... 중략 ...
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "External", "External", "{F95C61E3-AF95-4CA9-8837-A203762B2B29}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "gtest", "External\gtest\gtest.vcxproj", "{C7A81BFC-6E28-4859-A8B5-2FEA80E012B2}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{042F2157-2118-44AA-8BB9-8B5DD01FA3A9}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "unittest", "Test\unittest.vcxproj", "{24A57754-D332-4575-AEBF-2AFCBC0A7E4B}"
EndProject
... 후략 ...
+
C#으로 sln 파일을 파싱해주는 적당히 괜찮은 코드가 인터넷 어딘가에 돌아다닌다. 이곳에 있는 놈을 가져다 붙였다. build configuration 같은 걸 얻어올 순 없지만 프로젝트 리스트 얻는 데에는 충분하다.
튜닝 : 느리다. 멀티 스레드로 돌리자
한때는 툴을 만들때 lua도 써보고 python도 써봤지만 요즘은 C#만 쓰게된다. 디버깅 하기도 편하고, 특히 멀티스레딩으로 돌리기가 너무 편하다. TPL, Concurrent Collection조금 갖다 끄적거리면 금방 병렬처리된다.
특히나 이런 식으로 병렬성이 좋은 툴은 훨씬 빠르게 돌릴 수 있게 된다. 커맨드 라인 인자로 --multi-thread
를 주면 주요 작업을 Parallel.ForEach
로 돌리도록 처리했다. 다만 멀티스레드로 돌리면 파일로 남기는 로그가 엉망이 되기 때문에… 단일 스레드로도 돌 수 있도록 남겨둠.
diff --git a/2014/09/30/cleanup-cpp-project-3rd/index.html b/2014/09/30/cleanup-cpp-project-3rd/index.html
index 8dd907d0..c8eaeb26 100644
--- a/2014/09/30/cleanup-cpp-project-3rd/index.html
+++ b/2014/09/30/cleanup-cpp-project-3rd/index.html
@@ -3,7 +3,7 @@
-
+
@@ -123,12 +123,12 @@
@@ -213,10 +213,11 @@
pch 파일 사이즈
팀에서 만지는 코드에서는, 290Mb에 육박하는 pch파일을 본 적이 있다(…) 그 땐 코드를 정리하면서 pch 사이즈 변화를 자주 확인해봐야 했는데, 탐색기나 커맨드 창에서 매번 사이즈를 조회하기가 불편했던 기억이 있어서 pch 사이즈 확인하는 걸 만들어봤다.
-
+
MSBuild로 단일 cpp 파일을 컴파일하면 이런 메시지가 나오는데,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin\amd64\CL.exe
/c
/ID:\Dev\uni\External\
/ID:\Dev\uni\Test\
/ID:\Dev\uni\
/Zi
/nologo
/W4
/WX
/sdl
/Od
/D WIN32
/D _DEBUG
/D _CONSOLE
/D _LIB
/D _UNICODE
/D UNICODE
/Gm
/EHsc
/RTC1
/MDd
/GS
/fp:precise
/Zc:wchar_t
/Zc:forScope
/Yc"stdafx.h"
/Fp"x64\Debug\unittest.pch"
/Fo"x64\Debug\\"
/Fd"x64\Debug\vc120.pdb"
/Gd
/TP
/errorReport:queue
stdafx.cpp
+
여기 cl.exe
로 들어가는 인자 중에 /Fp"x64\Debug\unittest.pch"
요 부분에 pch 경로가 있음. 그러니까 결국 툴에서 pch사이즈를 구하려면
- 프로젝트 리빌드하고
@@ -237,6 +238,7 @@ 팀에서 정한 컨벤션도 이 규칙을 그대로 따라야 해서.. 매번 코딩할 때마다 인클루드 순서에 신경쓰기 싫어서 자동화 처리를 작성. 더불어 경로 없이 파일명만 적은 경우나 상대경로를 사용한 인클루드도 지정된 path를 모두 적어주도록 컨버팅하는 처리도 만듦. 만드는 과정이야 대단한 건 없다. sln, vcxproj파일 파싱하는 것은 만들어 두었으니, 그냥 스트링 처리만 좀 더 해주면 금방 만들어진다. 툴로 sorting하고나면 아래처럼 만들어줌.
TestCode.cpp 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// system headers
// other project's headers
// inner project's headers
void main() {
return;
}
+
epilog
대충 이정도 돌아가는 툴을 만들어서 개인 pc에 셋팅해둔 jenkins에 물려놓고 사용중. 원래는 필요없는 include찾아주는 기능만 만들려다가 include sorting 기능은 그냥 한 번 추가나 해볼까 싶어 넣은건데, 아주 편하다. 코딩할 땐 순서 상관 없이 상대경로로 대충 넣어놓고 툴을 돌리면 컨벤션에 맞게 예쁘게 수정해준다.
불필요 인클루드를 찾는 동작은 회사 코드 기준으로 컨텐츠 코드 전체 검색시 50분 정도 걸리는 듯. 이건 매일 새벽에 jenkins가 한 번씩 돌려놓게 해놓고, 매일 아침에 출근해서 확인한다.
pch사이즈는 baseline 구축을 생각하고 만들어 본건데.. (박일, 사례로 살펴보는 디버깅 참고) baseline을 만들려면 지표들을 좀 더 모아야 하고, db도 붙여야 하니 이건 제대로 만들려면 시간이 필요할 것 같다(..라고 쓰고 ‘더이상 업데이트 되지 않는다’ 라고 읽는다.)
diff --git "a/2018/11/12/\355\205\214\355\201\254\353\213\210\354\273\254-\353\246\254\353\215\224\354\213\255-\354\213\234\354\236\221\355\225\230\352\270\260/index.html" "b/2018/11/12/\355\205\214\355\201\254\353\213\210\354\273\254-\353\246\254\353\215\224\354\213\255-\354\213\234\354\236\221\355\225\230\352\270\260/index.html"
index 54a92f88..7560bd45 100644
--- "a/2018/11/12/\355\205\214\355\201\254\353\213\210\354\273\254-\353\246\254\353\215\224\354\213\255-\354\213\234\354\236\221\355\225\230\352\270\260/index.html"
+++ "b/2018/11/12/\355\205\214\355\201\254\353\213\210\354\273\254-\353\246\254\353\215\224\354\213\255-\354\213\234\354\236\221\355\225\230\352\270\260/index.html"
@@ -3,7 +3,7 @@
-
+
@@ -127,12 +127,12 @@
@@ -219,7 +219,7 @@
예전에 트위터 하다가 읽었던 글인데, 개인적으로 마음에 들어서 부족하게나마 번역해 보았습니다.
원문은 슬랙 개발 블로그의 Technical Leadership: Getting Started라는 글입니다.
번역에 크게 자신이 없으니 부담이 없으신 분들은 원문을 보셔요.
-
+
테크니컬 리더십: 시작하기
내가 소프트웨어 엔지니어가 되기 전에는 이 직업에서 가장 중요한 점은 코딩이라고 생각했다. 그것은 잘못된 생각이었고, 소프트웨어 공학의 가장 중요한(그리고 가장 어려운)점은 다른 사람들과 원만하게 잘 협력하는 것이다.
나는 “관리자는 되지 않을거야!”라고 스스로에게 말해왔고, “그렇게 하면, 내 모든 에너지를 개발에만 집중시킬 수 있을거야!” 라고 생각했다. 내 이후의 경력도 기술 지향적인 실무자 위주로만 관리해 간다면 이 어려운 대인관계를 어느 정도 무시할 수 있을 거라고 생각했다.
diff --git "a/2020/12/26/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-FILE-LINE-\353\214\200\354\262\264\354\240\234/index.html" "b/2020/12/26/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-FILE-LINE-\353\214\200\354\262\264\354\240\234/index.html"
index 4b5e4da7..ba3c1335 100644
--- "a/2020/12/26/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-FILE-LINE-\353\214\200\354\262\264\354\240\234/index.html"
+++ "b/2020/12/26/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-FILE-LINE-\353\214\200\354\262\264\354\240\234/index.html"
@@ -3,7 +3,7 @@
-
+
@@ -129,12 +129,12 @@
@@ -220,10 +220,11 @@
C++에서 가장 기본적으로 사용했던 __FILE__, __LINE__, __FUNCTION__
등의 매크로와 유사한 효과를 내는 방법에 대해 적어본다. 이와 함께 나에게는 생소했던 string interning 개념에 대해서도 살짝 소개해본다. 자바 같은 managed 언어를 깊이 다뤄본 적이 없는 네이티브 개발자에게는 생소한 개념일 것이다.
UI가 없는 서버에서 동작의 내용을 확인하는 가장 기본적인 방법은 file로 남기는 log다. 정상 동작이나 오류상황에 대한 상세한 로그가 남아야 문제가 생겼을 때 파악하기가 쉽기 때문에, 간단한 동작이지만 아주 빈번하게 호출되는 부분이다. 로그 출력에서 성능을 많이 빼앗기지 않도록 기반을 다져놓으면 비즈니스 로직 구현을 위해 더 많은 H/W 리소스를 배분할 수 있다.
성능을 굳이 신경쓰지 않는다면 아래 있는 내용을 끝까지 모두 적용할 필요는 없다.
-
+
콜스택을 얻어와서 가장 마지막 함수를 찍는 방법
현재 스레드 컨텍스트에서의 StackFrame 정보를 얻어온 후, 프레임 데이터의 가장 마지막 부분을 읽어 호출자의 정보를 얻어낼 수 있다. C#으로 함수 호출 위치를 얻어올 때 가장 많이 쓰이는 방법이다. 가장 태초부터 있었던 방법이기 때문이다. 다음에 설명할 CompilerServices attribute는 .Net Framework 4.5부터 사용이 가능해졌기 때문에, 초창기 C#에서는 콜스택에서 읽어내는 방법 말고는 딱히 다른 선택지도 없었다.
1
2
3
4
5
6
7
8
9
StackTrace st = new StackTrace(new StackFrame(true));
Console.WriteLine(" Stack trace for current level: {0}", st.ToString());
StackFrame sf = st.GetFrame(0);
Console.WriteLine(" File: {0}", sf.GetFileName());
Console.WriteLine(" Method: {0}", sf.GetMethod().Name);
Console.WriteLine(" Line Number: {0}", sf.GetFileLineNumber());
Console.WriteLine(" Column Number: {0}", sf.GetFileColumnNumber());
+
C#에서 흔하게 사용하는 로깅 라이브러리인 Log4Net, NLog 등에서도 이 방법을 사용한다.
콜스택 기반 장점 : 가장 범용적이다. 프레임워크 호환성이 가장 좋음
.Net Framework의 태초부터 있었던 방식이므로 가장 범용적이다. 오래된 버전의 닷넷 프레임워크나 mono 프레임워크 등을 지원해야 하는 상황이라면 이 방법 말고는 마땅한 대안이 없다. 그래서 Log4Net, NLog 등의 유명한 라이브러리도 이 방법을 사용하고 있다. 이들은 불특정 다수의 환경에서 실행되어야 할 범용성이 중요한 모듈이기 때문이다.
콜스택 기반 단점 : 말해서 무엇하랴. 비용이 비싸고 느리다.
지금 회사에서 사용하는 게임서버 엔진은 처음에 Log4Net을 쓰다가, 나중에 NLog로 바꾸었다가, 현재는 자체 구현한 파일로그 모듈을 쓰고 있다. 외부 모듈로는 내가 만족하는 성능을 얻지 못했기 때문이다.
@@ -232,13 +233,16 @@ System.Runtime.CompilerServices
.NET Framework 4.5부터 새로운 방식으로 함수 호출자의 정보를 가져올 수 있게 되었다. 요즘 .NET 6에 대한 뉴스도 돌고 있는 현시점에서 보면 충분히 오래된 방식이다. 만들어야 하는 프로그램의 런타임을 특정 프레임워크만 사용하도록 한정할 수 있다면 이 방식을 사용하는 것을 추천한다. 게임서버는 런타임 환경을 단 하나의 프레임워크로 고정할 수 있으니, 크게 문제될 것이 없다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void DoProcessing()
{
TraceMessage("Something happened.");
}
public void TraceMessage(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
Trace.WriteLine("message: " + message);
Trace.WriteLine("member name: " + memberName);
Trace.WriteLine("source file path: " + sourceFilePath);
Trace.WriteLine("source line number: " + sourceLineNumber);
}
+
함수 인자에 기본값이 있기 때문에 작업자가 함수를 호출할 때 값을 전달하지는 않지만, 그래도 보이지 않게 뒤쪽 인자를 통해 호출자의 파일명, 라인수 등이 넘어가는 방식이다. 인자에 붙어있는 attribute로 인해 함수 호출 위치에 맞는 값들이 런타임에
채워진다.
과거의 오래된 프레임워크를 지원할 수 없다는 점이 거꾸로 단점이 될텐데, 사실 NLog같이 누구나 어디서나 사용해야할 로그모듈을 만들게 아니고, 게임서버처럼 특정 비즈니스 프로젝트로 사용처를 한정한다면 오래된 프레임워크 미지원은 그렇게 큰 단점은 아니다.
CompilerServices 장점 : 가볍고 빠르다.
위에서 언급했던 StackFramek 클래스를 사용하는 방식보다 훨씬 빠르다. C++의 __FILE__, __LINE__
은 매크로니까 이미 컴파일 타임에 문자열과 숫자로 치환되어 코드에 포함된다. CompilerServices 사용 방식은 런타임에 함수의 인자로 넘어가는 방식이니까 이것만큼 optimal할 수는 없지만, 콜스택을 긁어오는 것보다는 훨씬 빠르다.
CompilerService 단점 : 가변인자 인터페이스 사용이 불가능 해진다.
1
2
3
4
5
6
7
8
9
10
11
12
public void DoProcessing()
{
WriteLog("invalid value:{0}", value); // 불가능합니다.
}
public void WriteLog(string format,
params object[] list,
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
...
}
+
함수의 뒷부분 인자를 사용하게 되니까, 위와 같은 사용이 불가능하다. 예시처럼 formatting이 될 문자열을 처음에 받고 두번째부터 가변 인자를 받는 방법은 C++에서 로그 인터페이스를 만드는 가장 익숙한 방식이다.
하지만 C#은 나름대로의 해결법이 있다. 보간 문자열을 이용해 문자열을 포매팅하면 된다. .NET Framework 4.6 과 함께 C# 문법이 6.0으로 올라갔고 이 때부터 보간 문자열이 사용 가능해졌다. 최신의 C#에서는 String.Format보다 보간 문자열의 사용이 더 권장된다. - Effective C#, 빌 와그너. Chapter 1.4 string.Format()을 보간 문자열로 대체하라
1
2
3
4
5
6
7
8
9
10
11
12
public void DoProcessing()
{
// WriteLog("invalid value:{0}", value); // C++스러워 보이지만, 촌스러운 방식이예요.
WriteLog($"invalid value:{value}"); // 가능합니다. 권장됩니다. Effective C# 읽어보세요.
}
public void WriteLog(string message,
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
...
}
+
C#이 5.0이었을 시점만 해도 이건 큰 단점이었다. 하지만 현 시점에서 이것도 그리 문제될 것이 없다.
C++은 코드영역을 사용하지만, C#은 힙을 사용한다.
좀 더 성능에 집착해보자(?).
윗부분에서 잠시 언급했듯이, C++의 __FILE__, __LINE__
은 컴파일 시점에 이미 실제 값으로 변환을 완료하는 preprocessing 이다. 런타임에 함수 호출자 정보를 얻기 위해 추가로 들이는 비용이 거의 없다.
@@ -256,8 +260,10 @@
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;
using System.IO;
using System.Runtime.CompilerServices;
public static class Log
{
public static void Debug(string message, [CallerFilePath] string file = "", [CallerLineNumber] int line = 0)
{
// ... 중략...
provider.Debug($"{message} ({BuildTag(file, line)})");
}
private static string BuildTag(string file, int line)
{
return string.Intern($"{Path.GetFileName(file)}:{line.ToString()}");
}
}
+
전달받은 파일명을 바로 사용하지 않고 string.Intern()으로 한 번 감싸서 사용한다. 로그를 출력하면 아래처럼 찍힌다.
1
2
3
4
5
2020-12-21 12:08:02.144 [Debug] [ConnectionMonitor] add uid:1 #connection:1 (ConnectionMonitor.cs:32)
2020-12-21 12:08:02.145 [Info] [Send] [20017] kREGISTER_GAME_SERVER_REQ actionId:3 (SerializableExt.cs:92)
2020-12-21 12:08:02.205 [Info] db connection Initialized. type:Auth server:localhost count:16 (DbPool.cs:40)
2020-12-21 12:08:02.221 [Info] db connection Initialized. type:Contents server:localhost count:16 (DbPool.cs:40)
2020-12-21 12:08:02.238 [Info] db connection Initialized. type:Game server:localhost count:16 (DbPool.cs:40)
+
interning은 입구만 있고, 출구는 없는 string pool이다. 풀에 등록은 할 수 있지만 해제할 수는 없다. 한 번 쓰고 마는 동적인 문자열은 당연히 interning해서는 안된다. 반복적으로 사용하더라도 빈도가 낮아서, heap의 할당과 해제에 큰 압력을 주지 않는다면 이것도 굳이 interning할 필요는 없다. 이런 문자열들을 interning하면 장시간 떠있어야 하는 서버 프로그램의 경우 오히려 더 악영향을 끼칠 수 있다. 용도에 맞게 적절하게 적용해야 한다.
C#에서 코드에 함께 적혀있는 literl text들은 기본적으로 interning된다. C++처럼 code segment를 직접 가르키지는 않지만, 비슷한 효과를 내기 위함이다. 그 외에 프로그램이 사용하는 나머지 문자열에 대해서는 어떤 것을 interning할지 직접 판단하고 선별 적용해야 한다. 로그 메세지에 반복적으로 찍히는 소스코드 파일명은 interning하기에 적합한 대상이다.
마치면서
로그파일에서 로그 출력 위치를 남기는 방식에 관련해 성능 위주의 고려사항을 정리해 보았다.
diff --git "a/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/index.html" "b/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/index.html"
index 598ca338..a6a335c2 100644
--- "a/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/index.html"
+++ "b/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/index.html"
@@ -3,7 +3,7 @@
-
+
@@ -133,12 +133,12 @@
@@ -227,7 +227,7 @@
2018년에 네트워크 레이어 성능을 끌어올리기 위해 도입했던 System.IO.Pipeline을 간단히 소개하고, 도입 후기를 적어본다.
윈도우 OS에서 고성능을 내기 위한 소켓 프로그래밍을 할 때 IOCP 의 사용은 오래도록 변하지 않는 정답의 자리를 유지하고 있다. 여기에서 좀 더 성능에 욕심을 내고자 한다면 Windows Server 2012부터 등장한 Registerd IO 라는 새로운 선택지가 있다. 하지만 API가 C++ 로만 열려 있어서, C# 구현에서는 사용하기가 쉽지 않다.
하지만 C#에도 고성능 IO를 위한 새로운 API가 추가되었다. Pipeline 이다.
-
+
System.IO.Pipeline 소개.
pipeline을 처음 들었을 때는 IOCP의 뒤를 잇는 새로운 소켓 API인줄 알았다. C++의 RIO가 iocp를 완전히 대체할 수 있는 것처럼.
RIO는 가장 핵심 요소인 등록된 버퍼(registered buffer)
외에, IO 요청 및 완료 통지 방식도 함께 제공하기 때문에 iocp를 완전히 드러내고 대신 사용할 수 있다. 반면 Pipeline은 RIO보다는 커버하는 범위가 좁아서, IOCP를 완전히 대체하는 물건이 될 수는 없다. 이벤트 통지는 기존의 방법들을 이용하면서, 메모리 버퍼의 운용만을 담당하는 라이브러리 이기 때문에 IOCP와 반드시 함께 사용해야 한다.
@@ -249,6 +249,7 @@
1
2
3
4
5
6
7
8
9
// msdn 블로그에 소개된 코드 일부 발췌. Pipe를 하나 만들면 읽기/쓰기 Task를 2개 만든다.
async Task ProcessLinesAsync(Socket socket)
{
var pipe = new Pipe();
Task writing = FillPipeAsync(socket, pipe.Writer);
Task reading = ReadPipeAsync(pipe.Reader);
return Task.WhenAll(reading, writing);
}
+
원인은 Pipeline과 함께 사용하는 task (System.Threading.Tasks.Task) 들이었다. class Pipe
인스턴스 하나를 쓸 때마다 파이프라인에 ‘읽기’와 ‘쓰기’를 담당하는 class Task
객체 두 개를 사용하게 된다. 수신버퍼에만 Pipe를 달면 소켓의 2배, 송수신 버퍼에 모두 달면 소켓의 4배수 만큼의 task가 생성 되어야 하기 때문이다. 게임서버 프로세스당 5,000 명의 동접을 받는다고 하면 최대 20,000개의 task가 생성되고, 이 중 상당수는 waiting 상태로 IO 이벤트를 기다리게 된다.
task가 아무리 가볍다고 해도 네트워크 레이어에만 몇 만개의 task를 만드는 것은 그리 효율적이지 않다. TPL에 대한 이야기를 시작하면 해야 할 말이 아주 많기 때문에 별도의 포스팅으로 분리해야 할 것이다. 과감히 한 줄로 정리해보면, task는 상대적으로 OS의 커널오브젝트인 스레드보다 가볍다는 것이지 수천 수만개를 만들만큼 깃털같은 물건은 아닌 것이다.
스레드가 코드를 한 단계씩 수행하다가 아직 완료되지 않은 task를 await 하는 구문을 만나면 호출 스택을 한 단계씩 거꾸로 올라가면서 동기 로직의 수행을 재개한다. 하지만 완료되지 않은 task를 만났다고 해서 그 즉시 task의 완료 및 반환값 획득을 포기하고 호출스택을 거슬러 올라가는 것은 아니다. 혹시 금방 task가 완료되지 않을까 하는 기대감으로 조금 대기하다가 완료될 기미가 보이지 않으면 그 제서야 태세를 전환하게 된다. 이 전략은 task가 동시성을 매끄럽게 처리하기 위해서는 바람직한 모습이지만, 아주 많은 개수의 task를 장시간(게임서버에서 다음 패킷을 받을 때까지의 평균 시간) 동안 대기시켜야 하는 네트워크 모델에 사용하기에는 적합하지 않다. 스레드들은 각 pipeline의 write task가 RecvComplete 통지를 받고 깨어나기를 기다리면서 수십만 cpu clock을 낭비하게 된다.
@@ -272,6 +273,7 @@ <
패킷을 보낼때는 데이터 타입을 버퍼로 직렬화 한 후, 이 버퍼를 메모리 복사 없이 소켓에 그대로 연결해주기 위한 추가 처리가 있어야 하는데, 이건 송신 버퍼에만 필요한 동작이라서 클래스를 별도로 나누었다. 각 용도에 특화된 메서드가 추가 구현 되어있을 뿐 코어는 모두 비슷하다. 모두 단위 버퍼를 줄줄이 비엔나처럼 연결해 들고 있는 역할을 한다.
이들 중에 가장 기본이 되는 ZeroCopyBuffer 를 조금 보면 아래와 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
namespace Cs.ServerEngine.Network.Buffer
{
public sealed class ZeroCopyBuffer
{
private readonly Queue<LohSegment> segments = new Queue<LohSegment>();
private LohSegment last;
public int SegmentCount => this.segments.Count;
public int CalcTotalSize()
{
int result = 0;
foreach (var data in this.segments)
{
result += data.DataSize;
}
return result;
}
public BinaryWriter GetWriter() => new BinaryWriter(new ZeroCopyOutputStream(this));
public BinaryReader GetReader() => new BinaryReader(new ZeroCopyInputStream(this));
internal void Write(byte[] buffer, int offset, int count)
{
while (count > 0)
{
if (this.last == null || this.last.IsFull)
{
this.last = LohSegment.Create(LohPool.SegmentSize.Size4k);
this.segments.Enqueue(this.last);
}
int copied = this.last.AddData(buffer, offset, count);
offset += copied;
count -= copied;
}
}
internal LohSegment[] Move()
{
var result = this.segments.ToArray();
this.segments.Clear();
this.last = null;
return result;
}
internal LohSegment Peek()
{
return this.segments.Peek();
}
internal void PopHeadSegment()
{
var segment = this.segments.Dequeue();
segment.ToRecycleBin();
if (this.segments.Count == 0)
{
this.last = null;
}
}
}
}
+
본 주제와 관련한 인터페이스만 몇 개 간추려 보았다. Queue<LogSegment>
가 Pipeline 안에 있는 단위버퍼의 링크드 리스트 역할을 한다. Write()와 Move()는 메모리 복사 없이 데이터를 쓰는 인터페이스가 되고, Peek(), PopHeadSegment()는 데이터를 읽는 인터페이스가 되는데, internal 접근자니까 실제 사용계층에는 노출하지 않는다. Detail 하위의 *Stream 클래스를 위한 메서드들이다.
조각난 버퍼를 하나의 가상버퍼처럼 추상화해주는 로직은 *Stream들이 담고있다. System.IO.Stream을 상속했기 때문에 사용 계층에서는 보통의 파일스트림, 메모리 스트림을 다루던 방식과 똑같이 값을 읽고 쓰면 된다. 사용한 segment들을 새지 않게 잘 pooling하고, 버퍼 오프셋 계산할때 오차없이 더하기 빼기 잘해주는 코드가 전부인지라 굳이 옮겨붙이지는 않는다.
이렇게 하니 ZeroCopyBuffer
는 가상의 무한 버퍼 역할을 하고, 사용 계층에는 Stream 형식의 인터페이스를 제공하는 System.IO.Pipeline
의 유사품이 되었다. 제공되는 메서드 중에는 async method
가 하나도 없으니 cpu clock을 불필요하게 낭비할 일도 없다. 이렇게 디자인 하는것이 기존의 iocp 기반 소켓 구현에 익숙한 프로그래머에겐 더 친숙한 모델이면서, 성능상으로도 Pipeline보다 훨씬 낫고(tcp 기반 게임서버 한정), Unity3D처럼 최신의 Memory api가 지원 안되는 환경에서도 문제없이 사용할 수 있다.
diff --git "a/2021/01/01/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-Thread-Local-Storage/index.html" "b/2021/01/01/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-Thread-Local-Storage/index.html"
index a2bb2da9..aff0383e 100644
--- "a/2021/01/01/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-Thread-Local-Storage/index.html"
+++ "b/2021/01/01/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-Thread-Local-Storage/index.html"
@@ -3,7 +3,7 @@
-
+
@@ -131,12 +131,12 @@
@@ -223,7 +223,7 @@
프로그래밍에서 각 스레드별로 고유한 상태를 설정할 수 있는 공간을 Thread Local Storage (이하 TLS. transport layer security 아님) 라고 한다. VC++에서는 __declspec(thread)
키워드를 이용해서 tls 변수를 선언할 수 있다.
C#에도 ThreadLocal<T>
라는 클래스를 이용해 tls를 사용할 수 있지만, 막상 실제로 사용해보면 C++에서는 존재하지 않았던 큰 차이점이 있다. C# 5.0부터 들어온 async / await 문법을 이용해 비동기 프로그래밍을 구현했다면, await 대기 시점 이전과 이후에 스레드가 달라지기 때문이다.
이를 해결하는 방법과 주의해야 할 사항을 정리해본다.
-
+
알림 : 이 글을 처음 포스팅한 후 받은 피드백을 통해 보다 명확한 원인과 해결방법을 추가 확인하게 되어 내용을 수정/보완 했습니다. 최초 버전의 글도 유지하려 했으나 글의 문맥이 복잡해지고 읽기가 어려워져 최종 버전만 남겼습니다.
수정한 내용 요약 : 새로 깨어난 스레드인데도 AsyncLocal<T>
에 값이 남아있던 이유는, 기존의 값이 지워지지 않았기 때문이 아니라, 네트워크 이벤트 콜백으로 깨어난 스레드에도 AsyncLocal<T>
의 값을 복사하고 있었기 때문이었습니다.
@@ -238,6 +238,7 @@ ThreadLocal
우선 잠깐 언급했던 ThreadLocal<T>
클래스를 간단히 알아보자. 이를 이용해 일반적인 tls 변수를 선언하고 사용할 수 있다. 이보다 전부터 있었던 [ThreadStatic]
어트리뷰트로도 똑같이 tls를 선언할 수 있지만, 변수의 초기화 처리에서 ThreadLocal<T>
가 좀 더 매끄러운 처리를 지원한다. 일반적인 tls가 필요할 때는 좀 더 최신의 방식인 ThreadLocal<T>
를 사용하면 된다.
모든 tls 변수에 동일한 값의 복제본을 저장해 두려는 경우가 있다. 예를들어 스레드가 3개 있으면, 메모리 공간상에 각 스레드를 위한 변수 3개가 있고, 이들 모두에 같은 의미를 가지는 인스턴스를 하나씩 생성해 할당하는 경우를 말한다. 서로 다른 스레드끼리 공유해야 할 자원이 있을 때, 해당 자원에 lock이 없이 접근하고 싶다면 tls를 이용해 각 스레드마다 자원을 따로 만들어 각자 자기 리소스를 쓰게 하면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace Cs.Math
{
public static class RandomGenerator
{
public static int Next(int maxValue)
{
return PerThreadRandom.Instance.Next(maxValue);
}
// ... 중략
// 사용 계층에 노출할 인터페이스를 이곳에 정의. 사용자는 tls에 대해 알지 못한다.
// System.Random 객체는 멀티스레드 사용에 안전하지 않으므로 각 스레드마다 개별 생성.
private static class PerThreadRandom
{
private static readonly ThreadLocal<Random> Random = new ThreadLocal<Random>(() => new Random());
internal static Random Instance => Random.Value;
}
}
}
+
이런 경우는 비동기 메서드의 실행중 스레드의 교체가 발생하더라도 아무 문제가 되지 않는다. 어차피 어떤 스레드로 바뀌더라도 tls 변수가 하는 역할은 동일하기 때문이다. 0번 스레드가 불러다 쓰는 Random
객체가 어느순간 2번 스레드의 Random
객체로 바뀐다 해도 동작에 큰 영향이 없다.
AsyncLocal
문제는 스레드별로 tls의 상태가 서로 달라야 할 때 발생한다. 0번 스레드에는 tls에 “철수”가, 2번 스레드에는 “영희”가 적혀있어야 하고, 이를 사용해 스레드마다 다른 동작을 해야 하는 경우. 그런데 거기다 async/await를 이용한 비동기 프로그래밍을 함께 사용한 경우. 0번 철수 스레드가 코드 수행 도중 await 구문을 만나 task의 완료를 기다리고 있었지만, 대기가 풀렸을 때는 2번 스레드로 갈아타게 되면서 철수가 영희가 되버리는 경우다.
@@ -271,9 +272,11 @@ 원치 않는 AsyncLocal 복사는 꺼준다.
다행히 이 동작은 ExecutionContext.SuppressFlow / RestoreFlow 라는 메서드가 있어 쉽게 제어가 가능하다. 우선 스레드풀에 백그라운드 작업을 요청할 때는 SuppressFlow()
호출이 묶여있는 별도의 인터페이스를 만들고 이를 사용하게 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static class BackgroundJob
{
public static void Execute(Action action)
{
using var control = ExecutionContext.SuppressFlow();
ThreadPool.QueueUserWorkItem(_ => action());
}
}
public static class Program
{
public void Foo()
{
int a = 10;
int b = 20;
// 백그라운드 작업이 필요할 때. Wrapping한 인터페이스를 사용한다.
BackgroundJob.Execute(() =>
{
Console.WriteLine($"a + b = {a+b}");
});
}
}
+
작업 요청 후에는 RestoreFlow
를 불러 복구해주면 되는데, SuppressFlow
메서드가 IDisposable인 AsyncFlowControl 객체를 반환하니까 예시처럼 using을 쓰면 좀 더 심플하게 처리할 수 있다.
네트워크 구현부에도 수정이 필요하다. SocketAsyncEventArgs
객체를 사용해 비동기 요청을 수행하는 모든 곳에도 RestoreFlow
를 불러준다. (SocketAsyncEventArgs
는 win32의 OVERLAPPED
구조체를 거의 그대로 랩핑해둔 클래스다.) 예시로 하나만 옮겨보면 아래처럼 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class ConnectionBase
{
public void ConnectAsync(IPAddress ip, int port)
{
var args = new SocketAsyncEventArgs();
args.Completed += this.OnConnectCompleted; // 이 메서드가 새로운 스레드에서 불리게 될 것이다.
args.RemoteEndPoint = new IPEndPoint(ip, port);
using var control = ExecutionContext.SuppressFlow(); // 이걸 넣어주어야 콜백 스레드로 AsyncLocal을 복사하지 않는다.
if (!this.socket.ConnectAsync(args))
{
this.OnConnectCompleted(this.socket, args);
}
}
}
+
이런식으로 SendAsync, RecvAsync 등도 다 막아주어야 일반적인 iocp 콜백 사용 방식과 동일해진다. 다른 코드상에서 아무데도 AsyncLocal<T>
을 사용중이지 않다면 굳이 SuppressFlow 호출이 없어도 동작에는 문제가 없다. 그래도 어차피 사용하지도 않을 암묵적인 실행 컨텍스트간 연결 동작은 그냥 끊어두는 것이 성능상 조금이라도 이득일 듯한 기분이 든다.
정리
- C#의 비동기 메서드는 코드상으로는 매끈하게 이어져 있는듯 보이지만 실은 비동기 요청 지점을 전후로 분리 실행되며, 실행 스레드가 서로 다를 수도 있다.
diff --git "a/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/index.html" "b/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/index.html"
index 2f5cd397..64ba8ee7 100644
--- "a/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/index.html"
+++ "b/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/index.html"
@@ -3,7 +3,7 @@
-
+
@@ -134,12 +134,12 @@
@@ -227,7 +227,7 @@
이제 닷넷의 GC는 꽤나 쓸만하게 발전하여, 웬만한 경우는 프로그래머가 메모리 관리를 굳이 신경쓰지 않고 코딩할 수 있게 도와준다. 그리고 그것이 C++ 대신 C#을 선택하는 큰 이유이기도 하다. 하지만 C# 게임서버로도 성능에 욕심을 내고자 한다면, 짧은 순간 대량의 TPS를 낼 수 있는 네트워크 IO를 구현하려고 한다면 어느정도 메모리 운용에 대한 이해가 필요하다.
이번 포스팅에서는 네트워크 IO의 부하가 가중될 때 겪을 수 있는 메모리 단편화 현상에 대해서 정리해본다.
-
+
기본 용어 및 개념 정리
SOH / LOH / POH
가장 먼저 관리 힙(managed heap)
의 구분부터 이야기 해야한다. 관리힙은 사용 메모리의 크기와 용도 등에 따라 SOH
, LOH
, POH
로 나뉜다.
@@ -273,8 +273,10 @@ C# 고성능 서버 - System.IO.Pipeline 도입 후기에서 여러개의 단위버퍼를 이어붙여 가상의 스트림처럼 운용하는 ZeroCopyBuffer
의 구현에 대해 간단히 소개했었다. 이 때 등장했던 단위버퍼 LohSegment
클래스가 바로 LOH
에 할당한 커다란 청크의 일부분에 해당한다.
1
2
3
4
5
6
7
8
namespace Cs.ServerEngine.Netork.Buffer
{
public sealed class ZeroCopyBuffer
{
private readonly Queue<LohSegment> segments = new Queue<LohSegment>();
private LohSegment last;
// ^ 여기 얘네들이예요.
...
+
LohSegment
를 생성, 풀링하고 관리하는 구현은 크게 대단할 것은 없다. 어차피 할당 크기가 85kb보다 크기만 하면 알아서 LOH
에 할당될 것이고.. 청크를 다시 잘 쪼개서 ConcurrentQueue<>
에 넣어뒀다가 잘 빌려주고 반납하고 관리만 해주면 된다.
조금 더 신경을 쓴다면 서비스 도중 메모리 청크를 추가할당 할 때의 처리 정도가 있겠다. Pool에 남아있는 버퍼의 개수가 좀 모자란다 싶을 때는 CAS 연산으로 소유권을 선점한 스레드 하나만 청크를 할당하게 만든다. 메모리는 추가만 할 뿐 해제는 하지 않을거니까 이렇게 하면 lock을 안 걸어도 되고, pool의 사용도 중단되지 않게 만들 수 있다. 해당 구현체의 멤버변수들만 붙여보면 아래와 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace Cs.Memory
{
public sealed class LohPool
{
private const int ChunkSizeMb = 100;
private const int LowSegmentNumberLimit = 1000;
private readonly int segmentSizeKb;
private readonly int segmentSizeBytes;
private readonly List<byte[]> chunks = new List<byte[]>(capacity: 10);
private readonly ConcurrentQueue<ArraySegment<byte>> segments = new ConcurrentQueue<ArraySegment<byte>>();
private readonly AtomicFlag producerLock = new AtomicFlag(false);
private int totalSegmentCount;
...
}
}
+
정리
C++로만 만들던 게임서버를 C#으로 만든다고 했을 때 가장 신경쓰였던 것이 메모리 부분이었다. 초기구현과 서비스를 거치면서 메모리 누수, 관리힙 사이즈 증가등 많은 메모리 문제를 겪었다. 그 중에서 가장 크게 문제를 겪었던 단편화에 대해 정리해 보았다.
우리가 겪었던 메모리 단편화 가장 주된 요인은 네트워크 IO용 바이트 버퍼의 pinning 때문이었다. 적당한 수준의 부하로는 별 문제 없는데.. 부하를 세게 걸면 점유 메모리가 계속 증가하고 가라않질 않았다. 이건 C++도 마찬가지지만 외형적으로만 관측하면 메모리 누수처럼 보이기 때문에, 단편화가 원인일 것이라는 의심을 하기까지도 많은 검증의 시간이 필요했다.
SOH
에서는 pinning되는 메모리가 많으면 GC 능력이 많이 저하되고 단편화가 심각해진다. 네트워크 버퍼로 사용할 객체들을 LOH
에 할당하면 이런 문제를 해결할 수 있다.
참고자료
@@ -313,6 +315,9 @@
+
+ iTerm2 없이 맥 기본 터미널 꾸미기
+
diff --git "a/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/cursor_setting.png" "b/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/cursor_setting.png"
new file mode 100644
index 00000000..d8a2af00
Binary files /dev/null and "b/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/cursor_setting.png" differ
diff --git "a/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/index.html" "b/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/index.html"
new file mode 100644
index 00000000..01b15ba5
--- /dev/null
+++ "b/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/index.html"
@@ -0,0 +1,348 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+iTerm2 없이 맥 기본 터미널 꾸미기 | leafbird/devnote
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git "a/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/mac_terminal.png" "b/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/mac_terminal.png"
new file mode 100644
index 00000000..d9bd7687
Binary files /dev/null and "b/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/mac_terminal.png" differ
diff --git "a/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/vscode.png" "b/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/vscode.png"
new file mode 100644
index 00000000..d49dd3ba
Binary files /dev/null and "b/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/vscode.png" differ
diff --git "a/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/vscode_setting.png" "b/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/vscode_setting.png"
new file mode 100644
index 00000000..2cae4238
Binary files /dev/null and "b/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/vscode_setting.png" differ
diff --git "a/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/before_after.png" "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/before_after.png"
new file mode 100644
index 00000000..a86e5f81
Binary files /dev/null and "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/before_after.png" differ
diff --git "a/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/final_desktop.png" "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/final_desktop.png"
new file mode 100644
index 00000000..4493ab29
Binary files /dev/null and "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/final_desktop.png" differ
diff --git "a/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/final_ios.png" "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/final_ios.png"
new file mode 100644
index 00000000..2feec5dc
Binary files /dev/null and "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/final_ios.png" differ
diff --git "a/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/grid_table.png" "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/grid_table.png"
new file mode 100644
index 00000000..4f0d9e2c
Binary files /dev/null and "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/grid_table.png" differ
diff --git "a/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/index.html" "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/index.html"
new file mode 100644
index 00000000..edcb095f
--- /dev/null
+++ "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/index.html"
@@ -0,0 +1,354 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+웹페이지 반응형 디자인하기 | leafbird/devnote
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git "a/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/wiki_image.png" "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/wiki_image.png"
new file mode 100644
index 00000000..4d6bd5ba
Binary files /dev/null and "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/wiki_image.png" differ
diff --git "a/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/\354\212\244\355\201\254\353\246\260\354\203\267 2023-10-07 \354\230\244\355\233\204 10.37.06.png" "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/\354\212\244\355\201\254\353\246\260\354\203\267 2023-10-07 \354\230\244\355\233\204 10.37.06.png"
new file mode 100644
index 00000000..ce9571e0
Binary files /dev/null and "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/\354\212\244\355\201\254\353\246\260\354\203\267 2023-10-07 \354\230\244\355\233\204 10.37.06.png" differ
diff --git "a/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/\354\212\244\355\201\254\353\246\260\354\203\267 2023-10-07 \354\230\244\355\233\204 10.37.27.png" "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/\354\212\244\355\201\254\353\246\260\354\203\267 2023-10-07 \354\230\244\355\233\204 10.37.27.png"
new file mode 100644
index 00000000..16e76765
Binary files /dev/null and "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/\354\212\244\355\201\254\353\246\260\354\203\267 2023-10-07 \354\230\244\355\233\204 10.37.27.png" differ
diff --git a/archives/2013/12/index.html b/archives/2013/12/index.html
index df229407..1f88f886 100644
--- a/archives/2013/12/index.html
+++ b/archives/2013/12/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,7 +155,7 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
diff --git a/archives/2013/index.html b/archives/2013/index.html
index b9c2634d..ab7db49d 100644
--- a/archives/2013/index.html
+++ b/archives/2013/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,7 +155,7 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
diff --git a/archives/2014/07/index.html b/archives/2014/07/index.html
index 82047c3a..9f510b00 100644
--- a/archives/2014/07/index.html
+++ b/archives/2014/07/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,7 +155,7 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
diff --git a/archives/2014/08/index.html b/archives/2014/08/index.html
index f3eb4402..f902bb4c 100644
--- a/archives/2014/08/index.html
+++ b/archives/2014/08/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,7 +155,7 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
diff --git a/archives/2014/09/index.html b/archives/2014/09/index.html
index 46426032..8ead37bd 100644
--- a/archives/2014/09/index.html
+++ b/archives/2014/09/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,7 +155,7 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
diff --git a/archives/2014/index.html b/archives/2014/index.html
index 2b87394d..8f904b0b 100644
--- a/archives/2014/index.html
+++ b/archives/2014/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,7 +155,7 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
diff --git a/archives/2018/11/index.html b/archives/2018/11/index.html
index 1afeffcc..8c85e978 100644
--- a/archives/2018/11/index.html
+++ b/archives/2018/11/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,7 +155,7 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
diff --git a/archives/2018/index.html b/archives/2018/index.html
index 524b00c0..e689367a 100644
--- a/archives/2018/index.html
+++ b/archives/2018/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,7 +155,7 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
diff --git a/archives/2020/12/index.html b/archives/2020/12/index.html
index 5e240bcb..acb8cf3d 100644
--- a/archives/2020/12/index.html
+++ b/archives/2020/12/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,7 +155,7 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
diff --git a/archives/2020/index.html b/archives/2020/index.html
index d2918d0a..219a8d9c 100644
--- a/archives/2020/index.html
+++ b/archives/2020/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,7 +155,7 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
diff --git a/archives/2021/01/index.html b/archives/2021/01/index.html
index 8f1d5d3b..42b0b6ea 100644
--- a/archives/2021/01/index.html
+++ b/archives/2021/01/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,7 +155,7 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
diff --git a/archives/2021/08/index.html b/archives/2021/08/index.html
index edb01a7e..6bc89e0d 100644
--- a/archives/2021/08/index.html
+++ b/archives/2021/08/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,7 +155,7 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
diff --git a/archives/2021/index.html b/archives/2021/index.html
index 91580aef..40450eb0 100644
--- a/archives/2021/index.html
+++ b/archives/2021/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,7 +155,7 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
diff --git a/archives/2023/10/index.html b/archives/2023/10/index.html
new file mode 100644
index 00000000..81e57bce
--- /dev/null
+++ b/archives/2023/10/index.html
@@ -0,0 +1,272 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+아카이브 | leafbird/devnote
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
+
+
+
+
+ 2023
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/archives/2023/index.html b/archives/2023/index.html
new file mode 100644
index 00000000..2a97b3c9
--- /dev/null
+++ b/archives/2023/index.html
@@ -0,0 +1,272 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+아카이브 | leafbird/devnote
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
+
+
+
+
+ 2023
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/archives/index.html b/archives/index.html
index 9525f08d..0a4a70d2 100644
--- a/archives/index.html
+++ b/archives/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,10 +155,53 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
+
+ 2023
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
2021
@@ -332,46 +375,6 @@ leafbird/devnote
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/archives/page/2/index.html b/archives/page/2/index.html
index fdbc7656..85b96737 100644
--- a/archives/page/2/index.html
+++ b/archives/page/2/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,7 +155,7 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
@@ -163,6 +163,46 @@ leafbird/devnote
2014
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
지금 참여중인 프로젝트에서 얼마전에 코딩 컨벤션을 통일하는 작업이 있었습니다.
본격적으로 컨벤션을 통일하고 이제 한 서너달? 정도 지난 것 같네요.
처음에는 팀원 대다수가 많이 혼란스러워 했지만 이제 어느 정도 시간이 지나고 나니 팀 내 프로그래머 모두가 거의 유사한 스타일의 코드를 작성하게 됐습니다. 이렇게 되니 전보다 코드 가독성이 좋아지고 협업을 할 때 이런 저런 많은 도움이 됩니다.
- +사실 컨벤션이 통일되면 좋다는 것은 아주 상식적인 말입니다만, 개개인이 선호하는 스타일이 다 다르기 때문에 통일을 하기가 쉽지 않다는 것이 문제입니다. 팀에서도 그동안 몇 차례 시도 했었지만 잘 안되었다가, 이번에서야 겨우 성공했어요.
이번에 컨벤션의 통일을 성공한 주된 요인 중의 하나는 구글 내부에서 사용하는 컨벤션을 정리해서 공개한 구글 C++ 스타일 가이드라고 볼 수 있습니다. 이 문서의 내용을 가져와 몇 가지 사항만 프로젝트에 맞게 조정하여 적용 하였지요. 구글 컨벤션의 코드들은 처음 볼 땐 좀 낮설었지만 적응하고 나니 이젠 괜찮군요.
diff --git a/2014/07/21/octopress-tips-on-windows/index.html b/2014/07/21/octopress-tips-on-windows/index.html index b369ca3c..81633968 100644 --- a/2014/07/21/octopress-tips-on-windows/index.html +++ b/2014/07/21/octopress-tips-on-windows/index.html @@ -3,7 +3,7 @@ - + @@ -128,12 +128,12 @@ @@ -218,7 +218,7 @@
개인적으로 Octopress를 윈도우에서 사용하도록 구성하면서 도움이 되었던 팁들을 몇가지 정리해 보려고 합니다.
앞으로 계속 사용해 가면서 추가적인 팁이 생길 때에도 이 포스팅에 업데이트 할 생각이예요.
-
+
윈도우 실행 (Windows + R) 창에서 블로그 패스로 바로 이동 하기
@@ -256,6 +256,7 @@ 1
2
3
4
5
6
7
8
cd %blogpath%
rmdir /s /q _deploy
mkdir _deploy
cd _deploy
git init
git remote add origin https://....
git pull
git check --track origin/gh-pages
+
diff --git a/2014/08/19/yoda-notation/index.html b/2014/08/19/yoda-notation/index.html
index ac16d676..845362e4 100644
--- a/2014/08/19/yoda-notation/index.html
+++ b/2014/08/19/yoda-notation/index.html
@@ -3,7 +3,7 @@
-
+
@@ -125,12 +125,12 @@
@@ -220,7 +220,7 @@
개인적으로 Octopress를 윈도우에서 사용하도록 구성하면서 도움이 되었던 팁들을 몇가지 정리해 보려고 합니다.
앞으로 계속 사용해 가면서 추가적인 팁이 생길 때에도 이 포스팅에 업데이트 할 생각이예요.
윈도우 실행 (Windows + R) 창에서 블로그 패스로 바로 이동 하기
@@ -256,6 +256,7 @@1 | cd %blogpath% |
이 책을 읽다가 ‘Yoda Notation’이란 표현을 처음 접했습니다. 표현이 재미있어서 블로그에 한 번 적어봅니다. 구글링해보면 Yoda Conditions 라고도 부르는 것 같네요. 프로그램 코드 상에서 조건문에 값 비교 구문을 적을 때 변수와 상수의 위치를 바꾸어 적는 것을 말합니다.
1 | int val = 20; |
조건문을 val == 20
으로 적는 것이 일반적인 언어 어순과 같아서 읽기가 좋지만
프로그래머의 실수로 val = 20
과 같이 잘못된 코드가 만들어지고 컴파일 에러 없이 그대로 실행되는 것을 막기 위해서
일부러 변수와 상수의 위치를 서로 바꾸는 거죠.
요다는 영화 스타워즈에서 영문권 사람들도 이해하기 어려울 정도로 꼬인 문법의 말을 사용합니다. 이를 빗대어 위와 같은 조건문 표기 방식을 Yoda Notation이라고 부르는군요. 재미있는 네이밍입니다 :)
diff --git a/2014/09/12/claenup-cpp-project-1st/index.html b/2014/09/12/claenup-cpp-project-1st/index.html index beb6816d..eefd426c 100644 --- a/2014/09/12/claenup-cpp-project-1st/index.html +++ b/2014/09/12/claenup-cpp-project-1st/index.html @@ -3,7 +3,7 @@ - + @@ -125,12 +125,12 @@ @@ -219,7 +219,7 @@Whole Tomato의 Spaghetti 처럼 인클루드간의 관계를 그래프로 보여주는 툴은 몇 번 본 적 있다. 조낸 멋지게 그래프까지 보여주었지만 정작 불필요한 놈이 무언지 콕 짚어주는 녀석은 없음. 참으로 척박한 현실이다.
그래서 한 번 직접 만들어보기로 했다.
- +프로젝트 내의 cpp 파일을 개별 컴파일 하기
일단은 만들려는 툴에서, 입력으로 받은 vc 프로젝트에 포함된 cpp 파일을 개별로 컴파일 할 수 있어야 한다.
그렇게 되면 cpp 파일마다 돌면서 코드 안에 있는 #include를 직접 하나씩 제거해보면서 컴파일이 성공하는지를 확인할거다. 그러면 불필요할 것이라 예상되는 #include의 후보를 만들 수 있다.
무식한 방법이다. cpu를 많이 먹을거고 시간도 오래 걸릴거다. 하지만 저렇게라도 알 수 있다면 새벽에 실행해서 리포트 뽑아놓도록 CI에 물려놓으면 그만이다.
diff --git a/2014/09/17/cleanup-cpp-project-2nd/index.html b/2014/09/17/cleanup-cpp-project-2nd/index.html index 2844494a..f02e445b 100644 --- a/2014/09/17/cleanup-cpp-project-2nd/index.html +++ b/2014/09/17/cleanup-cpp-project-2nd/index.html @@ -3,7 +3,7 @@ - + @@ -125,12 +125,12 @@ @@ -218,12 +218,14 @@
지워도 되는 인클루드를 찾아냈다
개별 파일 하나씩을 컴파일 할 수 있다면 이제 모든 인클루드를 하나씩 삭제하면서 컴파일 가능 여부를 확인해보면 된다. 이 부분은 간단한 file seeking과 string 처리 작업일 뿐이니 굳이 부연 설명은 필요 없다. 카페에서 여유롭게 음악을 들으며 즐겁게 툴을 만들자. 뚝딱뚝딱.
이정도 하고 나니 이제 vcxproj파일 경로를 주면 해당 프로젝트에 들어있는 소스코드에서 불필요한 인클루드를 색출해 위치정보를 출력해주는 물건이 만들어졌다.
1 | 작업 대상으로 1개의 프로젝트가 입력 되었습니다. |
이 정도 만들어서 회사에서 만들고 있는 프로젝트에 조금 돌려 보았는데, 덕분에 꽤나 많은 불필요 인클루드를 색출해 내었다. 회사 프로젝트는 덩치가 제법 크고, 아직 서비스 중이지 않은 코드여서 용감무쌍한 리팩토링이 자주 일어나기 때문에 관리가 잘 안되는 파일이 제법 있더라. 아무튼 덕을 톡톡히 보았다.
튜닝 : 솔루션 단위로 검사할 수 있게 만들자
프로젝트 파일 단위로 어느 정도 돌아가니까, 솔루션 파일 단위로도 돌릴수 있게 확장했다. sln 파일을 파싱해서 프로젝트 리스트만 얻어오면 끝나는 일이다.
하지만 sln 파일은 vcxproj 파일처럼 쉽게 파싱할 수는 없다. 이녀석은 xml 포맷이 아니라, 자체적인 포맷을 가지고 있다. 사실 sln 파일을 파싱해 본 게 이번이 처음이 아닌데, 예전에는 lua를 써서 직접 노가다 파싱을 했더니 별로 재미도 없고 잘 돌아가지도 않고 코딩하는 재미도 별로 없더라.
1 | // 솔루션 파일은 이렇게 생겼다. 왜죠... |
C#으로 sln 파일을 파싱해주는 적당히 괜찮은 코드가 인터넷 어딘가에 돌아다닌다. 이곳에 있는 놈을 가져다 붙였다. build configuration 같은 걸 얻어올 순 없지만 프로젝트 리스트 얻는 데에는 충분하다.
튜닝 : 느리다. 멀티 스레드로 돌리자
한때는 툴을 만들때 lua도 써보고 python도 써봤지만 요즘은 C#만 쓰게된다. 디버깅 하기도 편하고, 특히 멀티스레딩으로 돌리기가 너무 편하다. TPL, Concurrent Collection조금 갖다 끄적거리면 금방 병렬처리된다.
특히나 이런 식으로 병렬성이 좋은 툴은 훨씬 빠르게 돌릴 수 있게 된다. 커맨드 라인 인자로 --multi-thread
를 주면 주요 작업을 Parallel.ForEach
로 돌리도록 처리했다. 다만 멀티스레드로 돌리면 파일로 남기는 로그가 엉망이 되기 때문에… 단일 스레드로도 돌 수 있도록 남겨둠.
pch 파일 사이즈
팀에서 만지는 코드에서는, 290Mb에 육박하는 pch파일을 본 적이 있다(…) 그 땐 코드를 정리하면서 pch 사이즈 변화를 자주 확인해봐야 했는데, 탐색기나 커맨드 창에서 매번 사이즈를 조회하기가 불편했던 기억이 있어서 pch 사이즈 확인하는 걸 만들어봤다.
-
+
MSBuild로 단일 cpp 파일을 컴파일하면 이런 메시지가 나오는데,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin\amd64\CL.exe
/c
/ID:\Dev\uni\External\
/ID:\Dev\uni\Test\
/ID:\Dev\uni\
/Zi
/nologo
/W4
/WX
/sdl
/Od
/D WIN32
/D _DEBUG
/D _CONSOLE
/D _LIB
/D _UNICODE
/D UNICODE
/Gm
/EHsc
/RTC1
/MDd
/GS
/fp:precise
/Zc:wchar_t
/Zc:forScope
/Yc"stdafx.h"
/Fp"x64\Debug\unittest.pch"
/Fo"x64\Debug\\"
/Fd"x64\Debug\vc120.pdb"
/Gd
/TP
/errorReport:queue
stdafx.cpp
+
여기 cl.exe
로 들어가는 인자 중에 /Fp"x64\Debug\unittest.pch"
요 부분에 pch 경로가 있음. 그러니까 결국 툴에서 pch사이즈를 구하려면
- 프로젝트 리빌드하고
@@ -237,6 +238,7 @@ 팀에서 정한 컨벤션도 이 규칙을 그대로 따라야 해서.. 매번 코딩할 때마다 인클루드 순서에 신경쓰기 싫어서 자동화 처리를 작성. 더불어 경로 없이 파일명만 적은 경우나 상대경로를 사용한 인클루드도 지정된 path를 모두 적어주도록 컨버팅하는 처리도 만듦. 만드는 과정이야 대단한 건 없다. sln, vcxproj파일 파싱하는 것은 만들어 두었으니, 그냥 스트링 처리만 좀 더 해주면 금방 만들어진다. 툴로 sorting하고나면 아래처럼 만들어줌.
TestCode.cpp 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// system headers
// other project's headers
// inner project's headers
void main() {
return;
}
+
epilog
대충 이정도 돌아가는 툴을 만들어서 개인 pc에 셋팅해둔 jenkins에 물려놓고 사용중. 원래는 필요없는 include찾아주는 기능만 만들려다가 include sorting 기능은 그냥 한 번 추가나 해볼까 싶어 넣은건데, 아주 편하다. 코딩할 땐 순서 상관 없이 상대경로로 대충 넣어놓고 툴을 돌리면 컨벤션에 맞게 예쁘게 수정해준다.
불필요 인클루드를 찾는 동작은 회사 코드 기준으로 컨텐츠 코드 전체 검색시 50분 정도 걸리는 듯. 이건 매일 새벽에 jenkins가 한 번씩 돌려놓게 해놓고, 매일 아침에 출근해서 확인한다.
pch사이즈는 baseline 구축을 생각하고 만들어 본건데.. (박일, 사례로 살펴보는 디버깅 참고) baseline을 만들려면 지표들을 좀 더 모아야 하고, db도 붙여야 하니 이건 제대로 만들려면 시간이 필요할 것 같다(..라고 쓰고 ‘더이상 업데이트 되지 않는다’ 라고 읽는다.)
diff --git "a/2018/11/12/\355\205\214\355\201\254\353\213\210\354\273\254-\353\246\254\353\215\224\354\213\255-\354\213\234\354\236\221\355\225\230\352\270\260/index.html" "b/2018/11/12/\355\205\214\355\201\254\353\213\210\354\273\254-\353\246\254\353\215\224\354\213\255-\354\213\234\354\236\221\355\225\230\352\270\260/index.html"
index 54a92f88..7560bd45 100644
--- "a/2018/11/12/\355\205\214\355\201\254\353\213\210\354\273\254-\353\246\254\353\215\224\354\213\255-\354\213\234\354\236\221\355\225\230\352\270\260/index.html"
+++ "b/2018/11/12/\355\205\214\355\201\254\353\213\210\354\273\254-\353\246\254\353\215\224\354\213\255-\354\213\234\354\236\221\355\225\230\352\270\260/index.html"
@@ -3,7 +3,7 @@
-
+
@@ -127,12 +127,12 @@
@@ -219,7 +219,7 @@
예전에 트위터 하다가 읽었던 글인데, 개인적으로 마음에 들어서 부족하게나마 번역해 보았습니다.
원문은 슬랙 개발 블로그의 Technical Leadership: Getting Started라는 글입니다.
번역에 크게 자신이 없으니 부담이 없으신 분들은 원문을 보셔요.
-
+
테크니컬 리더십: 시작하기
내가 소프트웨어 엔지니어가 되기 전에는 이 직업에서 가장 중요한 점은 코딩이라고 생각했다. 그것은 잘못된 생각이었고, 소프트웨어 공학의 가장 중요한(그리고 가장 어려운)점은 다른 사람들과 원만하게 잘 협력하는 것이다.
나는 “관리자는 되지 않을거야!”라고 스스로에게 말해왔고, “그렇게 하면, 내 모든 에너지를 개발에만 집중시킬 수 있을거야!” 라고 생각했다. 내 이후의 경력도 기술 지향적인 실무자 위주로만 관리해 간다면 이 어려운 대인관계를 어느 정도 무시할 수 있을 거라고 생각했다.
diff --git "a/2020/12/26/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-FILE-LINE-\353\214\200\354\262\264\354\240\234/index.html" "b/2020/12/26/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-FILE-LINE-\353\214\200\354\262\264\354\240\234/index.html"
index 4b5e4da7..ba3c1335 100644
--- "a/2020/12/26/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-FILE-LINE-\353\214\200\354\262\264\354\240\234/index.html"
+++ "b/2020/12/26/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-FILE-LINE-\353\214\200\354\262\264\354\240\234/index.html"
@@ -3,7 +3,7 @@
-
+
@@ -129,12 +129,12 @@
@@ -220,10 +220,11 @@
C++에서 가장 기본적으로 사용했던 __FILE__, __LINE__, __FUNCTION__
등의 매크로와 유사한 효과를 내는 방법에 대해 적어본다. 이와 함께 나에게는 생소했던 string interning 개념에 대해서도 살짝 소개해본다. 자바 같은 managed 언어를 깊이 다뤄본 적이 없는 네이티브 개발자에게는 생소한 개념일 것이다.
UI가 없는 서버에서 동작의 내용을 확인하는 가장 기본적인 방법은 file로 남기는 log다. 정상 동작이나 오류상황에 대한 상세한 로그가 남아야 문제가 생겼을 때 파악하기가 쉽기 때문에, 간단한 동작이지만 아주 빈번하게 호출되는 부분이다. 로그 출력에서 성능을 많이 빼앗기지 않도록 기반을 다져놓으면 비즈니스 로직 구현을 위해 더 많은 H/W 리소스를 배분할 수 있다.
성능을 굳이 신경쓰지 않는다면 아래 있는 내용을 끝까지 모두 적용할 필요는 없다.
-
+
콜스택을 얻어와서 가장 마지막 함수를 찍는 방법
현재 스레드 컨텍스트에서의 StackFrame 정보를 얻어온 후, 프레임 데이터의 가장 마지막 부분을 읽어 호출자의 정보를 얻어낼 수 있다. C#으로 함수 호출 위치를 얻어올 때 가장 많이 쓰이는 방법이다. 가장 태초부터 있었던 방법이기 때문이다. 다음에 설명할 CompilerServices attribute는 .Net Framework 4.5부터 사용이 가능해졌기 때문에, 초창기 C#에서는 콜스택에서 읽어내는 방법 말고는 딱히 다른 선택지도 없었다.
1
2
3
4
5
6
7
8
9
StackTrace st = new StackTrace(new StackFrame(true));
Console.WriteLine(" Stack trace for current level: {0}", st.ToString());
StackFrame sf = st.GetFrame(0);
Console.WriteLine(" File: {0}", sf.GetFileName());
Console.WriteLine(" Method: {0}", sf.GetMethod().Name);
Console.WriteLine(" Line Number: {0}", sf.GetFileLineNumber());
Console.WriteLine(" Column Number: {0}", sf.GetFileColumnNumber());
+
C#에서 흔하게 사용하는 로깅 라이브러리인 Log4Net, NLog 등에서도 이 방법을 사용한다.
콜스택 기반 장점 : 가장 범용적이다. 프레임워크 호환성이 가장 좋음
.Net Framework의 태초부터 있었던 방식이므로 가장 범용적이다. 오래된 버전의 닷넷 프레임워크나 mono 프레임워크 등을 지원해야 하는 상황이라면 이 방법 말고는 마땅한 대안이 없다. 그래서 Log4Net, NLog 등의 유명한 라이브러리도 이 방법을 사용하고 있다. 이들은 불특정 다수의 환경에서 실행되어야 할 범용성이 중요한 모듈이기 때문이다.
콜스택 기반 단점 : 말해서 무엇하랴. 비용이 비싸고 느리다.
지금 회사에서 사용하는 게임서버 엔진은 처음에 Log4Net을 쓰다가, 나중에 NLog로 바꾸었다가, 현재는 자체 구현한 파일로그 모듈을 쓰고 있다. 외부 모듈로는 내가 만족하는 성능을 얻지 못했기 때문이다.
@@ -232,13 +233,16 @@ System.Runtime.CompilerServices
.NET Framework 4.5부터 새로운 방식으로 함수 호출자의 정보를 가져올 수 있게 되었다. 요즘 .NET 6에 대한 뉴스도 돌고 있는 현시점에서 보면 충분히 오래된 방식이다. 만들어야 하는 프로그램의 런타임을 특정 프레임워크만 사용하도록 한정할 수 있다면 이 방식을 사용하는 것을 추천한다. 게임서버는 런타임 환경을 단 하나의 프레임워크로 고정할 수 있으니, 크게 문제될 것이 없다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void DoProcessing()
{
TraceMessage("Something happened.");
}
public void TraceMessage(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
Trace.WriteLine("message: " + message);
Trace.WriteLine("member name: " + memberName);
Trace.WriteLine("source file path: " + sourceFilePath);
Trace.WriteLine("source line number: " + sourceLineNumber);
}
+
함수 인자에 기본값이 있기 때문에 작업자가 함수를 호출할 때 값을 전달하지는 않지만, 그래도 보이지 않게 뒤쪽 인자를 통해 호출자의 파일명, 라인수 등이 넘어가는 방식이다. 인자에 붙어있는 attribute로 인해 함수 호출 위치에 맞는 값들이 런타임에
채워진다.
과거의 오래된 프레임워크를 지원할 수 없다는 점이 거꾸로 단점이 될텐데, 사실 NLog같이 누구나 어디서나 사용해야할 로그모듈을 만들게 아니고, 게임서버처럼 특정 비즈니스 프로젝트로 사용처를 한정한다면 오래된 프레임워크 미지원은 그렇게 큰 단점은 아니다.
CompilerServices 장점 : 가볍고 빠르다.
위에서 언급했던 StackFramek 클래스를 사용하는 방식보다 훨씬 빠르다. C++의 __FILE__, __LINE__
은 매크로니까 이미 컴파일 타임에 문자열과 숫자로 치환되어 코드에 포함된다. CompilerServices 사용 방식은 런타임에 함수의 인자로 넘어가는 방식이니까 이것만큼 optimal할 수는 없지만, 콜스택을 긁어오는 것보다는 훨씬 빠르다.
CompilerService 단점 : 가변인자 인터페이스 사용이 불가능 해진다.
1
2
3
4
5
6
7
8
9
10
11
12
public void DoProcessing()
{
WriteLog("invalid value:{0}", value); // 불가능합니다.
}
public void WriteLog(string format,
params object[] list,
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
...
}
+
함수의 뒷부분 인자를 사용하게 되니까, 위와 같은 사용이 불가능하다. 예시처럼 formatting이 될 문자열을 처음에 받고 두번째부터 가변 인자를 받는 방법은 C++에서 로그 인터페이스를 만드는 가장 익숙한 방식이다.
하지만 C#은 나름대로의 해결법이 있다. 보간 문자열을 이용해 문자열을 포매팅하면 된다. .NET Framework 4.6 과 함께 C# 문법이 6.0으로 올라갔고 이 때부터 보간 문자열이 사용 가능해졌다. 최신의 C#에서는 String.Format보다 보간 문자열의 사용이 더 권장된다. - Effective C#, 빌 와그너. Chapter 1.4 string.Format()을 보간 문자열로 대체하라
1
2
3
4
5
6
7
8
9
10
11
12
public void DoProcessing()
{
// WriteLog("invalid value:{0}", value); // C++스러워 보이지만, 촌스러운 방식이예요.
WriteLog($"invalid value:{value}"); // 가능합니다. 권장됩니다. Effective C# 읽어보세요.
}
public void WriteLog(string message,
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
...
}
+
C#이 5.0이었을 시점만 해도 이건 큰 단점이었다. 하지만 현 시점에서 이것도 그리 문제될 것이 없다.
C++은 코드영역을 사용하지만, C#은 힙을 사용한다.
좀 더 성능에 집착해보자(?).
윗부분에서 잠시 언급했듯이, C++의 __FILE__, __LINE__
은 컴파일 시점에 이미 실제 값으로 변환을 완료하는 preprocessing 이다. 런타임에 함수 호출자 정보를 얻기 위해 추가로 들이는 비용이 거의 없다.
@@ -256,8 +260,10 @@
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;
using System.IO;
using System.Runtime.CompilerServices;
public static class Log
{
public static void Debug(string message, [CallerFilePath] string file = "", [CallerLineNumber] int line = 0)
{
// ... 중략...
provider.Debug($"{message} ({BuildTag(file, line)})");
}
private static string BuildTag(string file, int line)
{
return string.Intern($"{Path.GetFileName(file)}:{line.ToString()}");
}
}
+
전달받은 파일명을 바로 사용하지 않고 string.Intern()으로 한 번 감싸서 사용한다. 로그를 출력하면 아래처럼 찍힌다.
1
2
3
4
5
2020-12-21 12:08:02.144 [Debug] [ConnectionMonitor] add uid:1 #connection:1 (ConnectionMonitor.cs:32)
2020-12-21 12:08:02.145 [Info] [Send] [20017] kREGISTER_GAME_SERVER_REQ actionId:3 (SerializableExt.cs:92)
2020-12-21 12:08:02.205 [Info] db connection Initialized. type:Auth server:localhost count:16 (DbPool.cs:40)
2020-12-21 12:08:02.221 [Info] db connection Initialized. type:Contents server:localhost count:16 (DbPool.cs:40)
2020-12-21 12:08:02.238 [Info] db connection Initialized. type:Game server:localhost count:16 (DbPool.cs:40)
+
interning은 입구만 있고, 출구는 없는 string pool이다. 풀에 등록은 할 수 있지만 해제할 수는 없다. 한 번 쓰고 마는 동적인 문자열은 당연히 interning해서는 안된다. 반복적으로 사용하더라도 빈도가 낮아서, heap의 할당과 해제에 큰 압력을 주지 않는다면 이것도 굳이 interning할 필요는 없다. 이런 문자열들을 interning하면 장시간 떠있어야 하는 서버 프로그램의 경우 오히려 더 악영향을 끼칠 수 있다. 용도에 맞게 적절하게 적용해야 한다.
C#에서 코드에 함께 적혀있는 literl text들은 기본적으로 interning된다. C++처럼 code segment를 직접 가르키지는 않지만, 비슷한 효과를 내기 위함이다. 그 외에 프로그램이 사용하는 나머지 문자열에 대해서는 어떤 것을 interning할지 직접 판단하고 선별 적용해야 한다. 로그 메세지에 반복적으로 찍히는 소스코드 파일명은 interning하기에 적합한 대상이다.
마치면서
로그파일에서 로그 출력 위치를 남기는 방식에 관련해 성능 위주의 고려사항을 정리해 보았다.
diff --git "a/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/index.html" "b/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/index.html"
index 598ca338..a6a335c2 100644
--- "a/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/index.html"
+++ "b/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/index.html"
@@ -3,7 +3,7 @@
-
+
@@ -133,12 +133,12 @@
@@ -227,7 +227,7 @@
2018년에 네트워크 레이어 성능을 끌어올리기 위해 도입했던 System.IO.Pipeline을 간단히 소개하고, 도입 후기를 적어본다.
윈도우 OS에서 고성능을 내기 위한 소켓 프로그래밍을 할 때 IOCP 의 사용은 오래도록 변하지 않는 정답의 자리를 유지하고 있다. 여기에서 좀 더 성능에 욕심을 내고자 한다면 Windows Server 2012부터 등장한 Registerd IO 라는 새로운 선택지가 있다. 하지만 API가 C++ 로만 열려 있어서, C# 구현에서는 사용하기가 쉽지 않다.
하지만 C#에도 고성능 IO를 위한 새로운 API가 추가되었다. Pipeline 이다.
-
+
System.IO.Pipeline 소개.
pipeline을 처음 들었을 때는 IOCP의 뒤를 잇는 새로운 소켓 API인줄 알았다. C++의 RIO가 iocp를 완전히 대체할 수 있는 것처럼.
RIO는 가장 핵심 요소인 등록된 버퍼(registered buffer)
외에, IO 요청 및 완료 통지 방식도 함께 제공하기 때문에 iocp를 완전히 드러내고 대신 사용할 수 있다. 반면 Pipeline은 RIO보다는 커버하는 범위가 좁아서, IOCP를 완전히 대체하는 물건이 될 수는 없다. 이벤트 통지는 기존의 방법들을 이용하면서, 메모리 버퍼의 운용만을 담당하는 라이브러리 이기 때문에 IOCP와 반드시 함께 사용해야 한다.
@@ -249,6 +249,7 @@
1
2
3
4
5
6
7
8
9
// msdn 블로그에 소개된 코드 일부 발췌. Pipe를 하나 만들면 읽기/쓰기 Task를 2개 만든다.
async Task ProcessLinesAsync(Socket socket)
{
var pipe = new Pipe();
Task writing = FillPipeAsync(socket, pipe.Writer);
Task reading = ReadPipeAsync(pipe.Reader);
return Task.WhenAll(reading, writing);
}
+
원인은 Pipeline과 함께 사용하는 task (System.Threading.Tasks.Task) 들이었다. class Pipe
인스턴스 하나를 쓸 때마다 파이프라인에 ‘읽기’와 ‘쓰기’를 담당하는 class Task
객체 두 개를 사용하게 된다. 수신버퍼에만 Pipe를 달면 소켓의 2배, 송수신 버퍼에 모두 달면 소켓의 4배수 만큼의 task가 생성 되어야 하기 때문이다. 게임서버 프로세스당 5,000 명의 동접을 받는다고 하면 최대 20,000개의 task가 생성되고, 이 중 상당수는 waiting 상태로 IO 이벤트를 기다리게 된다.
task가 아무리 가볍다고 해도 네트워크 레이어에만 몇 만개의 task를 만드는 것은 그리 효율적이지 않다. TPL에 대한 이야기를 시작하면 해야 할 말이 아주 많기 때문에 별도의 포스팅으로 분리해야 할 것이다. 과감히 한 줄로 정리해보면, task는 상대적으로 OS의 커널오브젝트인 스레드보다 가볍다는 것이지 수천 수만개를 만들만큼 깃털같은 물건은 아닌 것이다.
스레드가 코드를 한 단계씩 수행하다가 아직 완료되지 않은 task를 await 하는 구문을 만나면 호출 스택을 한 단계씩 거꾸로 올라가면서 동기 로직의 수행을 재개한다. 하지만 완료되지 않은 task를 만났다고 해서 그 즉시 task의 완료 및 반환값 획득을 포기하고 호출스택을 거슬러 올라가는 것은 아니다. 혹시 금방 task가 완료되지 않을까 하는 기대감으로 조금 대기하다가 완료될 기미가 보이지 않으면 그 제서야 태세를 전환하게 된다. 이 전략은 task가 동시성을 매끄럽게 처리하기 위해서는 바람직한 모습이지만, 아주 많은 개수의 task를 장시간(게임서버에서 다음 패킷을 받을 때까지의 평균 시간) 동안 대기시켜야 하는 네트워크 모델에 사용하기에는 적합하지 않다. 스레드들은 각 pipeline의 write task가 RecvComplete 통지를 받고 깨어나기를 기다리면서 수십만 cpu clock을 낭비하게 된다.
@@ -272,6 +273,7 @@ <
패킷을 보낼때는 데이터 타입을 버퍼로 직렬화 한 후, 이 버퍼를 메모리 복사 없이 소켓에 그대로 연결해주기 위한 추가 처리가 있어야 하는데, 이건 송신 버퍼에만 필요한 동작이라서 클래스를 별도로 나누었다. 각 용도에 특화된 메서드가 추가 구현 되어있을 뿐 코어는 모두 비슷하다. 모두 단위 버퍼를 줄줄이 비엔나처럼 연결해 들고 있는 역할을 한다.
이들 중에 가장 기본이 되는 ZeroCopyBuffer 를 조금 보면 아래와 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
namespace Cs.ServerEngine.Network.Buffer
{
public sealed class ZeroCopyBuffer
{
private readonly Queue<LohSegment> segments = new Queue<LohSegment>();
private LohSegment last;
public int SegmentCount => this.segments.Count;
public int CalcTotalSize()
{
int result = 0;
foreach (var data in this.segments)
{
result += data.DataSize;
}
return result;
}
public BinaryWriter GetWriter() => new BinaryWriter(new ZeroCopyOutputStream(this));
public BinaryReader GetReader() => new BinaryReader(new ZeroCopyInputStream(this));
internal void Write(byte[] buffer, int offset, int count)
{
while (count > 0)
{
if (this.last == null || this.last.IsFull)
{
this.last = LohSegment.Create(LohPool.SegmentSize.Size4k);
this.segments.Enqueue(this.last);
}
int copied = this.last.AddData(buffer, offset, count);
offset += copied;
count -= copied;
}
}
internal LohSegment[] Move()
{
var result = this.segments.ToArray();
this.segments.Clear();
this.last = null;
return result;
}
internal LohSegment Peek()
{
return this.segments.Peek();
}
internal void PopHeadSegment()
{
var segment = this.segments.Dequeue();
segment.ToRecycleBin();
if (this.segments.Count == 0)
{
this.last = null;
}
}
}
}
+
본 주제와 관련한 인터페이스만 몇 개 간추려 보았다. Queue<LogSegment>
가 Pipeline 안에 있는 단위버퍼의 링크드 리스트 역할을 한다. Write()와 Move()는 메모리 복사 없이 데이터를 쓰는 인터페이스가 되고, Peek(), PopHeadSegment()는 데이터를 읽는 인터페이스가 되는데, internal 접근자니까 실제 사용계층에는 노출하지 않는다. Detail 하위의 *Stream 클래스를 위한 메서드들이다.
조각난 버퍼를 하나의 가상버퍼처럼 추상화해주는 로직은 *Stream들이 담고있다. System.IO.Stream을 상속했기 때문에 사용 계층에서는 보통의 파일스트림, 메모리 스트림을 다루던 방식과 똑같이 값을 읽고 쓰면 된다. 사용한 segment들을 새지 않게 잘 pooling하고, 버퍼 오프셋 계산할때 오차없이 더하기 빼기 잘해주는 코드가 전부인지라 굳이 옮겨붙이지는 않는다.
이렇게 하니 ZeroCopyBuffer
는 가상의 무한 버퍼 역할을 하고, 사용 계층에는 Stream 형식의 인터페이스를 제공하는 System.IO.Pipeline
의 유사품이 되었다. 제공되는 메서드 중에는 async method
가 하나도 없으니 cpu clock을 불필요하게 낭비할 일도 없다. 이렇게 디자인 하는것이 기존의 iocp 기반 소켓 구현에 익숙한 프로그래머에겐 더 친숙한 모델이면서, 성능상으로도 Pipeline보다 훨씬 낫고(tcp 기반 게임서버 한정), Unity3D처럼 최신의 Memory api가 지원 안되는 환경에서도 문제없이 사용할 수 있다.
diff --git "a/2021/01/01/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-Thread-Local-Storage/index.html" "b/2021/01/01/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-Thread-Local-Storage/index.html"
index a2bb2da9..aff0383e 100644
--- "a/2021/01/01/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-Thread-Local-Storage/index.html"
+++ "b/2021/01/01/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-Thread-Local-Storage/index.html"
@@ -3,7 +3,7 @@
-
+
@@ -131,12 +131,12 @@
@@ -223,7 +223,7 @@
프로그래밍에서 각 스레드별로 고유한 상태를 설정할 수 있는 공간을 Thread Local Storage (이하 TLS. transport layer security 아님) 라고 한다. VC++에서는 __declspec(thread)
키워드를 이용해서 tls 변수를 선언할 수 있다.
C#에도 ThreadLocal<T>
라는 클래스를 이용해 tls를 사용할 수 있지만, 막상 실제로 사용해보면 C++에서는 존재하지 않았던 큰 차이점이 있다. C# 5.0부터 들어온 async / await 문법을 이용해 비동기 프로그래밍을 구현했다면, await 대기 시점 이전과 이후에 스레드가 달라지기 때문이다.
이를 해결하는 방법과 주의해야 할 사항을 정리해본다.
-
+
알림 : 이 글을 처음 포스팅한 후 받은 피드백을 통해 보다 명확한 원인과 해결방법을 추가 확인하게 되어 내용을 수정/보완 했습니다. 최초 버전의 글도 유지하려 했으나 글의 문맥이 복잡해지고 읽기가 어려워져 최종 버전만 남겼습니다.
수정한 내용 요약 : 새로 깨어난 스레드인데도 AsyncLocal<T>
에 값이 남아있던 이유는, 기존의 값이 지워지지 않았기 때문이 아니라, 네트워크 이벤트 콜백으로 깨어난 스레드에도 AsyncLocal<T>
의 값을 복사하고 있었기 때문이었습니다.
@@ -238,6 +238,7 @@ ThreadLocal
우선 잠깐 언급했던 ThreadLocal<T>
클래스를 간단히 알아보자. 이를 이용해 일반적인 tls 변수를 선언하고 사용할 수 있다. 이보다 전부터 있었던 [ThreadStatic]
어트리뷰트로도 똑같이 tls를 선언할 수 있지만, 변수의 초기화 처리에서 ThreadLocal<T>
가 좀 더 매끄러운 처리를 지원한다. 일반적인 tls가 필요할 때는 좀 더 최신의 방식인 ThreadLocal<T>
를 사용하면 된다.
모든 tls 변수에 동일한 값의 복제본을 저장해 두려는 경우가 있다. 예를들어 스레드가 3개 있으면, 메모리 공간상에 각 스레드를 위한 변수 3개가 있고, 이들 모두에 같은 의미를 가지는 인스턴스를 하나씩 생성해 할당하는 경우를 말한다. 서로 다른 스레드끼리 공유해야 할 자원이 있을 때, 해당 자원에 lock이 없이 접근하고 싶다면 tls를 이용해 각 스레드마다 자원을 따로 만들어 각자 자기 리소스를 쓰게 하면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace Cs.Math
{
public static class RandomGenerator
{
public static int Next(int maxValue)
{
return PerThreadRandom.Instance.Next(maxValue);
}
// ... 중략
// 사용 계층에 노출할 인터페이스를 이곳에 정의. 사용자는 tls에 대해 알지 못한다.
// System.Random 객체는 멀티스레드 사용에 안전하지 않으므로 각 스레드마다 개별 생성.
private static class PerThreadRandom
{
private static readonly ThreadLocal<Random> Random = new ThreadLocal<Random>(() => new Random());
internal static Random Instance => Random.Value;
}
}
}
+
이런 경우는 비동기 메서드의 실행중 스레드의 교체가 발생하더라도 아무 문제가 되지 않는다. 어차피 어떤 스레드로 바뀌더라도 tls 변수가 하는 역할은 동일하기 때문이다. 0번 스레드가 불러다 쓰는 Random
객체가 어느순간 2번 스레드의 Random
객체로 바뀐다 해도 동작에 큰 영향이 없다.
AsyncLocal
문제는 스레드별로 tls의 상태가 서로 달라야 할 때 발생한다. 0번 스레드에는 tls에 “철수”가, 2번 스레드에는 “영희”가 적혀있어야 하고, 이를 사용해 스레드마다 다른 동작을 해야 하는 경우. 그런데 거기다 async/await를 이용한 비동기 프로그래밍을 함께 사용한 경우. 0번 철수 스레드가 코드 수행 도중 await 구문을 만나 task의 완료를 기다리고 있었지만, 대기가 풀렸을 때는 2번 스레드로 갈아타게 되면서 철수가 영희가 되버리는 경우다.
@@ -271,9 +272,11 @@ 원치 않는 AsyncLocal 복사는 꺼준다.
다행히 이 동작은 ExecutionContext.SuppressFlow / RestoreFlow 라는 메서드가 있어 쉽게 제어가 가능하다. 우선 스레드풀에 백그라운드 작업을 요청할 때는 SuppressFlow()
호출이 묶여있는 별도의 인터페이스를 만들고 이를 사용하게 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static class BackgroundJob
{
public static void Execute(Action action)
{
using var control = ExecutionContext.SuppressFlow();
ThreadPool.QueueUserWorkItem(_ => action());
}
}
public static class Program
{
public void Foo()
{
int a = 10;
int b = 20;
// 백그라운드 작업이 필요할 때. Wrapping한 인터페이스를 사용한다.
BackgroundJob.Execute(() =>
{
Console.WriteLine($"a + b = {a+b}");
});
}
}
+
작업 요청 후에는 RestoreFlow
를 불러 복구해주면 되는데, SuppressFlow
메서드가 IDisposable인 AsyncFlowControl 객체를 반환하니까 예시처럼 using을 쓰면 좀 더 심플하게 처리할 수 있다.
네트워크 구현부에도 수정이 필요하다. SocketAsyncEventArgs
객체를 사용해 비동기 요청을 수행하는 모든 곳에도 RestoreFlow
를 불러준다. (SocketAsyncEventArgs
는 win32의 OVERLAPPED
구조체를 거의 그대로 랩핑해둔 클래스다.) 예시로 하나만 옮겨보면 아래처럼 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class ConnectionBase
{
public void ConnectAsync(IPAddress ip, int port)
{
var args = new SocketAsyncEventArgs();
args.Completed += this.OnConnectCompleted; // 이 메서드가 새로운 스레드에서 불리게 될 것이다.
args.RemoteEndPoint = new IPEndPoint(ip, port);
using var control = ExecutionContext.SuppressFlow(); // 이걸 넣어주어야 콜백 스레드로 AsyncLocal을 복사하지 않는다.
if (!this.socket.ConnectAsync(args))
{
this.OnConnectCompleted(this.socket, args);
}
}
}
+
이런식으로 SendAsync, RecvAsync 등도 다 막아주어야 일반적인 iocp 콜백 사용 방식과 동일해진다. 다른 코드상에서 아무데도 AsyncLocal<T>
을 사용중이지 않다면 굳이 SuppressFlow 호출이 없어도 동작에는 문제가 없다. 그래도 어차피 사용하지도 않을 암묵적인 실행 컨텍스트간 연결 동작은 그냥 끊어두는 것이 성능상 조금이라도 이득일 듯한 기분이 든다.
정리
- C#의 비동기 메서드는 코드상으로는 매끈하게 이어져 있는듯 보이지만 실은 비동기 요청 지점을 전후로 분리 실행되며, 실행 스레드가 서로 다를 수도 있다.
diff --git "a/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/index.html" "b/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/index.html"
index 2f5cd397..64ba8ee7 100644
--- "a/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/index.html"
+++ "b/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/index.html"
@@ -3,7 +3,7 @@
-
+
@@ -134,12 +134,12 @@
@@ -227,7 +227,7 @@
이제 닷넷의 GC는 꽤나 쓸만하게 발전하여, 웬만한 경우는 프로그래머가 메모리 관리를 굳이 신경쓰지 않고 코딩할 수 있게 도와준다. 그리고 그것이 C++ 대신 C#을 선택하는 큰 이유이기도 하다. 하지만 C# 게임서버로도 성능에 욕심을 내고자 한다면, 짧은 순간 대량의 TPS를 낼 수 있는 네트워크 IO를 구현하려고 한다면 어느정도 메모리 운용에 대한 이해가 필요하다.
이번 포스팅에서는 네트워크 IO의 부하가 가중될 때 겪을 수 있는 메모리 단편화 현상에 대해서 정리해본다.
-
+
기본 용어 및 개념 정리
SOH / LOH / POH
가장 먼저 관리 힙(managed heap)
의 구분부터 이야기 해야한다. 관리힙은 사용 메모리의 크기와 용도 등에 따라 SOH
, LOH
, POH
로 나뉜다.
@@ -273,8 +273,10 @@ C# 고성능 서버 - System.IO.Pipeline 도입 후기에서 여러개의 단위버퍼를 이어붙여 가상의 스트림처럼 운용하는 ZeroCopyBuffer
의 구현에 대해 간단히 소개했었다. 이 때 등장했던 단위버퍼 LohSegment
클래스가 바로 LOH
에 할당한 커다란 청크의 일부분에 해당한다.
1
2
3
4
5
6
7
8
namespace Cs.ServerEngine.Netork.Buffer
{
public sealed class ZeroCopyBuffer
{
private readonly Queue<LohSegment> segments = new Queue<LohSegment>();
private LohSegment last;
// ^ 여기 얘네들이예요.
...
+
LohSegment
를 생성, 풀링하고 관리하는 구현은 크게 대단할 것은 없다. 어차피 할당 크기가 85kb보다 크기만 하면 알아서 LOH
에 할당될 것이고.. 청크를 다시 잘 쪼개서 ConcurrentQueue<>
에 넣어뒀다가 잘 빌려주고 반납하고 관리만 해주면 된다.
조금 더 신경을 쓴다면 서비스 도중 메모리 청크를 추가할당 할 때의 처리 정도가 있겠다. Pool에 남아있는 버퍼의 개수가 좀 모자란다 싶을 때는 CAS 연산으로 소유권을 선점한 스레드 하나만 청크를 할당하게 만든다. 메모리는 추가만 할 뿐 해제는 하지 않을거니까 이렇게 하면 lock을 안 걸어도 되고, pool의 사용도 중단되지 않게 만들 수 있다. 해당 구현체의 멤버변수들만 붙여보면 아래와 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace Cs.Memory
{
public sealed class LohPool
{
private const int ChunkSizeMb = 100;
private const int LowSegmentNumberLimit = 1000;
private readonly int segmentSizeKb;
private readonly int segmentSizeBytes;
private readonly List<byte[]> chunks = new List<byte[]>(capacity: 10);
private readonly ConcurrentQueue<ArraySegment<byte>> segments = new ConcurrentQueue<ArraySegment<byte>>();
private readonly AtomicFlag producerLock = new AtomicFlag(false);
private int totalSegmentCount;
...
}
}
+
정리
C++로만 만들던 게임서버를 C#으로 만든다고 했을 때 가장 신경쓰였던 것이 메모리 부분이었다. 초기구현과 서비스를 거치면서 메모리 누수, 관리힙 사이즈 증가등 많은 메모리 문제를 겪었다. 그 중에서 가장 크게 문제를 겪었던 단편화에 대해 정리해 보았다.
우리가 겪었던 메모리 단편화 가장 주된 요인은 네트워크 IO용 바이트 버퍼의 pinning 때문이었다. 적당한 수준의 부하로는 별 문제 없는데.. 부하를 세게 걸면 점유 메모리가 계속 증가하고 가라않질 않았다. 이건 C++도 마찬가지지만 외형적으로만 관측하면 메모리 누수처럼 보이기 때문에, 단편화가 원인일 것이라는 의심을 하기까지도 많은 검증의 시간이 필요했다.
SOH
에서는 pinning되는 메모리가 많으면 GC 능력이 많이 저하되고 단편화가 심각해진다. 네트워크 버퍼로 사용할 객체들을 LOH
에 할당하면 이런 문제를 해결할 수 있다.
참고자료
@@ -313,6 +315,9 @@
+
+ iTerm2 없이 맥 기본 터미널 꾸미기
+
diff --git "a/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/cursor_setting.png" "b/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/cursor_setting.png"
new file mode 100644
index 00000000..d8a2af00
Binary files /dev/null and "b/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/cursor_setting.png" differ
diff --git "a/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/index.html" "b/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/index.html"
new file mode 100644
index 00000000..01b15ba5
--- /dev/null
+++ "b/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/index.html"
@@ -0,0 +1,348 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+iTerm2 없이 맥 기본 터미널 꾸미기 | leafbird/devnote
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git "a/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/mac_terminal.png" "b/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/mac_terminal.png"
new file mode 100644
index 00000000..d9bd7687
Binary files /dev/null and "b/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/mac_terminal.png" differ
diff --git "a/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/vscode.png" "b/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/vscode.png"
new file mode 100644
index 00000000..d49dd3ba
Binary files /dev/null and "b/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/vscode.png" differ
diff --git "a/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/vscode_setting.png" "b/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/vscode_setting.png"
new file mode 100644
index 00000000..2cae4238
Binary files /dev/null and "b/2023/10/09/iTerm2-\354\227\206\354\235\264-\353\247\245-\352\270\260\353\263\270-\355\204\260\353\257\270\353\204\220-\352\276\270\353\257\270\352\270\260/vscode_setting.png" differ
diff --git "a/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/before_after.png" "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/before_after.png"
new file mode 100644
index 00000000..a86e5f81
Binary files /dev/null and "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/before_after.png" differ
diff --git "a/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/final_desktop.png" "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/final_desktop.png"
new file mode 100644
index 00000000..4493ab29
Binary files /dev/null and "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/final_desktop.png" differ
diff --git "a/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/final_ios.png" "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/final_ios.png"
new file mode 100644
index 00000000..2feec5dc
Binary files /dev/null and "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/final_ios.png" differ
diff --git "a/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/grid_table.png" "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/grid_table.png"
new file mode 100644
index 00000000..4f0d9e2c
Binary files /dev/null and "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/grid_table.png" differ
diff --git "a/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/index.html" "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/index.html"
new file mode 100644
index 00000000..edcb095f
--- /dev/null
+++ "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/index.html"
@@ -0,0 +1,354 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+웹페이지 반응형 디자인하기 | leafbird/devnote
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git "a/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/wiki_image.png" "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/wiki_image.png"
new file mode 100644
index 00000000..4d6bd5ba
Binary files /dev/null and "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/wiki_image.png" differ
diff --git "a/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/\354\212\244\355\201\254\353\246\260\354\203\267 2023-10-07 \354\230\244\355\233\204 10.37.06.png" "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/\354\212\244\355\201\254\353\246\260\354\203\267 2023-10-07 \354\230\244\355\233\204 10.37.06.png"
new file mode 100644
index 00000000..ce9571e0
Binary files /dev/null and "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/\354\212\244\355\201\254\353\246\260\354\203\267 2023-10-07 \354\230\244\355\233\204 10.37.06.png" differ
diff --git "a/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/\354\212\244\355\201\254\353\246\260\354\203\267 2023-10-07 \354\230\244\355\233\204 10.37.27.png" "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/\354\212\244\355\201\254\353\246\260\354\203\267 2023-10-07 \354\230\244\355\233\204 10.37.27.png"
new file mode 100644
index 00000000..16e76765
Binary files /dev/null and "b/2023/10/12/\354\233\271\355\216\230\354\235\264\354\247\200-\353\260\230\354\235\221\355\230\225-\353\224\224\354\236\220\354\235\270\355\225\230\352\270\260/\354\212\244\355\201\254\353\246\260\354\203\267 2023-10-07 \354\230\244\355\233\204 10.37.27.png" differ
diff --git a/archives/2013/12/index.html b/archives/2013/12/index.html
index df229407..1f88f886 100644
--- a/archives/2013/12/index.html
+++ b/archives/2013/12/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,7 +155,7 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
diff --git a/archives/2013/index.html b/archives/2013/index.html
index b9c2634d..ab7db49d 100644
--- a/archives/2013/index.html
+++ b/archives/2013/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,7 +155,7 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
diff --git a/archives/2014/07/index.html b/archives/2014/07/index.html
index 82047c3a..9f510b00 100644
--- a/archives/2014/07/index.html
+++ b/archives/2014/07/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,7 +155,7 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
diff --git a/archives/2014/08/index.html b/archives/2014/08/index.html
index f3eb4402..f902bb4c 100644
--- a/archives/2014/08/index.html
+++ b/archives/2014/08/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,7 +155,7 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
diff --git a/archives/2014/09/index.html b/archives/2014/09/index.html
index 46426032..8ead37bd 100644
--- a/archives/2014/09/index.html
+++ b/archives/2014/09/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,7 +155,7 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
diff --git a/archives/2014/index.html b/archives/2014/index.html
index 2b87394d..8f904b0b 100644
--- a/archives/2014/index.html
+++ b/archives/2014/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,7 +155,7 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
diff --git a/archives/2018/11/index.html b/archives/2018/11/index.html
index 1afeffcc..8c85e978 100644
--- a/archives/2018/11/index.html
+++ b/archives/2018/11/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,7 +155,7 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
diff --git a/archives/2018/index.html b/archives/2018/index.html
index 524b00c0..e689367a 100644
--- a/archives/2018/index.html
+++ b/archives/2018/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,7 +155,7 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
diff --git a/archives/2020/12/index.html b/archives/2020/12/index.html
index 5e240bcb..acb8cf3d 100644
--- a/archives/2020/12/index.html
+++ b/archives/2020/12/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,7 +155,7 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
diff --git a/archives/2020/index.html b/archives/2020/index.html
index d2918d0a..219a8d9c 100644
--- a/archives/2020/index.html
+++ b/archives/2020/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,7 +155,7 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
diff --git a/archives/2021/01/index.html b/archives/2021/01/index.html
index 8f1d5d3b..42b0b6ea 100644
--- a/archives/2021/01/index.html
+++ b/archives/2021/01/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,7 +155,7 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
diff --git a/archives/2021/08/index.html b/archives/2021/08/index.html
index edb01a7e..6bc89e0d 100644
--- a/archives/2021/08/index.html
+++ b/archives/2021/08/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,7 +155,7 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
diff --git a/archives/2021/index.html b/archives/2021/index.html
index 91580aef..40450eb0 100644
--- a/archives/2021/index.html
+++ b/archives/2021/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,7 +155,7 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
diff --git a/archives/2023/10/index.html b/archives/2023/10/index.html
new file mode 100644
index 00000000..81e57bce
--- /dev/null
+++ b/archives/2023/10/index.html
@@ -0,0 +1,272 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+아카이브 | leafbird/devnote
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
+
+
+
+
+ 2023
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/archives/2023/index.html b/archives/2023/index.html
new file mode 100644
index 00000000..2a97b3c9
--- /dev/null
+++ b/archives/2023/index.html
@@ -0,0 +1,272 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+아카이브 | leafbird/devnote
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
+
+
+
+
+ 2023
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/archives/index.html b/archives/index.html
index 9525f08d..0a4a70d2 100644
--- a/archives/index.html
+++ b/archives/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,10 +155,53 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
+
+ 2023
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
2021
@@ -332,46 +375,6 @@ leafbird/devnote
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/archives/page/2/index.html b/archives/page/2/index.html
index fdbc7656..85b96737 100644
--- a/archives/page/2/index.html
+++ b/archives/page/2/index.html
@@ -3,7 +3,7 @@
-
+
@@ -117,12 +117,12 @@ leafbird/devnote
@@ -155,7 +155,7 @@ leafbird/devnote
- 음..! 총 14개의 포스트 포스트를 마저 작성하세요
+ 음..! 총 16개의 포스트 포스트를 마저 작성하세요
@@ -163,6 +163,46 @@ leafbird/devnote
2014
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
pch 파일 사이즈
팀에서 만지는 코드에서는, 290Mb에 육박하는 pch파일을 본 적이 있다(…) 그 땐 코드를 정리하면서 pch 사이즈 변화를 자주 확인해봐야 했는데, 탐색기나 커맨드 창에서 매번 사이즈를 조회하기가 불편했던 기억이 있어서 pch 사이즈 확인하는 걸 만들어봤다.
- +MSBuild로 단일 cpp 파일을 컴파일하면 이런 메시지가 나오는데,
1 | C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin\amd64\CL.exe |
여기 cl.exe
로 들어가는 인자 중에 /Fp"x64\Debug\unittest.pch"
요 부분에 pch 경로가 있음. 그러니까 결국 툴에서 pch사이즈를 구하려면
- 프로젝트 리빌드하고 @@ -237,6 +238,7 @@
- C#의 비동기 메서드는 코드상으로는 매끈하게 이어져 있는듯 보이지만 실은 비동기 요청 지점을 전후로 분리 실행되며, 실행 스레드가 서로 다를 수도 있다. diff --git "a/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/index.html" "b/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/index.html" index 2f5cd397..64ba8ee7 100644 --- "a/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/index.html" +++ "b/2021/08/08/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-\353\251\224\353\252\250\353\246\254-\353\213\250\355\216\270\355\231\224/index.html" @@ -3,7 +3,7 @@ - + @@ -134,12 +134,12 @@ @@ -227,7 +227,7 @@
팀에서 정한 컨벤션도 이 규칙을 그대로 따라야 해서.. 매번 코딩할 때마다 인클루드 순서에 신경쓰기 싫어서 자동화 처리를 작성. 더불어 경로 없이 파일명만 적은 경우나 상대경로를 사용한 인클루드도 지정된 path를 모두 적어주도록 컨버팅하는 처리도 만듦. 만드는 과정이야 대단한 건 없다. sln, vcxproj파일 파싱하는 것은 만들어 두었으니, 그냥 스트링 처리만 좀 더 해주면 금방 만들어진다. 툴로 sorting하고나면 아래처럼 만들어줌.
TestCode.cpp 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// system headers
// other project's headers
// inner project's headers
void main() {
return;
}
+
epilog
대충 이정도 돌아가는 툴을 만들어서 개인 pc에 셋팅해둔 jenkins에 물려놓고 사용중. 원래는 필요없는 include찾아주는 기능만 만들려다가 include sorting 기능은 그냥 한 번 추가나 해볼까 싶어 넣은건데, 아주 편하다. 코딩할 땐 순서 상관 없이 상대경로로 대충 넣어놓고 툴을 돌리면 컨벤션에 맞게 예쁘게 수정해준다.
불필요 인클루드를 찾는 동작은 회사 코드 기준으로 컨텐츠 코드 전체 검색시 50분 정도 걸리는 듯. 이건 매일 새벽에 jenkins가 한 번씩 돌려놓게 해놓고, 매일 아침에 출근해서 확인한다.
pch사이즈는 baseline 구축을 생각하고 만들어 본건데.. (박일, 사례로 살펴보는 디버깅 참고) baseline을 만들려면 지표들을 좀 더 모아야 하고, db도 붙여야 하니 이건 제대로 만들려면 시간이 필요할 것 같다(..라고 쓰고 ‘더이상 업데이트 되지 않는다’ 라고 읽는다.)
diff --git "a/2018/11/12/\355\205\214\355\201\254\353\213\210\354\273\254-\353\246\254\353\215\224\354\213\255-\354\213\234\354\236\221\355\225\230\352\270\260/index.html" "b/2018/11/12/\355\205\214\355\201\254\353\213\210\354\273\254-\353\246\254\353\215\224\354\213\255-\354\213\234\354\236\221\355\225\230\352\270\260/index.html"
index 54a92f88..7560bd45 100644
--- "a/2018/11/12/\355\205\214\355\201\254\353\213\210\354\273\254-\353\246\254\353\215\224\354\213\255-\354\213\234\354\236\221\355\225\230\352\270\260/index.html"
+++ "b/2018/11/12/\355\205\214\355\201\254\353\213\210\354\273\254-\353\246\254\353\215\224\354\213\255-\354\213\234\354\236\221\355\225\230\352\270\260/index.html"
@@ -3,7 +3,7 @@
-
+
@@ -127,12 +127,12 @@
@@ -219,7 +219,7 @@
예전에 트위터 하다가 읽었던 글인데, 개인적으로 마음에 들어서 부족하게나마 번역해 보았습니다.
원문은 슬랙 개발 블로그의 Technical Leadership: Getting Started라는 글입니다.
번역에 크게 자신이 없으니 부담이 없으신 분들은 원문을 보셔요.
-
+
테크니컬 리더십: 시작하기
1 |
|
원문은 슬랙 개발 블로그의 Technical Leadership: Getting Started라는 글입니다.
번역에 크게 자신이 없으니 부담이 없으신 분들은 원문을 보셔요.
내가 소프트웨어 엔지니어가 되기 전에는 이 직업에서 가장 중요한 점은 코딩이라고 생각했다. 그것은 잘못된 생각이었고, 소프트웨어 공학의 가장 중요한(그리고 가장 어려운)점은 다른 사람들과 원만하게 잘 협력하는 것이다.
나는 “관리자는 되지 않을거야!”라고 스스로에게 말해왔고, “그렇게 하면, 내 모든 에너지를 개발에만 집중시킬 수 있을거야!” 라고 생각했다. 내 이후의 경력도 기술 지향적인 실무자 위주로만 관리해 간다면 이 어려운 대인관계를 어느 정도 무시할 수 있을 거라고 생각했다.
diff --git "a/2020/12/26/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-FILE-LINE-\353\214\200\354\262\264\354\240\234/index.html" "b/2020/12/26/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-FILE-LINE-\353\214\200\354\262\264\354\240\234/index.html" index 4b5e4da7..ba3c1335 100644 --- "a/2020/12/26/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-FILE-LINE-\353\214\200\354\262\264\354\240\234/index.html" +++ "b/2020/12/26/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-FILE-LINE-\353\214\200\354\262\264\354\240\234/index.html" @@ -3,7 +3,7 @@ - + @@ -129,12 +129,12 @@ @@ -220,10 +220,11 @@
C++에서 가장 기본적으로 사용했던 __FILE__, __LINE__, __FUNCTION__
등의 매크로와 유사한 효과를 내는 방법에 대해 적어본다. 이와 함께 나에게는 생소했던 string interning 개념에 대해서도 살짝 소개해본다. 자바 같은 managed 언어를 깊이 다뤄본 적이 없는 네이티브 개발자에게는 생소한 개념일 것이다.
UI가 없는 서버에서 동작의 내용을 확인하는 가장 기본적인 방법은 file로 남기는 log다. 정상 동작이나 오류상황에 대한 상세한 로그가 남아야 문제가 생겼을 때 파악하기가 쉽기 때문에, 간단한 동작이지만 아주 빈번하게 호출되는 부분이다. 로그 출력에서 성능을 많이 빼앗기지 않도록 기반을 다져놓으면 비즈니스 로직 구현을 위해 더 많은 H/W 리소스를 배분할 수 있다.
성능을 굳이 신경쓰지 않는다면 아래 있는 내용을 끝까지 모두 적용할 필요는 없다.
-
+
콜스택을 얻어와서 가장 마지막 함수를 찍는 방법
현재 스레드 컨텍스트에서의 StackFrame 정보를 얻어온 후, 프레임 데이터의 가장 마지막 부분을 읽어 호출자의 정보를 얻어낼 수 있다. C#으로 함수 호출 위치를 얻어올 때 가장 많이 쓰이는 방법이다. 가장 태초부터 있었던 방법이기 때문이다. 다음에 설명할 CompilerServices attribute는 .Net Framework 4.5부터 사용이 가능해졌기 때문에, 초창기 C#에서는 콜스택에서 읽어내는 방법 말고는 딱히 다른 선택지도 없었다.
1
2
3
4
5
6
7
8
9
StackTrace st = new StackTrace(new StackFrame(true));
Console.WriteLine(" Stack trace for current level: {0}", st.ToString());
StackFrame sf = st.GetFrame(0);
Console.WriteLine(" File: {0}", sf.GetFileName());
Console.WriteLine(" Method: {0}", sf.GetMethod().Name);
Console.WriteLine(" Line Number: {0}", sf.GetFileLineNumber());
Console.WriteLine(" Column Number: {0}", sf.GetFileColumnNumber());
+
C#에서 흔하게 사용하는 로깅 라이브러리인 Log4Net, NLog 등에서도 이 방법을 사용한다.
콜스택 기반 장점 : 가장 범용적이다. 프레임워크 호환성이 가장 좋음
.Net Framework의 태초부터 있었던 방식이므로 가장 범용적이다. 오래된 버전의 닷넷 프레임워크나 mono 프레임워크 등을 지원해야 하는 상황이라면 이 방법 말고는 마땅한 대안이 없다. 그래서 Log4Net, NLog 등의 유명한 라이브러리도 이 방법을 사용하고 있다. 이들은 불특정 다수의 환경에서 실행되어야 할 범용성이 중요한 모듈이기 때문이다.
콜스택 기반 단점 : 말해서 무엇하랴. 비용이 비싸고 느리다.
지금 회사에서 사용하는 게임서버 엔진은 처음에 Log4Net을 쓰다가, 나중에 NLog로 바꾸었다가, 현재는 자체 구현한 파일로그 모듈을 쓰고 있다. 외부 모듈로는 내가 만족하는 성능을 얻지 못했기 때문이다.
@@ -232,13 +233,16 @@ System.Runtime.CompilerServices
.NET Framework 4.5부터 새로운 방식으로 함수 호출자의 정보를 가져올 수 있게 되었다. 요즘 .NET 6에 대한 뉴스도 돌고 있는 현시점에서 보면 충분히 오래된 방식이다. 만들어야 하는 프로그램의 런타임을 특정 프레임워크만 사용하도록 한정할 수 있다면 이 방식을 사용하는 것을 추천한다. 게임서버는 런타임 환경을 단 하나의 프레임워크로 고정할 수 있으니, 크게 문제될 것이 없다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void DoProcessing()
{
TraceMessage("Something happened.");
}
public void TraceMessage(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
Trace.WriteLine("message: " + message);
Trace.WriteLine("member name: " + memberName);
Trace.WriteLine("source file path: " + sourceFilePath);
Trace.WriteLine("source line number: " + sourceLineNumber);
}
+
함수 인자에 기본값이 있기 때문에 작업자가 함수를 호출할 때 값을 전달하지는 않지만, 그래도 보이지 않게 뒤쪽 인자를 통해 호출자의 파일명, 라인수 등이 넘어가는 방식이다. 인자에 붙어있는 attribute로 인해 함수 호출 위치에 맞는 값들이 런타임에
채워진다.
과거의 오래된 프레임워크를 지원할 수 없다는 점이 거꾸로 단점이 될텐데, 사실 NLog같이 누구나 어디서나 사용해야할 로그모듈을 만들게 아니고, 게임서버처럼 특정 비즈니스 프로젝트로 사용처를 한정한다면 오래된 프레임워크 미지원은 그렇게 큰 단점은 아니다.
CompilerServices 장점 : 가볍고 빠르다.
위에서 언급했던 StackFramek 클래스를 사용하는 방식보다 훨씬 빠르다. C++의 __FILE__, __LINE__
은 매크로니까 이미 컴파일 타임에 문자열과 숫자로 치환되어 코드에 포함된다. CompilerServices 사용 방식은 런타임에 함수의 인자로 넘어가는 방식이니까 이것만큼 optimal할 수는 없지만, 콜스택을 긁어오는 것보다는 훨씬 빠르다.
CompilerService 단점 : 가변인자 인터페이스 사용이 불가능 해진다.
1
2
3
4
5
6
7
8
9
10
11
12
public void DoProcessing()
{
WriteLog("invalid value:{0}", value); // 불가능합니다.
}
public void WriteLog(string format,
params object[] list,
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
...
}
+
함수의 뒷부분 인자를 사용하게 되니까, 위와 같은 사용이 불가능하다. 예시처럼 formatting이 될 문자열을 처음에 받고 두번째부터 가변 인자를 받는 방법은 C++에서 로그 인터페이스를 만드는 가장 익숙한 방식이다.
하지만 C#은 나름대로의 해결법이 있다. 보간 문자열을 이용해 문자열을 포매팅하면 된다. .NET Framework 4.6 과 함께 C# 문법이 6.0으로 올라갔고 이 때부터 보간 문자열이 사용 가능해졌다. 최신의 C#에서는 String.Format보다 보간 문자열의 사용이 더 권장된다. - Effective C#, 빌 와그너. Chapter 1.4 string.Format()을 보간 문자열로 대체하라
1
2
3
4
5
6
7
8
9
10
11
12
public void DoProcessing()
{
// WriteLog("invalid value:{0}", value); // C++스러워 보이지만, 촌스러운 방식이예요.
WriteLog($"invalid value:{value}"); // 가능합니다. 권장됩니다. Effective C# 읽어보세요.
}
public void WriteLog(string message,
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
...
}
+
C#이 5.0이었을 시점만 해도 이건 큰 단점이었다. 하지만 현 시점에서 이것도 그리 문제될 것이 없다.
C++은 코드영역을 사용하지만, C#은 힙을 사용한다.
좀 더 성능에 집착해보자(?).
윗부분에서 잠시 언급했듯이, C++의 __FILE__, __LINE__
은 컴파일 시점에 이미 실제 값으로 변환을 완료하는 preprocessing 이다. 런타임에 함수 호출자 정보를 얻기 위해 추가로 들이는 비용이 거의 없다.
@@ -256,8 +260,10 @@
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;
using System.IO;
using System.Runtime.CompilerServices;
public static class Log
{
public static void Debug(string message, [CallerFilePath] string file = "", [CallerLineNumber] int line = 0)
{
// ... 중략...
provider.Debug($"{message} ({BuildTag(file, line)})");
}
private static string BuildTag(string file, int line)
{
return string.Intern($"{Path.GetFileName(file)}:{line.ToString()}");
}
}
+
전달받은 파일명을 바로 사용하지 않고 string.Intern()으로 한 번 감싸서 사용한다. 로그를 출력하면 아래처럼 찍힌다.
1
2
3
4
5
2020-12-21 12:08:02.144 [Debug] [ConnectionMonitor] add uid:1 #connection:1 (ConnectionMonitor.cs:32)
2020-12-21 12:08:02.145 [Info] [Send] [20017] kREGISTER_GAME_SERVER_REQ actionId:3 (SerializableExt.cs:92)
2020-12-21 12:08:02.205 [Info] db connection Initialized. type:Auth server:localhost count:16 (DbPool.cs:40)
2020-12-21 12:08:02.221 [Info] db connection Initialized. type:Contents server:localhost count:16 (DbPool.cs:40)
2020-12-21 12:08:02.238 [Info] db connection Initialized. type:Game server:localhost count:16 (DbPool.cs:40)
+
interning은 입구만 있고, 출구는 없는 string pool이다. 풀에 등록은 할 수 있지만 해제할 수는 없다. 한 번 쓰고 마는 동적인 문자열은 당연히 interning해서는 안된다. 반복적으로 사용하더라도 빈도가 낮아서, heap의 할당과 해제에 큰 압력을 주지 않는다면 이것도 굳이 interning할 필요는 없다. 이런 문자열들을 interning하면 장시간 떠있어야 하는 서버 프로그램의 경우 오히려 더 악영향을 끼칠 수 있다. 용도에 맞게 적절하게 적용해야 한다.
C#에서 코드에 함께 적혀있는 literl text들은 기본적으로 interning된다. C++처럼 code segment를 직접 가르키지는 않지만, 비슷한 효과를 내기 위함이다. 그 외에 프로그램이 사용하는 나머지 문자열에 대해서는 어떤 것을 interning할지 직접 판단하고 선별 적용해야 한다. 로그 메세지에 반복적으로 찍히는 소스코드 파일명은 interning하기에 적합한 대상이다.
마치면서
로그파일에서 로그 출력 위치를 남기는 방식에 관련해 성능 위주의 고려사항을 정리해 보았다.
diff --git "a/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/index.html" "b/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/index.html"
index 598ca338..a6a335c2 100644
--- "a/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/index.html"
+++ "b/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/index.html"
@@ -3,7 +3,7 @@
-
+
@@ -133,12 +133,12 @@
@@ -227,7 +227,7 @@
2018년에 네트워크 레이어 성능을 끌어올리기 위해 도입했던 System.IO.Pipeline을 간단히 소개하고, 도입 후기를 적어본다.
윈도우 OS에서 고성능을 내기 위한 소켓 프로그래밍을 할 때 IOCP 의 사용은 오래도록 변하지 않는 정답의 자리를 유지하고 있다. 여기에서 좀 더 성능에 욕심을 내고자 한다면 Windows Server 2012부터 등장한 Registerd IO 라는 새로운 선택지가 있다. 하지만 API가 C++ 로만 열려 있어서, C# 구현에서는 사용하기가 쉽지 않다.
하지만 C#에도 고성능 IO를 위한 새로운 API가 추가되었다. Pipeline 이다.
-
+
System.IO.Pipeline 소개.
pipeline을 처음 들었을 때는 IOCP의 뒤를 잇는 새로운 소켓 API인줄 알았다. C++의 RIO가 iocp를 완전히 대체할 수 있는 것처럼.
RIO는 가장 핵심 요소인 등록된 버퍼(registered buffer)
외에, IO 요청 및 완료 통지 방식도 함께 제공하기 때문에 iocp를 완전히 드러내고 대신 사용할 수 있다. 반면 Pipeline은 RIO보다는 커버하는 범위가 좁아서, IOCP를 완전히 대체하는 물건이 될 수는 없다. 이벤트 통지는 기존의 방법들을 이용하면서, 메모리 버퍼의 운용만을 담당하는 라이브러리 이기 때문에 IOCP와 반드시 함께 사용해야 한다.
@@ -249,6 +249,7 @@
1
2
3
4
5
6
7
8
9
// msdn 블로그에 소개된 코드 일부 발췌. Pipe를 하나 만들면 읽기/쓰기 Task를 2개 만든다.
async Task ProcessLinesAsync(Socket socket)
{
var pipe = new Pipe();
Task writing = FillPipeAsync(socket, pipe.Writer);
Task reading = ReadPipeAsync(pipe.Reader);
return Task.WhenAll(reading, writing);
}
+
원인은 Pipeline과 함께 사용하는 task (System.Threading.Tasks.Task) 들이었다. class Pipe
인스턴스 하나를 쓸 때마다 파이프라인에 ‘읽기’와 ‘쓰기’를 담당하는 class Task
객체 두 개를 사용하게 된다. 수신버퍼에만 Pipe를 달면 소켓의 2배, 송수신 버퍼에 모두 달면 소켓의 4배수 만큼의 task가 생성 되어야 하기 때문이다. 게임서버 프로세스당 5,000 명의 동접을 받는다고 하면 최대 20,000개의 task가 생성되고, 이 중 상당수는 waiting 상태로 IO 이벤트를 기다리게 된다.
task가 아무리 가볍다고 해도 네트워크 레이어에만 몇 만개의 task를 만드는 것은 그리 효율적이지 않다. TPL에 대한 이야기를 시작하면 해야 할 말이 아주 많기 때문에 별도의 포스팅으로 분리해야 할 것이다. 과감히 한 줄로 정리해보면, task는 상대적으로 OS의 커널오브젝트인 스레드보다 가볍다는 것이지 수천 수만개를 만들만큼 깃털같은 물건은 아닌 것이다.
스레드가 코드를 한 단계씩 수행하다가 아직 완료되지 않은 task를 await 하는 구문을 만나면 호출 스택을 한 단계씩 거꾸로 올라가면서 동기 로직의 수행을 재개한다. 하지만 완료되지 않은 task를 만났다고 해서 그 즉시 task의 완료 및 반환값 획득을 포기하고 호출스택을 거슬러 올라가는 것은 아니다. 혹시 금방 task가 완료되지 않을까 하는 기대감으로 조금 대기하다가 완료될 기미가 보이지 않으면 그 제서야 태세를 전환하게 된다. 이 전략은 task가 동시성을 매끄럽게 처리하기 위해서는 바람직한 모습이지만, 아주 많은 개수의 task를 장시간(게임서버에서 다음 패킷을 받을 때까지의 평균 시간) 동안 대기시켜야 하는 네트워크 모델에 사용하기에는 적합하지 않다. 스레드들은 각 pipeline의 write task가 RecvComplete 통지를 받고 깨어나기를 기다리면서 수십만 cpu clock을 낭비하게 된다.
@@ -272,6 +273,7 @@ <
패킷을 보낼때는 데이터 타입을 버퍼로 직렬화 한 후, 이 버퍼를 메모리 복사 없이 소켓에 그대로 연결해주기 위한 추가 처리가 있어야 하는데, 이건 송신 버퍼에만 필요한 동작이라서 클래스를 별도로 나누었다. 각 용도에 특화된 메서드가 추가 구현 되어있을 뿐 코어는 모두 비슷하다. 모두 단위 버퍼를 줄줄이 비엔나처럼 연결해 들고 있는 역할을 한다.
이들 중에 가장 기본이 되는 ZeroCopyBuffer 를 조금 보면 아래와 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
namespace Cs.ServerEngine.Network.Buffer
{
public sealed class ZeroCopyBuffer
{
private readonly Queue<LohSegment> segments = new Queue<LohSegment>();
private LohSegment last;
public int SegmentCount => this.segments.Count;
public int CalcTotalSize()
{
int result = 0;
foreach (var data in this.segments)
{
result += data.DataSize;
}
return result;
}
public BinaryWriter GetWriter() => new BinaryWriter(new ZeroCopyOutputStream(this));
public BinaryReader GetReader() => new BinaryReader(new ZeroCopyInputStream(this));
internal void Write(byte[] buffer, int offset, int count)
{
while (count > 0)
{
if (this.last == null || this.last.IsFull)
{
this.last = LohSegment.Create(LohPool.SegmentSize.Size4k);
this.segments.Enqueue(this.last);
}
int copied = this.last.AddData(buffer, offset, count);
offset += copied;
count -= copied;
}
}
internal LohSegment[] Move()
{
var result = this.segments.ToArray();
this.segments.Clear();
this.last = null;
return result;
}
internal LohSegment Peek()
{
return this.segments.Peek();
}
internal void PopHeadSegment()
{
var segment = this.segments.Dequeue();
segment.ToRecycleBin();
if (this.segments.Count == 0)
{
this.last = null;
}
}
}
}
+
본 주제와 관련한 인터페이스만 몇 개 간추려 보았다. Queue<LogSegment>
가 Pipeline 안에 있는 단위버퍼의 링크드 리스트 역할을 한다. Write()와 Move()는 메모리 복사 없이 데이터를 쓰는 인터페이스가 되고, Peek(), PopHeadSegment()는 데이터를 읽는 인터페이스가 되는데, internal 접근자니까 실제 사용계층에는 노출하지 않는다. Detail 하위의 *Stream 클래스를 위한 메서드들이다.
조각난 버퍼를 하나의 가상버퍼처럼 추상화해주는 로직은 *Stream들이 담고있다. System.IO.Stream을 상속했기 때문에 사용 계층에서는 보통의 파일스트림, 메모리 스트림을 다루던 방식과 똑같이 값을 읽고 쓰면 된다. 사용한 segment들을 새지 않게 잘 pooling하고, 버퍼 오프셋 계산할때 오차없이 더하기 빼기 잘해주는 코드가 전부인지라 굳이 옮겨붙이지는 않는다.
이렇게 하니 ZeroCopyBuffer
는 가상의 무한 버퍼 역할을 하고, 사용 계층에는 Stream 형식의 인터페이스를 제공하는 System.IO.Pipeline
의 유사품이 되었다. 제공되는 메서드 중에는 async method
가 하나도 없으니 cpu clock을 불필요하게 낭비할 일도 없다. 이렇게 디자인 하는것이 기존의 iocp 기반 소켓 구현에 익숙한 프로그래머에겐 더 친숙한 모델이면서, 성능상으로도 Pipeline보다 훨씬 낫고(tcp 기반 게임서버 한정), Unity3D처럼 최신의 Memory api가 지원 안되는 환경에서도 문제없이 사용할 수 있다.
diff --git "a/2021/01/01/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-Thread-Local-Storage/index.html" "b/2021/01/01/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-Thread-Local-Storage/index.html"
index a2bb2da9..aff0383e 100644
--- "a/2021/01/01/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-Thread-Local-Storage/index.html"
+++ "b/2021/01/01/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-Thread-Local-Storage/index.html"
@@ -3,7 +3,7 @@
-
+
@@ -131,12 +131,12 @@
@@ -223,7 +223,7 @@
프로그래밍에서 각 스레드별로 고유한 상태를 설정할 수 있는 공간을 Thread Local Storage (이하 TLS. transport layer security 아님) 라고 한다. VC++에서는 __declspec(thread)
키워드를 이용해서 tls 변수를 선언할 수 있다.
C#에도 ThreadLocal<T>
라는 클래스를 이용해 tls를 사용할 수 있지만, 막상 실제로 사용해보면 C++에서는 존재하지 않았던 큰 차이점이 있다. C# 5.0부터 들어온 async / await 문법을 이용해 비동기 프로그래밍을 구현했다면, await 대기 시점 이전과 이후에 스레드가 달라지기 때문이다.
이를 해결하는 방법과 주의해야 할 사항을 정리해본다.
-
+
알림 : 이 글을 처음 포스팅한 후 받은 피드백을 통해 보다 명확한 원인과 해결방법을 추가 확인하게 되어 내용을 수정/보완 했습니다. 최초 버전의 글도 유지하려 했으나 글의 문맥이 복잡해지고 읽기가 어려워져 최종 버전만 남겼습니다.
수정한 내용 요약 : 새로 깨어난 스레드인데도 AsyncLocal<T>
에 값이 남아있던 이유는, 기존의 값이 지워지지 않았기 때문이 아니라, 네트워크 이벤트 콜백으로 깨어난 스레드에도 AsyncLocal<T>
의 값을 복사하고 있었기 때문이었습니다.
@@ -238,6 +238,7 @@ ThreadLocal
우선 잠깐 언급했던 ThreadLocal<T>
클래스를 간단히 알아보자. 이를 이용해 일반적인 tls 변수를 선언하고 사용할 수 있다. 이보다 전부터 있었던 [ThreadStatic]
어트리뷰트로도 똑같이 tls를 선언할 수 있지만, 변수의 초기화 처리에서 ThreadLocal<T>
가 좀 더 매끄러운 처리를 지원한다. 일반적인 tls가 필요할 때는 좀 더 최신의 방식인 ThreadLocal<T>
를 사용하면 된다.
모든 tls 변수에 동일한 값의 복제본을 저장해 두려는 경우가 있다. 예를들어 스레드가 3개 있으면, 메모리 공간상에 각 스레드를 위한 변수 3개가 있고, 이들 모두에 같은 의미를 가지는 인스턴스를 하나씩 생성해 할당하는 경우를 말한다. 서로 다른 스레드끼리 공유해야 할 자원이 있을 때, 해당 자원에 lock이 없이 접근하고 싶다면 tls를 이용해 각 스레드마다 자원을 따로 만들어 각자 자기 리소스를 쓰게 하면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace Cs.Math
{
public static class RandomGenerator
{
public static int Next(int maxValue)
{
return PerThreadRandom.Instance.Next(maxValue);
}
// ... 중략
// 사용 계층에 노출할 인터페이스를 이곳에 정의. 사용자는 tls에 대해 알지 못한다.
// System.Random 객체는 멀티스레드 사용에 안전하지 않으므로 각 스레드마다 개별 생성.
private static class PerThreadRandom
{
private static readonly ThreadLocal<Random> Random = new ThreadLocal<Random>(() => new Random());
internal static Random Instance => Random.Value;
}
}
}
+
이런 경우는 비동기 메서드의 실행중 스레드의 교체가 발생하더라도 아무 문제가 되지 않는다. 어차피 어떤 스레드로 바뀌더라도 tls 변수가 하는 역할은 동일하기 때문이다. 0번 스레드가 불러다 쓰는 Random
객체가 어느순간 2번 스레드의 Random
객체로 바뀐다 해도 동작에 큰 영향이 없다.
AsyncLocal
문제는 스레드별로 tls의 상태가 서로 달라야 할 때 발생한다. 0번 스레드에는 tls에 “철수”가, 2번 스레드에는 “영희”가 적혀있어야 하고, 이를 사용해 스레드마다 다른 동작을 해야 하는 경우. 그런데 거기다 async/await를 이용한 비동기 프로그래밍을 함께 사용한 경우. 0번 철수 스레드가 코드 수행 도중 await 구문을 만나 task의 완료를 기다리고 있었지만, 대기가 풀렸을 때는 2번 스레드로 갈아타게 되면서 철수가 영희가 되버리는 경우다.
@@ -271,9 +272,11 @@ 원치 않는 AsyncLocal 복사는 꺼준다.
다행히 이 동작은 ExecutionContext.SuppressFlow / RestoreFlow 라는 메서드가 있어 쉽게 제어가 가능하다. 우선 스레드풀에 백그라운드 작업을 요청할 때는 SuppressFlow()
호출이 묶여있는 별도의 인터페이스를 만들고 이를 사용하게 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static class BackgroundJob
{
public static void Execute(Action action)
{
using var control = ExecutionContext.SuppressFlow();
ThreadPool.QueueUserWorkItem(_ => action());
}
}
public static class Program
{
public void Foo()
{
int a = 10;
int b = 20;
// 백그라운드 작업이 필요할 때. Wrapping한 인터페이스를 사용한다.
BackgroundJob.Execute(() =>
{
Console.WriteLine($"a + b = {a+b}");
});
}
}
+
작업 요청 후에는 RestoreFlow
를 불러 복구해주면 되는데, SuppressFlow
메서드가 IDisposable인 AsyncFlowControl 객체를 반환하니까 예시처럼 using을 쓰면 좀 더 심플하게 처리할 수 있다.
네트워크 구현부에도 수정이 필요하다. SocketAsyncEventArgs
객체를 사용해 비동기 요청을 수행하는 모든 곳에도 RestoreFlow
를 불러준다. (SocketAsyncEventArgs
는 win32의 OVERLAPPED
구조체를 거의 그대로 랩핑해둔 클래스다.) 예시로 하나만 옮겨보면 아래처럼 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class ConnectionBase
{
public void ConnectAsync(IPAddress ip, int port)
{
var args = new SocketAsyncEventArgs();
args.Completed += this.OnConnectCompleted; // 이 메서드가 새로운 스레드에서 불리게 될 것이다.
args.RemoteEndPoint = new IPEndPoint(ip, port);
using var control = ExecutionContext.SuppressFlow(); // 이걸 넣어주어야 콜백 스레드로 AsyncLocal을 복사하지 않는다.
if (!this.socket.ConnectAsync(args))
{
this.OnConnectCompleted(this.socket, args);
}
}
}
+
이런식으로 SendAsync, RecvAsync 등도 다 막아주어야 일반적인 iocp 콜백 사용 방식과 동일해진다. 다른 코드상에서 아무데도 AsyncLocal<T>
을 사용중이지 않다면 굳이 SuppressFlow 호출이 없어도 동작에는 문제가 없다. 그래도 어차피 사용하지도 않을 암묵적인 실행 컨텍스트간 연결 동작은 그냥 끊어두는 것이 성능상 조금이라도 이득일 듯한 기분이 든다.
정리
이제 닷넷의 GC는 꽤나 쓸만하게 발전하여, 웬만한 경우는 프로그래머가 메모리 관리를 굳이 신경쓰지 않고 코딩할 수 있게 도와준다. 그리고 그것이 C++ 대신 C#을 선택하는 큰 이유이기도 하다. 하지만 C# 게임서버로도 성능에 욕심을 내고자 한다면, 짧은 순간 대량의 TPS를 낼 수 있는 네트워크 IO를 구현하려고 한다면 어느정도 메모리 운용에 대한 이해가 필요하다.
이번 포스팅에서는 네트워크 IO의 부하가 가중될 때 겪을 수 있는 메모리 단편화 현상에 대해서 정리해본다.
-
+
기본 용어 및 개념 정리
SOH / LOH / POH
가장 먼저 관리 힙(managed heap)
의 구분부터 이야기 해야한다. 관리힙은 사용 메모리의 크기와 용도 등에 따라 SOH
, LOH
, POH
로 나뉜다.
@@ -273,8 +273,10 @@ C# 고성능 서버 - System.IO.Pipeline 도입 후기에서 여러개의 단위버퍼를 이어붙여 가상의 스트림처럼 운용하는 ZeroCopyBuffer
의 구현에 대해 간단히 소개했었다. 이 때 등장했던 단위버퍼 LohSegment
클래스가 바로 LOH
에 할당한 커다란 청크의 일부분에 해당한다.
1
2
3
4
5
6
7
8
namespace Cs.ServerEngine.Netork.Buffer
{
public sealed class ZeroCopyBuffer
{
private readonly Queue<LohSegment> segments = new Queue<LohSegment>();
private LohSegment last;
// ^ 여기 얘네들이예요.
...
+
LohSegment
를 생성, 풀링하고 관리하는 구현은 크게 대단할 것은 없다. 어차피 할당 크기가 85kb보다 크기만 하면 알아서 LOH
에 할당될 것이고.. 청크를 다시 잘 쪼개서 ConcurrentQueue<>
에 넣어뒀다가 잘 빌려주고 반납하고 관리만 해주면 된다.
조금 더 신경을 쓴다면 서비스 도중 메모리 청크를 추가할당 할 때의 처리 정도가 있겠다. Pool에 남아있는 버퍼의 개수가 좀 모자란다 싶을 때는 CAS 연산으로 소유권을 선점한 스레드 하나만 청크를 할당하게 만든다. 메모리는 추가만 할 뿐 해제는 하지 않을거니까 이렇게 하면 lock을 안 걸어도 되고, pool의 사용도 중단되지 않게 만들 수 있다. 해당 구현체의 멤버변수들만 붙여보면 아래와 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace Cs.Memory
{
public sealed class LohPool
{
private const int ChunkSizeMb = 100;
private const int LowSegmentNumberLimit = 1000;
private readonly int segmentSizeKb;
private readonly int segmentSizeBytes;
private readonly List<byte[]> chunks = new List<byte[]>(capacity: 10);
private readonly ConcurrentQueue<ArraySegment<byte>> segments = new ConcurrentQueue<ArraySegment<byte>>();
private readonly AtomicFlag producerLock = new AtomicFlag(false);
private int totalSegmentCount;
...
}
}
+
정리
C++로만 만들던 게임서버를 C#으로 만든다고 했을 때 가장 신경쓰였던 것이 메모리 부분이었다. 초기구현과 서비스를 거치면서 메모리 누수, 관리힙 사이즈 증가등 많은 메모리 문제를 겪었다. 그 중에서 가장 크게 문제를 겪었던 단편화에 대해 정리해 보았다.
우리가 겪었던 메모리 단편화 가장 주된 요인은 네트워크 IO용 바이트 버퍼의 pinning 때문이었다. 적당한 수준의 부하로는 별 문제 없는데.. 부하를 세게 걸면 점유 메모리가 계속 증가하고 가라않질 않았다. 이건 C++도 마찬가지지만 외형적으로만 관측하면 메모리 누수처럼 보이기 때문에, 단편화가 원인일 것이라는 의심을 하기까지도 많은 검증의 시간이 필요했다.
SOH
에서는 pinning되는 메모리가 많으면 GC 능력이 많이 저하되고 단편화가 심각해진다. 네트워크 버퍼로 사용할 객체들을 LOH
에 할당하면 이런 문제를 해결할 수 있다.
참고자료
@@ -313,6 +315,9 @@
+
+ iTerm2 없이 맥 기본 터미널 꾸미기
+
C++에서 가장 기본적으로 사용했던 __FILE__, __LINE__, __FUNCTION__
등의 매크로와 유사한 효과를 내는 방법에 대해 적어본다. 이와 함께 나에게는 생소했던 string interning 개념에 대해서도 살짝 소개해본다. 자바 같은 managed 언어를 깊이 다뤄본 적이 없는 네이티브 개발자에게는 생소한 개념일 것이다.
UI가 없는 서버에서 동작의 내용을 확인하는 가장 기본적인 방법은 file로 남기는 log다. 정상 동작이나 오류상황에 대한 상세한 로그가 남아야 문제가 생겼을 때 파악하기가 쉽기 때문에, 간단한 동작이지만 아주 빈번하게 호출되는 부분이다. 로그 출력에서 성능을 많이 빼앗기지 않도록 기반을 다져놓으면 비즈니스 로직 구현을 위해 더 많은 H/W 리소스를 배분할 수 있다.
성능을 굳이 신경쓰지 않는다면 아래 있는 내용을 끝까지 모두 적용할 필요는 없다.
- +콜스택을 얻어와서 가장 마지막 함수를 찍는 방법
현재 스레드 컨텍스트에서의 StackFrame 정보를 얻어온 후, 프레임 데이터의 가장 마지막 부분을 읽어 호출자의 정보를 얻어낼 수 있다. C#으로 함수 호출 위치를 얻어올 때 가장 많이 쓰이는 방법이다. 가장 태초부터 있었던 방법이기 때문이다. 다음에 설명할 CompilerServices attribute는 .Net Framework 4.5부터 사용이 가능해졌기 때문에, 초창기 C#에서는 콜스택에서 읽어내는 방법 말고는 딱히 다른 선택지도 없었다.
1 | StackTrace st = new StackTrace(new StackFrame(true)); |
C#에서 흔하게 사용하는 로깅 라이브러리인 Log4Net, NLog 등에서도 이 방법을 사용한다.
콜스택 기반 장점 : 가장 범용적이다. 프레임워크 호환성이 가장 좋음
.Net Framework의 태초부터 있었던 방식이므로 가장 범용적이다. 오래된 버전의 닷넷 프레임워크나 mono 프레임워크 등을 지원해야 하는 상황이라면 이 방법 말고는 마땅한 대안이 없다. 그래서 Log4Net, NLog 등의 유명한 라이브러리도 이 방법을 사용하고 있다. 이들은 불특정 다수의 환경에서 실행되어야 할 범용성이 중요한 모듈이기 때문이다.
콜스택 기반 단점 : 말해서 무엇하랴. 비용이 비싸고 느리다.
지금 회사에서 사용하는 게임서버 엔진은 처음에 Log4Net을 쓰다가, 나중에 NLog로 바꾸었다가, 현재는 자체 구현한 파일로그 모듈을 쓰고 있다. 외부 모듈로는 내가 만족하는 성능을 얻지 못했기 때문이다.
@@ -232,13 +233,16 @@System.Runtime.CompilerServices
.NET Framework 4.5부터 새로운 방식으로 함수 호출자의 정보를 가져올 수 있게 되었다. 요즘 .NET 6에 대한 뉴스도 돌고 있는 현시점에서 보면 충분히 오래된 방식이다. 만들어야 하는 프로그램의 런타임을 특정 프레임워크만 사용하도록 한정할 수 있다면 이 방식을 사용하는 것을 추천한다. 게임서버는 런타임 환경을 단 하나의 프레임워크로 고정할 수 있으니, 크게 문제될 것이 없다.
1 | public void DoProcessing() |
함수 인자에 기본값이 있기 때문에 작업자가 함수를 호출할 때 값을 전달하지는 않지만, 그래도 보이지 않게 뒤쪽 인자를 통해 호출자의 파일명, 라인수 등이 넘어가는 방식이다. 인자에 붙어있는 attribute로 인해 함수 호출 위치에 맞는 값들이 런타임에
채워진다.
과거의 오래된 프레임워크를 지원할 수 없다는 점이 거꾸로 단점이 될텐데, 사실 NLog같이 누구나 어디서나 사용해야할 로그모듈을 만들게 아니고, 게임서버처럼 특정 비즈니스 프로젝트로 사용처를 한정한다면 오래된 프레임워크 미지원은 그렇게 큰 단점은 아니다.
CompilerServices 장점 : 가볍고 빠르다.
위에서 언급했던 StackFramek 클래스를 사용하는 방식보다 훨씬 빠르다. C++의 __FILE__, __LINE__
은 매크로니까 이미 컴파일 타임에 문자열과 숫자로 치환되어 코드에 포함된다. CompilerServices 사용 방식은 런타임에 함수의 인자로 넘어가는 방식이니까 이것만큼 optimal할 수는 없지만, 콜스택을 긁어오는 것보다는 훨씬 빠르다.
CompilerService 단점 : 가변인자 인터페이스 사용이 불가능 해진다.
1 | public void DoProcessing() |
함수의 뒷부분 인자를 사용하게 되니까, 위와 같은 사용이 불가능하다. 예시처럼 formatting이 될 문자열을 처음에 받고 두번째부터 가변 인자를 받는 방법은 C++에서 로그 인터페이스를 만드는 가장 익숙한 방식이다.
하지만 C#은 나름대로의 해결법이 있다. 보간 문자열을 이용해 문자열을 포매팅하면 된다. .NET Framework 4.6 과 함께 C# 문법이 6.0으로 올라갔고 이 때부터 보간 문자열이 사용 가능해졌다. 최신의 C#에서는 String.Format보다 보간 문자열의 사용이 더 권장된다. - Effective C#, 빌 와그너. Chapter 1.4 string.Format()을 보간 문자열로 대체하라
1 | public void DoProcessing() |
C#이 5.0이었을 시점만 해도 이건 큰 단점이었다. 하지만 현 시점에서 이것도 그리 문제될 것이 없다.
C++은 코드영역을 사용하지만, C#은 힙을 사용한다.
좀 더 성능에 집착해보자(?).
윗부분에서 잠시 언급했듯이, C++의 __FILE__, __LINE__
은 컴파일 시점에 이미 실제 값으로 변환을 완료하는 preprocessing 이다. 런타임에 함수 호출자 정보를 얻기 위해 추가로 들이는 비용이 거의 없다.
1 | using System; |
전달받은 파일명을 바로 사용하지 않고 string.Intern()으로 한 번 감싸서 사용한다. 로그를 출력하면 아래처럼 찍힌다.
1 | 2020-12-21 12:08:02.144 [Debug] [ConnectionMonitor] add uid:1 #connection:1 (ConnectionMonitor.cs:32) |
interning은 입구만 있고, 출구는 없는 string pool이다. 풀에 등록은 할 수 있지만 해제할 수는 없다. 한 번 쓰고 마는 동적인 문자열은 당연히 interning해서는 안된다. 반복적으로 사용하더라도 빈도가 낮아서, heap의 할당과 해제에 큰 압력을 주지 않는다면 이것도 굳이 interning할 필요는 없다. 이런 문자열들을 interning하면 장시간 떠있어야 하는 서버 프로그램의 경우 오히려 더 악영향을 끼칠 수 있다. 용도에 맞게 적절하게 적용해야 한다.
C#에서 코드에 함께 적혀있는 literl text들은 기본적으로 interning된다. C++처럼 code segment를 직접 가르키지는 않지만, 비슷한 효과를 내기 위함이다. 그 외에 프로그램이 사용하는 나머지 문자열에 대해서는 어떤 것을 interning할지 직접 판단하고 선별 적용해야 한다. 로그 메세지에 반복적으로 찍히는 소스코드 파일명은 interning하기에 적합한 대상이다.
마치면서
로그파일에서 로그 출력 위치를 남기는 방식에 관련해 성능 위주의 고려사항을 정리해 보았다.
diff --git "a/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/index.html" "b/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/index.html" index 598ca338..a6a335c2 100644 --- "a/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/index.html" +++ "b/2020/12/27/C-\352\263\240\354\204\261\353\212\245-\354\204\234\353\262\204-System-IO-Pipeline-\353\217\204\354\236\205-\355\233\204\352\270\260/index.html" @@ -3,7 +3,7 @@ - + @@ -133,12 +133,12 @@ @@ -227,7 +227,7 @@2018년에 네트워크 레이어 성능을 끌어올리기 위해 도입했던 System.IO.Pipeline을 간단히 소개하고, 도입 후기를 적어본다.
윈도우 OS에서 고성능을 내기 위한 소켓 프로그래밍을 할 때 IOCP 의 사용은 오래도록 변하지 않는 정답의 자리를 유지하고 있다. 여기에서 좀 더 성능에 욕심을 내고자 한다면 Windows Server 2012부터 등장한 Registerd IO 라는 새로운 선택지가 있다. 하지만 API가 C++ 로만 열려 있어서, C# 구현에서는 사용하기가 쉽지 않다.
하지만 C#에도 고성능 IO를 위한 새로운 API가 추가되었다. Pipeline 이다.
- +System.IO.Pipeline 소개.
pipeline을 처음 들었을 때는 IOCP의 뒤를 잇는 새로운 소켓 API인줄 알았다. C++의 RIO가 iocp를 완전히 대체할 수 있는 것처럼.
RIO는 가장 핵심 요소인 등록된 버퍼(registered buffer)
외에, IO 요청 및 완료 통지 방식도 함께 제공하기 때문에 iocp를 완전히 드러내고 대신 사용할 수 있다. 반면 Pipeline은 RIO보다는 커버하는 범위가 좁아서, IOCP를 완전히 대체하는 물건이 될 수는 없다. 이벤트 통지는 기존의 방법들을 이용하면서, 메모리 버퍼의 운용만을 담당하는 라이브러리 이기 때문에 IOCP와 반드시 함께 사용해야 한다.
1 | // msdn 블로그에 소개된 코드 일부 발췌. Pipe를 하나 만들면 읽기/쓰기 Task를 2개 만든다. |
원인은 Pipeline과 함께 사용하는 task (System.Threading.Tasks.Task) 들이었다. class Pipe
인스턴스 하나를 쓸 때마다 파이프라인에 ‘읽기’와 ‘쓰기’를 담당하는 class Task
객체 두 개를 사용하게 된다. 수신버퍼에만 Pipe를 달면 소켓의 2배, 송수신 버퍼에 모두 달면 소켓의 4배수 만큼의 task가 생성 되어야 하기 때문이다. 게임서버 프로세스당 5,000 명의 동접을 받는다고 하면 최대 20,000개의 task가 생성되고, 이 중 상당수는 waiting 상태로 IO 이벤트를 기다리게 된다.
task가 아무리 가볍다고 해도 네트워크 레이어에만 몇 만개의 task를 만드는 것은 그리 효율적이지 않다. TPL에 대한 이야기를 시작하면 해야 할 말이 아주 많기 때문에 별도의 포스팅으로 분리해야 할 것이다. 과감히 한 줄로 정리해보면, task는 상대적으로 OS의 커널오브젝트인 스레드보다 가볍다는 것이지 수천 수만개를 만들만큼 깃털같은 물건은 아닌 것이다.
스레드가 코드를 한 단계씩 수행하다가 아직 완료되지 않은 task를 await 하는 구문을 만나면 호출 스택을 한 단계씩 거꾸로 올라가면서 동기 로직의 수행을 재개한다. 하지만 완료되지 않은 task를 만났다고 해서 그 즉시 task의 완료 및 반환값 획득을 포기하고 호출스택을 거슬러 올라가는 것은 아니다. 혹시 금방 task가 완료되지 않을까 하는 기대감으로 조금 대기하다가 완료될 기미가 보이지 않으면 그 제서야 태세를 전환하게 된다. 이 전략은 task가 동시성을 매끄럽게 처리하기 위해서는 바람직한 모습이지만, 아주 많은 개수의 task를 장시간(게임서버에서 다음 패킷을 받을 때까지의 평균 시간) 동안 대기시켜야 하는 네트워크 모델에 사용하기에는 적합하지 않다. 스레드들은 각 pipeline의 write task가 RecvComplete 통지를 받고 깨어나기를 기다리면서 수십만 cpu clock을 낭비하게 된다.
@@ -272,6 +273,7 @@<
패킷을 보낼때는 데이터 타입을 버퍼로 직렬화 한 후, 이 버퍼를 메모리 복사 없이 소켓에 그대로 연결해주기 위한 추가 처리가 있어야 하는데, 이건 송신 버퍼에만 필요한 동작이라서 클래스를 별도로 나누었다. 각 용도에 특화된 메서드가 추가 구현 되어있을 뿐 코어는 모두 비슷하다. 모두 단위 버퍼를 줄줄이 비엔나처럼 연결해 들고 있는 역할을 한다.
이들 중에 가장 기본이 되는 ZeroCopyBuffer 를 조금 보면 아래와 같다.
1 | namespace Cs.ServerEngine.Network.Buffer |
본 주제와 관련한 인터페이스만 몇 개 간추려 보았다. Queue<LogSegment>
가 Pipeline 안에 있는 단위버퍼의 링크드 리스트 역할을 한다. Write()와 Move()는 메모리 복사 없이 데이터를 쓰는 인터페이스가 되고, Peek(), PopHeadSegment()는 데이터를 읽는 인터페이스가 되는데, internal 접근자니까 실제 사용계층에는 노출하지 않는다. Detail 하위의 *Stream 클래스를 위한 메서드들이다.
조각난 버퍼를 하나의 가상버퍼처럼 추상화해주는 로직은 *Stream들이 담고있다. System.IO.Stream을 상속했기 때문에 사용 계층에서는 보통의 파일스트림, 메모리 스트림을 다루던 방식과 똑같이 값을 읽고 쓰면 된다. 사용한 segment들을 새지 않게 잘 pooling하고, 버퍼 오프셋 계산할때 오차없이 더하기 빼기 잘해주는 코드가 전부인지라 굳이 옮겨붙이지는 않는다.
이렇게 하니 ZeroCopyBuffer
는 가상의 무한 버퍼 역할을 하고, 사용 계층에는 Stream 형식의 인터페이스를 제공하는 System.IO.Pipeline
의 유사품이 되었다. 제공되는 메서드 중에는 async method
가 하나도 없으니 cpu clock을 불필요하게 낭비할 일도 없다. 이렇게 디자인 하는것이 기존의 iocp 기반 소켓 구현에 익숙한 프로그래머에겐 더 친숙한 모델이면서, 성능상으로도 Pipeline보다 훨씬 낫고(tcp 기반 게임서버 한정), Unity3D처럼 최신의 Memory api가 지원 안되는 환경에서도 문제없이 사용할 수 있다.
프로그래밍에서 각 스레드별로 고유한 상태를 설정할 수 있는 공간을 Thread Local Storage (이하 TLS. transport layer security 아님) 라고 한다. VC++에서는 __declspec(thread)
키워드를 이용해서 tls 변수를 선언할 수 있다.
C#에도 ThreadLocal<T>
라는 클래스를 이용해 tls를 사용할 수 있지만, 막상 실제로 사용해보면 C++에서는 존재하지 않았던 큰 차이점이 있다. C# 5.0부터 들어온 async / await 문법을 이용해 비동기 프로그래밍을 구현했다면, await 대기 시점 이전과 이후에 스레드가 달라지기 때문이다.
이를 해결하는 방법과 주의해야 할 사항을 정리해본다.
- +알림 : 이 글을 처음 포스팅한 후 받은 피드백을 통해 보다 명확한 원인과 해결방법을 추가 확인하게 되어 내용을 수정/보완 했습니다. 최초 버전의 글도 유지하려 했으나 글의 문맥이 복잡해지고 읽기가 어려워져 최종 버전만 남겼습니다.
수정한 내용 요약 : 새로 깨어난 스레드인데도
@@ -238,6 +238,7 @@AsyncLocal<T>
에 값이 남아있던 이유는, 기존의 값이 지워지지 않았기 때문이 아니라, 네트워크 이벤트 콜백으로 깨어난 스레드에도AsyncLocal<T>
의 값을 복사하고 있었기 때문이었습니다.ThreadLocal
우선 잠깐 언급했던
ThreadLocal<T>
클래스를 간단히 알아보자. 이를 이용해 일반적인 tls 변수를 선언하고 사용할 수 있다. 이보다 전부터 있었던[ThreadStatic]
어트리뷰트로도 똑같이 tls를 선언할 수 있지만, 변수의 초기화 처리에서ThreadLocal<T>
가 좀 더 매끄러운 처리를 지원한다. 일반적인 tls가 필요할 때는 좀 더 최신의 방식인ThreadLocal<T>
를 사용하면 된다.모든 tls 변수에 동일한 값의 복제본을 저장해 두려는 경우가 있다. 예를들어 스레드가 3개 있으면, 메모리 공간상에 각 스레드를 위한 변수 3개가 있고, 이들 모두에 같은 의미를 가지는 인스턴스를 하나씩 생성해 할당하는 경우를 말한다. 서로 다른 스레드끼리 공유해야 할 자원이 있을 때, 해당 자원에 lock이 없이 접근하고 싶다면 tls를 이용해 각 스레드마다 자원을 따로 만들어 각자 자기 리소스를 쓰게 하면 된다.
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 namespace Cs.Math
{
public static class RandomGenerator
{
public static int Next(int maxValue)
{
return PerThreadRandom.Instance.Next(maxValue);
}
// ... 중략
// 사용 계층에 노출할 인터페이스를 이곳에 정의. 사용자는 tls에 대해 알지 못한다.
// System.Random 객체는 멀티스레드 사용에 안전하지 않으므로 각 스레드마다 개별 생성.
private static class PerThreadRandom
{
private static readonly ThreadLocal<Random> Random = new ThreadLocal<Random>(() => new Random());
internal static Random Instance => Random.Value;
}
}
}이런 경우는 비동기 메서드의 실행중 스레드의 교체가 발생하더라도 아무 문제가 되지 않는다. 어차피 어떤 스레드로 바뀌더라도 tls 변수가 하는 역할은 동일하기 때문이다. 0번 스레드가 불러다 쓰는
Random
객체가 어느순간 2번 스레드의Random
객체로 바뀐다 해도 동작에 큰 영향이 없다.AsyncLocal
문제는 스레드별로 tls의 상태가 서로 달라야 할 때 발생한다. 0번 스레드에는 tls에 “철수”가, 2번 스레드에는 “영희”가 적혀있어야 하고, 이를 사용해 스레드마다 다른 동작을 해야 하는 경우. 그런데 거기다 async/await를 이용한 비동기 프로그래밍을 함께 사용한 경우. 0번 철수 스레드가 코드 수행 도중 await 구문을 만나 task의 완료를 기다리고 있었지만, 대기가 풀렸을 때는 2번 스레드로 갈아타게 되면서 철수가 영희가 되버리는 경우다.
@@ -271,9 +272,11 @@원치 않는 AsyncLocal 복사는 꺼준다.
다행히 이 동작은 ExecutionContext.SuppressFlow / RestoreFlow 라는 메서드가 있어 쉽게 제어가 가능하다. 우선 스레드풀에 백그라운드 작업을 요청할 때는
SuppressFlow()
호출이 묶여있는 별도의 인터페이스를 만들고 이를 사용하게 한다.+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 public static class BackgroundJob
{
public static void Execute(Action action)
{
using var control = ExecutionContext.SuppressFlow();
ThreadPool.QueueUserWorkItem(_ => action());
}
}
public static class Program
{
public void Foo()
{
int a = 10;
int b = 20;
// 백그라운드 작업이 필요할 때. Wrapping한 인터페이스를 사용한다.
BackgroundJob.Execute(() =>
{
Console.WriteLine($"a + b = {a+b}");
});
}
}작업 요청 후에는
RestoreFlow
를 불러 복구해주면 되는데,SuppressFlow
메서드가 IDisposable인 AsyncFlowControl 객체를 반환하니까 예시처럼 using을 쓰면 좀 더 심플하게 처리할 수 있다.네트워크 구현부에도 수정이 필요하다.
SocketAsyncEventArgs
객체를 사용해 비동기 요청을 수행하는 모든 곳에도RestoreFlow
를 불러준다. (SocketAsyncEventArgs
는 win32의OVERLAPPED
구조체를 거의 그대로 랩핑해둔 클래스다.) 예시로 하나만 옮겨보면 아래처럼 된다.+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 public abstract class ConnectionBase
{
public void ConnectAsync(IPAddress ip, int port)
{
var args = new SocketAsyncEventArgs();
args.Completed += this.OnConnectCompleted; // 이 메서드가 새로운 스레드에서 불리게 될 것이다.
args.RemoteEndPoint = new IPEndPoint(ip, port);
using var control = ExecutionContext.SuppressFlow(); // 이걸 넣어주어야 콜백 스레드로 AsyncLocal을 복사하지 않는다.
if (!this.socket.ConnectAsync(args))
{
this.OnConnectCompleted(this.socket, args);
}
}
}이런식으로 SendAsync, RecvAsync 등도 다 막아주어야 일반적인 iocp 콜백 사용 방식과 동일해진다. 다른 코드상에서 아무데도
AsyncLocal<T>
을 사용중이지 않다면 굳이 SuppressFlow 호출이 없어도 동작에는 문제가 없다. 그래도 어차피 사용하지도 않을 암묵적인 실행 컨텍스트간 연결 동작은 그냥 끊어두는 것이 성능상 조금이라도 이득일 듯한 기분이 든다.정리
이제 닷넷의 GC는 꽤나 쓸만하게 발전하여, 웬만한 경우는 프로그래머가 메모리 관리를 굳이 신경쓰지 않고 코딩할 수 있게 도와준다. 그리고 그것이 C++ 대신 C#을 선택하는 큰 이유이기도 하다. 하지만 C# 게임서버로도 성능에 욕심을 내고자 한다면, 짧은 순간 대량의 TPS를 낼 수 있는 네트워크 IO를 구현하려고 한다면 어느정도 메모리 운용에 대한 이해가 필요하다.
이번 포스팅에서는 네트워크 IO의 부하가 가중될 때 겪을 수 있는 메모리 단편화 현상에 대해서 정리해본다.
- +기본 용어 및 개념 정리
SOH / LOH / POH
가장 먼저
관리 힙(managed heap)
의 구분부터 이야기 해야한다. 관리힙은 사용 메모리의 크기와 용도 등에 따라SOH
,LOH
,POH
로 나뉜다.@@ -273,8 +273,10 @@
C# 고성능 서버 - System.IO.Pipeline 도입 후기에서 여러개의 단위버퍼를 이어붙여 가상의 스트림처럼 운용하는
ZeroCopyBuffer
의 구현에 대해 간단히 소개했었다. 이 때 등장했던 단위버퍼LohSegment
클래스가 바로LOH
에 할당한 커다란 청크의 일부분에 해당한다.+
1
2
3
4
5
6
7
8 namespace Cs.ServerEngine.Netork.Buffer
{
public sealed class ZeroCopyBuffer
{
private readonly Queue<LohSegment> segments = new Queue<LohSegment>();
private LohSegment last;
// ^ 여기 얘네들이예요.
...
LohSegment
를 생성, 풀링하고 관리하는 구현은 크게 대단할 것은 없다. 어차피 할당 크기가 85kb보다 크기만 하면 알아서LOH
에 할당될 것이고.. 청크를 다시 잘 쪼개서ConcurrentQueue<>
에 넣어뒀다가 잘 빌려주고 반납하고 관리만 해주면 된다.
조금 더 신경을 쓴다면 서비스 도중 메모리 청크를 추가할당 할 때의 처리 정도가 있겠다. Pool에 남아있는 버퍼의 개수가 좀 모자란다 싶을 때는 CAS 연산으로 소유권을 선점한 스레드 하나만 청크를 할당하게 만든다. 메모리는 추가만 할 뿐 해제는 하지 않을거니까 이렇게 하면 lock을 안 걸어도 되고, pool의 사용도 중단되지 않게 만들 수 있다. 해당 구현체의 멤버변수들만 붙여보면 아래와 같다.+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 namespace Cs.Memory
{
public sealed class LohPool
{
private const int ChunkSizeMb = 100;
private const int LowSegmentNumberLimit = 1000;
private readonly int segmentSizeKb;
private readonly int segmentSizeBytes;
private readonly List<byte[]> chunks = new List<byte[]>(capacity: 10);
private readonly ConcurrentQueue<ArraySegment<byte>> segments = new ConcurrentQueue<ArraySegment<byte>>();
private readonly AtomicFlag producerLock = new AtomicFlag(false);
private int totalSegmentCount;
...
}
}정리
C++로만 만들던 게임서버를 C#으로 만든다고 했을 때 가장 신경쓰였던 것이 메모리 부분이었다. 초기구현과 서비스를 거치면서 메모리 누수, 관리힙 사이즈 증가등 많은 메모리 문제를 겪었다. 그 중에서 가장 크게 문제를 겪었던 단편화에 대해 정리해 보았다.
우리가 겪었던 메모리 단편화 가장 주된 요인은 네트워크 IO용 바이트 버퍼의 pinning 때문이었다. 적당한 수준의 부하로는 별 문제 없는데.. 부하를 세게 걸면 점유 메모리가 계속 증가하고 가라않질 않았다. 이건 C++도 마찬가지지만 외형적으로만 관측하면 메모리 누수처럼 보이기 때문에, 단편화가 원인일 것이라는 의심을 하기까지도 많은 검증의 시간이 필요했다.
SOH
에서는 pinning되는 메모리가 많으면 GC 능력이 많이 저하되고 단편화가 심각해진다. 네트워크 버퍼로 사용할 객체들을LOH
에 할당하면 이런 문제를 해결할 수 있다.참고자료
@@ -313,6 +315,9 @@
+ + iTerm2 없이 맥 기본 터미널 꾸미기 +