Go에서 쓰레드간 데이터 공유시, Channel과 Mutex의 속도 비교

Don’t communicate by sharing memory; share memory by communicating.

라길래, channel과 mutex의 성능을 알아봤다.

테스트 머신은 Xeon CPU가 두 개 달렸고, 12G의 램을 가지고 있다. 각 CPU당 6개의 코어가 있으므로, 하이퍼쓰레딩을 이용하여 최대 24개의 코어를 사용할 수 있다.

테스트는 channel을 사용하는 경우와 mutex를 사용하는 경우로 나눴으며, 10번의 결과를 평균내어 최종 결론을 만든다.

아래의 그래프가 그 결과로, 사용하는 코어의 개수와 관계없이 channel을 사용하는 것이 mutex를 사용하는 것보다 느리다는 것을 알려준다.

추가로 테스트 한 바, channel의 크기를 변경하는 것은 결과에 별다른 영향을 미치지 못한다.

golang_comapre_message_mutex

Go로 프로그래밍 할 경우, channel을 사용할지 mutex를 사용할지에 대한 조언은 여기에 좀 더 자세히 나와있다.

hiredis for Windows

Redis is simple but powerful memory-based key/value DB and it supports various programming languages. As it is like a dark horse of web, you can easily find any of its drivers for popular web programming languages. But, if you don’t write code for web and use C/C++, don’t worry! They have official C client library called hiredis also.

As you expected, of course, it works well if you are under Linux-based machine. The problem is hiredis does not support Windows compile by itself. So, you have to port its source code to Windows, huh?

Right, it’s not a good idea and not even easy for ordinary C/C++ programmer. You may think you can find some with googling. Okay, here’s my story.

I’m a game server programmer and decided to use Redis for my game. Because I had an experience with hiredis on my Mac, I didn’t expect I will spend such a long time for that – almost a whole work day.

Firstly, as other programmers usually do, I’ve googled about hiredis for windows compile and got the link. Even though the project was a little bit old, it’s under control by Microsoft! I’ve just kept following blog instruction and it worked in some wise. But, as Microsoft’s hard situation, it vomited tons of link errors I could not solve easily, while testing pub/sub functionality. I consumed most of my time to figure it out and almost gave up, almost.

At that moment, by chance, I thought about Node.js. If I google ‘hiredis windows’, the top most result is ‘hiredis – npm’. Node.js is built by C++ and I could get some clue from it. If I followed the link and there was Eldorado!

Windows

Dmitry Gorbunov (@fuwaneko) made a fork of hiredis-node with Windows support.

That fork works clearly + perfectly! No additional code is needed at all. Pub/sub works nicely, too. Node saved my life and everybody live in peace.

That’s how could I find Eldorado for a whole work day. If you encounter above situation, just ignore MSOpenTech and give your hands to Node.

Windows Batch Script

이 블로그에는 세 개의 카테고리가 있다. ‘개(Dog)’, ‘발(Foot)’, ‘인생(Life)’이 그것인데, 순서대로 ‘어떠한 것에 대한 설명’, ‘실수한 경험’, ‘코드가 없는 이야기’라는 의미를 담아두었다. 대충 그렇다.

이번에 쓰려던 것은 사실, ‘개(Dog)’카테고리에 넣으려고 했던 것이다. 이것이 ‘발(Foot)’카테고리에 들어가게 된 데에는 아래와 같은 사연이 있다.

회사에는 수십 대의 서버가 있고, 거기에는 수시로 로그가 쌓이게 된다. 그 양이 꽤 많아서, 몇 달 지나면 금새 하드디스크가 다 차버리게 되는데, 어찌된 영문인지 이를 자동으로 지워주는 기능이 없었다.

서버는 윈도 서버였으며, 로그는 최근 세 달의 것만 유지하도록 하고자 했다. 어려울 거 없다. 세달전의 로그를 지우는 스크립트 혹은 프로그램을 주기적으로 실행해주기만 하면 된다.

