Microsoft MVP성태의 닷넷 이야기
.NET Framework: 192. GC의 부하는 상대적인 것! [링크 복사], [링크+제목 복사]
조회: 19336
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 

GC의 부하는 상대적인 것!

최근에 읽은 글인데요.

Managed code Performance 측정하기
; http://blogs.msdn.com/b/heejaechang/archive/2010/11/07/managed-code-performance.aspx

꼭 위의 글을 읽지 않았더라도, 우리가 흔히 알고 있기를 GC가 구동된다는 것은 그만큼 부하가 높아진다는 인식을 가지고 있습니다.

"제니퍼 닷넷" 제품을 만들어서 테스트하다 보니, 나름대로 성능에 대한 다양한 경험을 하게 되는데, 위의 글을 읽으면서 갑자기 GC와 관련된 한가지 사례가 생각나더군요.

이야기의 발단은, "제니퍼 닷넷"에 COM+ 모니터링을 추가한 것에서부터 시작됩니다.

COM+ 호출 모니터링 및 누수 확인
; https://www.sysnet.pe.kr/2/0/881

COM+ 서버 모니터링
; https://www.sysnet.pe.kr/2/0/884

처음 "제니퍼 닷넷"에 "COM+ 모니터링" 기능을 넣었을 때는 명시적인 Dispose를 하지 않은 경우, 그에 대해 "경고"로 알려주는 것이 기본 동작이었습니다. 그런데, 차츰 제니퍼 닷넷 Trial 적용을 직접 사이트에 해보면서 제법 많은 사이트들이 "명시적인 Dispose"를 하지 않는다는 것을 경험하게 되었고, 처음에는 그런 고객들에게 "명시적인 Dispose"를 하지 않은 경우 웹 사이트의 성능이 어느 정도 안 좋아질 수 있는 지를 "성능 비교 자료"를 통해 알려주기 위해 내부적으로 부하 테스트를 시작하게 되었습니다.

그런데, 엉뚱하게도 그 결과로 인해 "제니퍼 닷넷"의 기본 동작이 바뀌게 됩니다.




자, 그럼 어떤 결과이길래 그럴까요?

테스트를 위해, "COM+ 서버"와 그것을 호출하는 "ComplusLeakTest.aspx"를 만들었습니다.

[환경]
* 부하 도구가 설치된 운영체제: Windows Server 2008 R2 (물리 머신 A)
* 테스트 웹 응용 프로그램이 설치된 운영체제: Windows Server 2008 R2 (물리 머신 B의 가상 머신)
* EXE 유형의 COM+ 서버와 웹 서버는 동일한 PC에 구성

=== EXE 유형의 COM+ 서버 ===

public class MyComponentServer : ServicedComponent
{
    public MyComponentServer() 
    {
        count = 0;
    }

    int count;

    [AutoComplete]
    public int MethodAC()
    {
        return count++;
    }
}

=== 웹 페이지 ComplusLeakTest.aspx ===

if (Request.QueryString["m"] == "6")
{
    // 명시적인 Dispose 호출
    using (EntLibServerApp.MyComponentServer mcs = new EntLibServerApp.MyComponentServer())
    {
        mcs.MethodAC(connectionString);
    }
}
else
{
    // Dispose 호출하지 않음.
    EntLibServerApp.MyComponentServer mcs = new EntLibServerApp.MyComponentServer();
    mcs.MethodAC(connectionString);
}

이제 부하 툴을 이용해서 "ComplusLeakTest.aspx?m=6"과 "ComplusLeakTest.aspx?m=7"로 비교를 해봅니다.

CPU 소비가 100%가 되도록 요청 부하를 올리고, 처리량을 보니 전혀 의도하지 않았던 결과가 나왔습니다.

"명시적인 Dispose(m=6)"

gc_expensive_perf_test_1.png

"Dispose 를 GC에 맡긴 경우(m=7)"

gc_expensive_perf_test_2.png

