PHP로 짠 형태소 분석기 프로그램을 C++로 고친 이야기

Posted by 적분 ∫2tdt=t²+c
2016.11.14 03:23 프로그래밍

몇 개월 전에 살아있는 라틴어 홈페이지에 형태소 분석기 서비스가 추가되었습니다. 원리는 뭐 복잡한거 없이 지금까지 구축한 라틴어 코퍼스에서 격별, 성별, 수별 전이확률(어떤 격 뒤에는 어떤 격이 올 확률이 높은가, 어떤 동사 주변에는 어떤 격이 올 확률이 높은가, 어떤 전치사 뒤에는 어떤 격이 올 확률이 높은가 등)을 조건부 확률 계산식을 이용해서 계산하는 겁니다. 그래서 가장 확률 높은 성/수/격 요소를 추천해주는 거죠.

코드가 복잡하지 않다고 생각해서 그 때 PHP로 약 340줄 코딩해서 형태소 분석기를 작성했었습니다. (각 상태별 전이확률은 MariaDB에 저장되어있고, 쿼리를 날려서 가져오는 형태)

문제는 이렇게 후딱 코딩을 끝내고 나니 1. 굉장히 느리고, 2. 굉장히 메모리를 많이 먹었습니다. 약 30 단어 이상만 되어도 실행시간은 몇 백초가 걸리고, 메모리 초과로 사망하기 일쑤였죠. 새로운 코퍼스를 확충하려고 하다보니 자꾸 죽어버리는 PHP 프로그램이 짜증나서 C++로 짜기로 결심했습니다.


마이크로소프트가 빛을 내려주다

사실 리눅스 서버용으로 C++ 코딩을 해본적은 한 번도 없습니다. IDE가 다 해주는 세상에서 자랐기때문에 커맨드로 gcc 같은거 실행해서 컴파일하고 링킹하고 이런거 해본적도 없고 할줄도 몰라요. 검색검색해서 어찌한다면 할수야 있겠지만, 그러기는 너무 싫어서...

검색을 해보니 비주얼스튜디오2015에 리눅스용 원격 컴파일/디버깅을 도와주는 툴이 있었습니다. (마솔루야 마소 만세)

Visual C++ for Linux Developmenthttps://visualstudiogallery.msdn.microsoft.com/725025cf-7067-45c2-8d01-1e0fd359ae6e

이런 멋진 확장기능을 주시다니 감사 또 감사하며 바로 설치했습니다. 이 기능만 있다고 바로 리눅스 서버에 연결해서 개발을 시작할 수 있는건 아니고, 서버쪽에서도 세팅을 해줘야합니다. 해줘야할건 많진 않고 몇개만 설치하면 됩니다.

openssh-server, g++, gdb, gdbserver 요 4개입니다.

>> sudo apt-get install openssh-server g++ gdb gdbserver

이렇게 설치하면 되겠죠.

세팅이 끝났으면 페이지에 나와 있는것처럼 리눅스용 프로젝트를 만드시고, 서버 접속을 위해 계정, 비번 입력하고 개발을 시작하면 됩니다.


외부 라이브러리 사용하기

MariaDB에 확률 데이터가 들어있기때문에 형태소 분석기 프로그램을 위해서는 디비에 연결하는 MariaDB C Connector를 사용해야합니다. 친절히도 보통 이런 라이브러리는 리눅스용은 기본적으로 배포해주기 때문에 홈페이지에서 다운받아 설치해주면 됩니다. 리눅스 서버에 원격으로 연결해서 개발하고 컴파일하는 것이므로, 라이브러리들도 당연히 리눅스 서버에 설치되어야 합니다. (처음엔 자동으로 라이브러리도 리눅스 쪽으로 전송해주는줄 알고 안 깔았다가 한참 헤맸네요)

라이브러리는 보통 /usr/lib 폴더 안으로 들어가고, 헤더 파일들은 /usr/include 안으로 들어간다는거 아시죠? 라이브러리 설치가 끝났다면 흔히 Visual Studio에서 헤더 파일 디렉토리 추가하듯이 추가 디렉토리를 설정해주면 됩니다.

노란 형광펜 칠해진거처럼 리눅스 경로명을 잘 입력해주세요. 헤더 디렉토리를 추가했으니 다음은 링커 설정에서 추가 라이브러리를 넣어줄 차례죠.


라이브러리 의존을 추가해줍니다. 당연한건데 며칠전까지 몰랐던거는 lib~~.a 또는 lib~~.so 이름을 라이브러리를 추가할때 앞의 lib와 뒤의 확장자는 안 적는겁니다. .a는 정적 링크파일이고 (VC++의 정적 lib와 유사), .so는 동적 링크파일(VC++에서 동적 lib + dll 파일)입니다. 처음엔 mariadb 하나만 적으면 될줄 알았는데, mariadb 라이브러리가 참조하는 기타 라이브러리가 내부에 있을 경우 걔네들도 다 적어줘야합니다.