윈도에는 리눅스의 crontab과 비슷한 Task Scheduler가 있으며, crontab과 비슷한 형태로 실행하려는 명령을 등록할 수 있다. Manager프로그램을 이용해서 Task를 등록하려면, 이를 실행할 계정정보를 명시해야 한다. 개발자가 공통으로 사용하는 계정은 몇 달 간격으로 비밀번호를 변경하는데, 이것이 Task Scheduler가 등록한 Task를 실행하지 못하는 원인이 되므로, 다른 개발자가 주기적으로 명령어를 실행하는 프로그램을 윈도 서비스로 만들어 놨다. 그리곤, 그걸 써보랜다.

‘MS가 바보도 아니고, 그런걸 생각하지 못했을 리 없잖아!. 게다가, 당신이 만든 그 서비스를 등록하는 것도 귀찮다고!’라고 속으로만 생각했다. 왜냐면, 그 사람이 한참 상사였으니깐.

AT라는 Command Line명령으로 Task Scheduler에 Task를 등록하면, 기본적으로(Manager프로그램에서 변경할 수 있다) System계정이 사용된다. 즉, 그냥 AT명령어 사용하면 된다. 몇 번만 구글링해보면 나온다. 한번은 부족할 수 있다.

로그를 지우는 스크립트는 Windows Batch파일로 작성했다. 이 과정이 길었다. Batch파일의 문법은 제약이 많고, 표현도 풍부하지 못하다. 결정적으로, 난 거의 모른다. 그래도 굳이 Batch파일을 사용한 이유는, 누구나 소스를 보고 수정할 수 있고, 서버에 설치해야 할 프로그램이 없다는 점 때문이었다. 어줍잖은 오픈 마인드.

도저히 익숙해지지 않을법한 문법도 문제였지만, 더 큰 문제는 날짜간 연산을 지원하지 않는 관계로, 대학교 숙제 생각하며 윤년 계산하며 이를 직접 구현해야 했다는데 있다. 그래서…, Ruby로 했으면 30분이면 끝났을 일을 하루 종일 했다.

최종적으로 스크립트를 복사하고 Task Scheduler에 등록하는 등의 일련의 과정은 Ruby를 이용해서 만들고, 이를 OCRA로 exe 변환, 서버에서 실행/설치하는 것으로 마무리 지었다.

당연히 테스트 했고, 날짜계산도 잘 되고…, 뭐 암튼 다 잘 되었다. 처음 작성한 Batch파일에 문제가 없음에 스스로 대견스러웠다.

여기까지였으면 ‘개(Dog)’카테고리에 이 글을 넣었을 거다.

문제는 정확히 2013년 2월 8일 발생했다. 스크립트는 1월 25일부터 매일 문제없이 돌고 있었다. 2월 8일까지는 그랬던 거다. 2월 8일 새벽에…, 스크립트는 모든 로그를 지워버렸다. 젠장! 이 한마디는 결국, 이 포스트가 ‘발(Foot)’카테고리에 들어가 되었음을 의미한다.

난, 이런 경우 심한 자괴감에 빠져든다. 물론, 남들 모르게.

역시나 날짜를 계산하는 부분이 썩어있었다. 날짜는 연월일을 포함한 8자리 문자열로 구성되는데, 90일 이전의 날짜를 구하기 위해서 이를 연, 월, 일로 분리한다. 이를 Batch파일에서는 다음과 같이 처리한다.

SET /A "year=%from:~0,4%"
SET /A "month=%from:~4,2"
SET /A "day=%from:~6,2"

짐작하듯이, 8자리 문자열을 4/2/2 분할하여 이를 숫자로 각 변수에 저장한다. /A옵션은 문자열을 숫자로 인식하도록 한다. 예를 들어 문자열 04는 숫자로 인식되어 4로 저장되게 된다. 물론, Batch에는 타입이 존재하지 않으므로, 4는 다시금 문자열로 다룰 수 있다.