보는 바와 같이 명시적인 Dispose를 한 경우에는 서비스 처리가 초당 384건만 되었던 반면, GC에 맡긴 경우에는 초당 600건이 넘는 압도적인 차이를 보였습니다.

재미있는 것은, 이렇게 호출이 되는 경우 GC가 발생한 건수입니다. 테스트를 1분 정도 진행하는 동안 GC의 발생횟수입니다.

[m=6, 명시적인 Dispose]
1분 동안 총 호출 건수: 22,108
GC: 35번

[m=7, Dispose 제거]
1분 동안 총 호출 건수: 34,678
GC: 6,024번

웹 페이지 처리 건수에 비례해서 GC가 호출된다고 보기에는 너무나 큰 차이입니다. 즉, "명시적인 Dispose"를 안해줌으로 인해 GC의 호출 건수는 대폭 증가하지만, 오히려 그 부하는 아무것도 아니라는 듯이 처리 건수는 Dispose를 호출하지 않았을 때가 더 많습니다.

게다가 CPU 소비 차이를 보이기 위해서 부하량을 초당 220번으로 낮춰서 꾸준히 보내는 방식으로도 테스트를 해본 것도 흥미롭습니다.

[m=6, 명시적인 Dispose]
CPU 사용률: 62%

[m=7, Dispose 제거]
CPU 사용률: 33%

6,024번의 GC 발생을 동반한 CPU 소비율이 33%인 반면, 명시적인 Dispose를 이용하여 겨우 35번의 GC만 발생했을 때의 CPU 소비율이 62%라는 것은, 개인적으로도 매우 의외의 결과였습니다. 이 결과로 판단해 보면, COM+ 서버의 명시적인 Dispose 호출로 인해 프로세스간 통신이 발생하고 자원 정리가 되는 부하가 GC의 일반적인 부하보다도 훨씬 크다는 결론이 나오고, 따라서 우리가 알고 있는 "GC의 부하"라는 것이 상황에 따라서는 결코 expensive하지 않다는 것입니다.

결국, 오히려 COM+ 개체의 명시적인 Dispose 호출이 시스템 성능적인 면에서 결코 도움이 되지 않았다는 것인데 "제니퍼 닷넷"에서 이를 "경고"로 알려주기에는 무리가 있었으므로 기본 동작이 바뀌게 된 것입니다. (물론, 옵션 값 조정을 통해서 경고로 나타낼 수 있습니다. ^^)




이 글을 읽어보시는 분들 중에 실제로 위와 같은 테스트를 하시는 분들이 있다면 데이터 공유를 부탁드립니다. 혹시나 제가 테스트 조건을 올바르지 않게 주었을 가능성도 있기 때문에, 가능한 여러 테스트 결과가 나오면 좋을 것 같습니다.

그리고, 혹시나 극단적인 결론을 내리실 분들을 위해서 미리 말씀드리면,

위의 결과만 보면, 명시적인 Dispose를 코드 상에서 없애는 것이 더 좋을 수도 있지만 꼭 그렇지 않을 수도 있습니다. 예를 들어, JITA/AutoComplete이 아닌 COM+ 개체가 native 자원을 사용하는 경우 등에는 반드시 명시적인 Dispose가 필요하겠습니다.

그나저나, 제가 직접 저런 테스트 결과를 겪긴 했지만, 여전히 Dispose를 없애는 것에 대해서 망설여지는 것은... 왜일까요? ^^



[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 12/2/2022]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2010-11-26 09시15분
[lancers] 아니, 왜 이러셔요.. 아마추어 같이..
ServicedComponent의 Dispose 내부 동작을 쫓아 들어가다 보면 얼마나 많은 일을 하는지 뻔히 알면서...
게다가 COM+쪽에서 JITA를 쓰느냐 안 쓰느냐, 트랜잭션을 거느냐 안거느냐에 따라서 시나리오가 매우 달라질 것 같은데요.