만약 필요한 라이브러리가 누락되었을 경우 

collect2: ld returned 1 exit

와 같은 에러 메세지가 뜨고, 어떤 심볼들이 누락됐는지 뜨니깐 해당 메세지를 구글에 검색해보면 뭘 추가로 링킹해야 될지 알수 있습니다. 만약 다른 에러 메세지는 없고 달랑 위의 메세지만 뜬다면 Library Dependencies 항목의 라이브러리 이름을 잘못 적었거나, 해당 라이브러리를 리눅스 서버에서 찾지 못한 것일 수 있어요. 입력된 값을 잘 확인해보고 /usr/lib에 해당 파일이 있는지도 확인해서 잘 수정하셔야합니다.


끔찍한 Segfault와 디버깅

디버깅도 그저 비주얼 스튜디오 이용하던 때처럼 브레이크 포인트 찍고 하시면 됩니다. 다만 서버와 통신하며 디버깅하는 것으므로 watch창에 너무 많은 변수를 찍어두면 해당 값을 일일히 가져오느라 느려집니다. 같은 코드가 윈도우용으로 컴파일했을땐 잘 돌아가는데, 리눅스용에서는 자꾸  세그먼테이션 폴트를 일으키며 죽어서 해가 뜨고 하늘이 밝아질때까지 생고생을 했었는데요... (솔직히 이때는 걍 리눅스용 크로스 컴파일 버리고, Write once, Run everywhere하는 자바로 짜야겠다는 유혹이 들기도 했었지만)

나중에 알고보니 리눅스 실행시 넘기는 인자가 잘못 입력되어서 발생하는거라는 것을 깨닫고 허탈하게 웃고 말았습니다. 역시 잘 짜인 C코드는 어디서 컴파일해도 잘 돌아가는 거였습니다.

사실 PHP코드를 C++로 옮기는데는 별로 오래걸리진 않았는데, 이 디버깅에서 하루밤을 꼴딱 새워버렸습니다.


C++로 옮긴 결과...!

340줄 짜리 PHP 프로그램을 C++로 옮기면서 300줄짜리 mysql 함수 래퍼 클래스 파일과 약 300줄짜리 알고리즘 등 해서 전체 700여줄로 불어났습니다. (헤더 파일도 합치고 하면 더 길텐데.) 다행히도 DB호출을 위한 mysql 함수 래퍼 클래스는 예전에 작성해두었기때문에 그대로 재활용할 수 있었고, 새로 짠 코드는 사실 300줄 정도로 PHP와 비슷한 양이네요. (이게 다 C++11에서 도입된 편리한 문법들 덕분입니다.) 태생이 C 계통이라 그런지 잘 짜여지고 타입이 훤히 들여다보이는 코드를 보니 안정감도 느껴지고 코딩할 맛도 더 나구요

코드 구조가 안정적으로 잡히니깐 최적화할 부분도 보이고, 알고리즘 교체도 원활해서 타이트하게 최적화도 수행할 수 있었습니다. 자 그러면 이제 Before vs After를 비교할 시간이죠. 하루를 꼴딱 새운 일이 얼마나 가치있었는지 평가해봅니다.

Before(PHP 코드)After (C++코드)

30단어 문장

(10개 후보)

메모리

약 20MB

10MB 이내

응답 시간

1930ms290ms

40단어 문장

(100개 후보)

메모리

약 600MB

20MB 이내

응답 시간8900ms

840ms

C++ 프로그램 메모리 사용량을 측정하려고 했는데 금방 종료되어서 정확히 알길이 없네요. PHP7이 구리다고 욕하려고한건 아니고, PHP에서는 사용할 수 있는 자료구조도 제한되고 디테일한 부분에서 손대기가 어려운게 저렇게 메모리 처묵처묵이라는 결과로 나온듯합니다. 응답시간 차이는 알고리즘 최적화 차이(c++버전에서는 분할정복을 완전하게 적용해서 시간복잡도를 O(n^2)에서 O(nlog)에 가깝게 낮췄거든요)도 한몫 할듯싶구요. 응답시간에는 서버와의 연결시간도 들어가니 그것도 제외하면 C++쪽이 10배 이상 빠르다고 볼수 있겠습니다.


제일 신나는 점은 이제 리눅스용 C++ 개발도 두렵지 않아졌다는 겁니다. PHP로 짜다가 더럽게 복잡하거나 느린게 있으면 C++로 개발해서 붙이면 해결!!이 되니깐, 굳이 지저분하게 PHP로 복잡한 시스템을 짜지 않아도 된다는 가능성을 보았습니다.

저작자 표시 비영리 동일 조건 변경 허락
신고
크리에이티브 커먼즈 라이선스
Creative Commons License
Tags
이 댓글을 비밀 댓글로

티스토리 툴바