이때, 모르고 넘어가면 안될 결정적인 부분이 있는데, 내가 놓친 것이 이것이다.

Batch파일에서 문자열을 숫자로 변환할 때, 문자열이 0으로 시작하면 이는 무조건 8진수로 인식한다. 따라서, 문자열 08은 유효하지 않은 숫자가 된다. 8진수는 0~7의 숫자만 사용하니깐! 그래서 2월 8일 문제가 생긴 거다. 중간에 에러가 난 거지. 그래서 잘못된 날짜를 갖고 현재날짜와 비교해서 로그를 지우는 바람에, 모든 로그를 몽땅 지워버린 거라고.

수정 후 위 코드는 다음과 같이 바뀌었다.

SET /A "year=%from% / 10000"
SET /A "month=%from% %% 10000 / 100"
SET /A "day=%from% %% 100"

%%는 mod연산이다. 원 날짜 값을 문자열로써 파싱하지 않고, 그냥 산술 식으로 계산해서 변환시켰다. 진작 이럴걸 후회해도 이미 로그는 다 지워졌고, 고객문의는 처리되지 못할 뿐이었다.

덕분에 사건 다음 출근 일에는 하루 종일 입이 부르트도록 로그파일에 인공 호흡했다. 다행히 대부분은 살아나서 우울증은 걸리지 않았다. 그리고 결심했다. Windows Batch파일은 더 이상 사용하지 않으리!

WriteFile() 이 느리다구요?

새로운 프로젝트를 진행 중, 내부 망에서 진행한 테스트였음에도 반응속도가 더뎌지는 경우가 발생했다. 다행히 그 원인을 발견하는 데는 오랜 시간이 걸리지 않았는데, 다름아닌 사내 로깅(Logging) 라이브러리가 그 주범이었다. 안타깝게도, 라이브러리 담당자도 원인을 찾아주지는 못했다. 이 프로젝트는 철저히 혼자 만드는 것이었고, (존재한다면 반드시 사내 라이브러리를 사용해야 한다는 등의)기술적인 간섭 받지 않았기 때문에 로깅 라이브러리를 간단히 새로 만들었고, 결과는 만족스러웠다. 물론, 사내 라이브러리에 대한 불신은 좀 더 커졌다.

기존 프로젝트에도 꾸준히 문제되는 부분이 있었는데, 이 역시 다름아닌 로깅이었다. 이 프로젝트에서 사용되는 로깅 라이브러리는 위에서 언급했던 사내 로깅 라이브러리보다 더 오래된 것으로, 큰 문제가 없어서 그냥 사용하고 있었지만, 로그 양이 갑자기 증가하는 경우에는 문제가 발생했다. 결국, 기존의 소스코드를 약간 수정해서 성능이 개선되는 것을 확인했다.

수정한 부분은 기본 API인 WriteFile() 함수대신 fputs() 함수를 사용한 것인데, 선뜻 이해가 되지 않았던 것은, fputs() 함수도 결국 기본 API를 사용하므로, 성능이 느려지면 느려져야지, 빨리질 수 없다는 사실이었다. 그러나 분명히 성능이 개선되었으므로 소스를 커밋했다.

수정된 소스가 반영되어 세상에 나가기 이틀 전. 전체회의에서 ‘WriteFile() 함수가 fputs() 함수보다 느릴 수 없다’라는 이유로, 소스 적용이 취소되었고, 난 다시 원인을 찾기 시작했다.

내가 가장 의심했던 부분은, WriteFile()함수와 CreateFile() 함수에 붙어있는 무수한 플래그였다. 특히, WriteFile() 함수를 중심으로 살펴봤는데, 별다른 문제를 발견하지 못했다. 그렇지만, 역시나 대부분의 시간은 이 함수가 소모하고 있었다.