명시적 Dispose를 부르는게 단기간의 단순 호출에 대해서는 분명히 GC보다 더 많은 Cost가 있을 겁니다.
Dispose를 부르는 목적이 자원 관리를 보다 즉각적으로 안전하고 확실하게 하기 위한 것이지, 성능을 위한 것은 아니잖아요.
[guest]
2010-11-26 10시35분
음... 이 글의 주제는 그러니까... ^^ 'GC의 부하'라는 것이 우리가 인식했던 것만큼 그리 크지 않다는 것입니다.
그리고, 테스트를 해봤는데 COM+ 쪽에서 JITA 및 트랜잭션 유무에 대해서 전혀 결과가 달라지지 않았습니다. (혹시나 제가 테스트를 잘못 했을 수도 있으니 한번 해보세요. ^^ 결과는 동일합니다.)

게다가, 어쩌면 우리가 COM+의 Dispose에 너무 민감했던 것이 아닌가 싶은 생각도 듭니다. 마이크로소프트가 ServicedComponent 해제 시에 반드시 정리해야 하는 민감한 Native 자원이 있었다면 FileStream에라도 구현되어 있는 Finalize를 구현했을 것입니다. 즉, 그다지 중요하게 관리될 Native 자원이 없다는 것이고, 나머지는 'Managed 자원'들일 텐데 그 부분들에서 Dispose 없이 오히려 CPU가 안정적으로 서비스를 하고 있다면 문제될 것이 없어 보입니다.

그 동안 반대로 질문했던 적이 없었던 것 같습니다. 그렇다면 왜? COM+를 Dispose 해야하는 걸까요?
위의 글에서도 밝힌 것처럼 사용자가 정의한 COM+ 개체에 별도로 정의된 Native 자원이 없는 상황에서 Dispose를 해줘야 할 이유가 없습니다. AutoComplete만으로 자원 해제 문제는 충분히 해결됩니다.

그리고... 사실 모든 것이 '성능'을 위해서가 아니었나요? ^^ DB 연결이 아무리 늘어나도 여전히 DB 서버가 뛰어난 성능으로 서비스를 할 수 있었다면, 아마도 대부분의 개발자들이 굳이 DB 연결을 닫는 작업을 할 생각조차 느끼지 않았을 것처럼. 트랜잭션을 AutoComplete으로 닫게 하는 것도 결국 성능이죠. 빨리 안 닫았다가는 그만큼 다른 코드에서 DB 처리가 어려워지니. JITA 옵션도 그렇고.
정성태
2010-11-26 08시50분
[lancers] 주제는 당연히 동의합니다. ㅎㅎ
ServicedCompoent의 Dispose()는 Unmanaged 리소스와 Managed 리소스를 둘 다 해제하는 역할을 합니다.
그에 따라 COM+ 컴포넌트가 Managed로만 작성된 ServicedComponent이고, 내부에서 별도의 Unmanaged 리소스를 사용하지 않고, Unmanaged 리소스를 사용하지 않는 다른 COM+ 컴포넌트를 참조하지도 않는다면 Dispose를 부르는 것이 GC되도록 내버려두는 것보다 Cost가 더 크겠죠.
Dispose에서는 GC의 동작보다 더 많은 동작을 하니까요. 이 경우, 상대적으로 GC가 Dispose보다 Cost가 낮다라는 것이 이 글의 핵심이잖아요? ^^

하지만 내가 호출하는 COM+ 개체가 Managed인지 Unmanaged인지, 그 안에서 Managed 리소스만 쓰는지, Unmanaged 리소스만 쓰는지, 사용하는 메모리 이외의 자원을 내부나 Deactivate에서 깔끔하게 정리하는지.. 내가 그걸 만든 사람이 아니고서는 알 수가 없다는 겁니다. 그래서 Dispose를 없애는 것에 대해 망설여지는 거죠. ^^ (확인사살은 성능적으로만 보면 불필요한 과정이니까..)
[guest]
2010-11-26 10시26분
넵. 맞습니다. 서버쪽의 코드를 어떻게 구현했는지 알 수 없으므로. 어느 시점까지는 native를 사용하지 않다가도, 사용하는 쪽으로 바뀔 수도 있고. ^^
정성태