결국, 파일과 관련되지 않은 다른 모든 부분을 제거했다. 여전히 느렸고, 따라서 다른 부분은 문제가 아니라고 확신할 수 있었다.

그 전에, 내가 만든 로깅 라이브러리의 파일 입출력 부분을 모두 WriteFile() 함수를 사용하여 시간을 재어보니, 속도가 미미하게나마 더 빨라졌다. 당연히 이래야 하는 거잖아~!?

시간을 되돌려 다시 CreateFile() 함수와 WriteFile() 함수를 살펴보고 있었다. 그리고, 바로 전에 테스트 한 부분과의 차이가 눈에 들어왔다. 내가 만든 라이브러리에서 CreateFile() 함수를 사용할 때는 FILE_FLAG_WRITE_THROUGH를 사용하지 않았다.

다시 테스트해본 결과, 문제의 원인은 FILE_FLAG_WRITE_THROUGH이 확실했다. 구세주 MSDN과 StackOverflow의 글을 살펴본 결과 위 플래그의 역할은 다음과 같다.

C API를 사용하여 파일에 데이터를 기록하려고 할 때, 기본적으로 flush()함수를 호출하지 않으면 파일에 데이터가 기록되어있다는 것을 보장할 수 없다. 이는 쓰려는 데이터가 어딘가에 임시로 저장되어 있다는 것이며, 이로 인해 성능(속도)가 증가할 수 있다. FILE_FLAG_WRITE_THROUGH 플래그는 이 동작을 제어하며, 이 플래그를 사용하여 파일을 생성하였을 경우, WriteFile() 함수는 데이터를 버퍼링하지 않고 바로 디스크캐시로 보낸다. 그러나 디스크캐시로 보낸다는 것이 물리 디스크에 기록하는 것과 동일한 의미는 아니며, 이를 위해서는 추가로 FILE_FLAG_NO_BUFFERING 플래그를 사용해야 한다. 그러면, WriteFile() 함수의 종료 후, 데이터가 물리 디스크에 저장되었다고 보장할 수 있으며, 이에 대한 트레이드오프로 성능(속도)이 떨어지게 된다. 테스트결과, FILE_FLAG_WRITE_THROUGH 플래그만으로도 수 백배의 성능차이가 발생한다.

아마도 라이브러리 개발자는 버퍼링되는 로그가 파일에 남지 않게 될 것을 우려해서 FILE_FLAG_WRITE_THROUGH 플래그를 기본 옵션으로 설정해 둔 듯 하나, 엄밀하게는 FILE_FLAG_NO_BUFFERING 플래그도 함께 설정해야 했다.

그래도, 로깅의 성능은 상당히 크리티컬한 부분인 만큼, 그 의도는 납득하기 어려우며, 오히려 flush() 함수와 같은 인터페이스를 만드는 것이 옳지 않았을까 싶다.

결론: 네가 이해 못하는 코드는 있어도, 이해할 수 없는 현상이 발생하는 코드는 없다.

참고사이트 MSDN, StackOverflow

C++로 만든 라이브러리를 C#에서 사용하기 2

최근에 잠깐 C++/CLI에 대해서 살펴볼 일이 있어서, 무려 4년적에 적었던 ‘C++로 만든 라이브러리를 C#에서 사용하기‘를 살펴봤다.

당시와는 달리, 검색하면 관련된 내용을 찾기가 수월해진 것 같다. C++/CLI가 사용되는 경우가 늘었나 라고 생각했지만, VS2010에서 CLR프로젝트의 자동완성 기능은 제대로 동작하지 않는다. 스마트 기기류에 대응하느라 바쁜 Microsoft라 여기까지 신경 쓸 여력은 없나 보다.

일전의 설명만으론 부족한 부분을 보충하기위해 테스트코드를 만들었다.

Visual Studio 2010을 사용했으며, 솔루션의 구성은 다음과 같다.

LibAdder C++ 라이브러리
TestLibAdder LibAdder 테스트
LibAdderCS LibAdder의 인터페이스를 ManagedCode로 감싸주는 C++/CLI 라이브러리
TestLibAdderCS LibAdderCS 테스트

즉, LibAdder(C++) -> LibAdderCS(C++/CLI) -> TestLibAdderCS(C#) 이다.

예제파일은 github 에서 받을 수 있다.

참조사이트1(Managed String을 Unmanaged String으로 변환하기)
참조사이트2(델리게이트를 함수포인터로 변환하기)

코딩 인터뷰 완전 분석 215쪽 고난이도 연습문제 18.10(알고리즘 이벤트 마지막 문제)

이번 문제는 같은 길이의 단어로 구성되어있는 사전에서, 한 단어에서 다른 단어로 한 글자씩 바꿔가며 변형해 나가는 과정을 알아내는 것이 과제이다.

심화문제에서 최단/최장 경로를 구하는 것까지 나와서, 처음에는 사전 데이터를 그래프로 구성하려고 했다. 그러면 기존의 알고리즘을 이용해 경로를 찾아내기 수월할 테니. 그러나, 수천 개에 달하는 사전데이터를 그래프로 구성하는데 예상보다 시간이 많이 걸렸고, 이걸 최적화 해보겠다고 삽질하다가, 방법을 선회해야만 했다.

1. 미리 존재하는 파일로부터 사전을 구성한다. read_dic/1
2. 주어진 단어 A, B로 부터 중간단어를 하나씩 구한다. replace_word/3
3. 중간단어가 사전에 존재하는지 확인하고, 존재한다면 결과에 저장하고 그렇지 않으면 다음 순서의 중간단어를 구해서 2의 과정을 반복한다. find_path/6
4. 마지막의 중간단어가 B와 동일하면 경로를 구한 것이고, 그렇지 않으면 해당하는 경로가 존재하지 않는 것이다.
5. 단어 A, B의 길이가 다르면, word_length_mismatch를 반환하고, 4자리나 5자리 단어가 아니면, no_dictionary_exist를 반환한다. find_path/2
6. 각 사전은 word4.txt와 word5.txt로 존재한다고 가정한다.

출력예

74> insight:find_path("wean", "zein").
["wean",impossible]
75> insight:find_path("damp", "like").
["damp","dame","dime","dike","like"]
76> insight:find_path("damp", "damp2").
word_length_mismatched
77> insight:find_path("damp", "damp"). 
["damp"]

코딩 인터뷰 완전 분석 210쪽 17.3 변형 문제(알고리즘 코딩 이벤트 2주차 문제)

문제는 인사이트의 블로그에서 확인할 수 있듯이,
팩토리얼(!)의 결과값에서, 마지막에 연속되는 0의 개수와 0이 아닌 첫번째 숫자를 알아내는 것이다.

사실, 이전 문제를 낑낑대며 풀고, 다른 사람들의 풀이 법을 보니, 완전 삽질했구나 싶었다. 더구나 이미 얼랭을 사용한 사람이 있다니…OTL

그러나 이건 좀 쉽다! 한 15분 걸린 것 같다. Erlang도 조금 익숙해 졌고…

1. 팩토리얼식의 각 값을 순차적으로 곱한다. last/1
1.1 1의 과정 중, 마지막 0의 수를 세어 누적한다. countzero/1
1.2 1의 과정 중, 0이 아닌 마지막 수를 다음 곱셈에 사용하고, 나머지 숫자는 버린다.

출력예

Eshell V5.9.1  (abort with ^G)
1> c(insight2).
{ok,insight2}
2> insight2:last(1).
{0,1}
3> insight2:last(10).
{2,8}
4> insight2:last(100).
{24,6}
5> insight2:last(2012).
{490,2}
6> insight2:last(10000).
{2444,2}
7>