[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13609정성태4/27/202470닷넷: 2250. PInvoke 호출 시 참조 타입(class)을 마샬링하는 [IN], [OUT] 특성파일 다운로드1
13608정성태4/26/2024424닷넷: 2249. C# - 부모의 필드/프로퍼티에 대해 서로 다른 자식 클래스 간에 Reflection 접근이 동작할까요?파일 다운로드1
13607정성태4/25/2024424닷넷: 2248. C# - 인터페이스 타입의 다중 포인터를 인자로 갖는 C/C++ 함수 연동
13606정성태4/24/2024480닷넷: 2247. C# - tensorflow 연동 (MNIST 예제)파일 다운로드1
13605정성태4/23/2024715닷넷: 2246. C# - Python.NET을 이용한 파이썬 소스코드 연동파일 다운로드1
13604정성태4/22/2024759오류 유형: 901. Visual Studio - Unable to set the next statement. Set next statement cannot be used in '[Exception]' call stack frames.
13603정성태4/21/2024905닷넷: 2245. C# - IronPython을 이용한 파이썬 소스코드 연동파일 다운로드1
13602정성태4/20/2024945닷넷: 2244. C# - PCM 오디오 데이터를 연속(Streaming) 재생 (Windows Multimedia)파일 다운로드1
13601정성태4/19/2024972닷넷: 2243. C# - PCM 사운드 재생(NAudio)파일 다운로드1
13600정성태4/18/2024989닷넷: 2242. C# - 관리 스레드와 비관리 스레드
13599정성태4/17/2024938닷넷: 2241. C# - WAV 파일의 PCM 사운드 재생(Windows Multimedia)파일 다운로드1
13598정성태4/16/2024981닷넷: 2240. C# - WAV 파일 포맷 + LIST 헤더파일 다운로드2
13597정성태4/15/2024977닷넷: 2239. C# - WAV 파일의 PCM 데이터 생성 및 출력파일 다운로드1
13596정성태4/14/20241084닷넷: 2238. C# - WAV 기본 파일 포맷파일 다운로드1
13595정성태4/13/20241070닷넷: 2237. C# - Audio 장치 열기 (Windows Multimedia, NAudio)파일 다운로드1
13594정성태4/12/20241088닷넷: 2236. C# - Audio 장치 열람 (Windows Multimedia, NAudio)파일 다운로드1
13593정성태4/8/20241091닷넷: 2235. MSBuild - AccelerateBuildsInVisualStudio 옵션
13592정성태4/2/20241228C/C++: 165. CLion으로 만든 Rust Win32 DLL을 C#과 연동
13591정성태4/2/20241203닷넷: 2234. C# - WPF 응용 프로그램에 Blazor App 통합파일 다운로드1
13590정성태3/31/20241084Linux: 70. Python - uwsgi 응용 프로그램이 k8s 환경에서 OOM 발생하는 문제
13589정성태3/29/20241162닷넷: 2233. C# - 프로세스 CPU 사용량을 나타내는 성능 카운터와 Win32 API파일 다운로드1
13588정성태3/28/20241436닷넷: 2232. C# - Unity + 닷넷 App(WinForms/WPF) 간의 Named Pipe 통신 [2]파일 다운로드1
13587정성태3/27/20241362오류 유형: 900. Windows Update 오류 - 8024402C, 80070643
13586정성태3/27/20241542Windows: 263. Windows - 복구 파티션(Recovery Partition) 용량을 늘리는 방법
13585정성태3/26/20241501Windows: 262. PerformanceCounter의 InstanceName에 pid를 추가한 "Process V2"
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...