t

10분 완성 MATLAB MEX

MATLAB은 행렬연산에 최적화된 스크립트 언어이며, machine learning 알고리즘의 많은 요소들이 행렬연산 및 component-wise연산으로 표현 가능하기 때문에 ML 엔지니어들이 애용하는 언어 중 하나이다. 최근에 유행하는 deep learning이 MATLAB과 같은 high-level 스크립트 언어로 작성하기 아주 좋은 예이다. Deep learning을 C와 같은 native 언어로 구현하는 경우는 GPU 등의 외부 장비를 사용할 때의 성능을 극대화 하기 위함이며, single machine에서 온전히 CPU만 사용하는 경우에는 MATLAB, python, R과 같은 스크립트 언어로 작성해도 아주 빠른 시간 내에 수행 가능하다.

가끔씩 if와 같은 분기문 혹은 random access가 자주 발생하는 알고리즘이 있는데, prototyping만 하는 경우라면 MATLAB으로도 무리 없이 구현 가능하다. 하지만 수행 시간에 초점을 맞추는 경우에는 이러한 취약점을 반드시 짚고 넘어가야 하는데, MATLAB에서는 이를 위한 솔루션으로 MEX라는 framework을 제공한다. MATLAB에서 low-level 코드를 호출한 뒤에 그 결과를 얻어오는 아이디어이다. C와 Fortran코드와의 결합을 지원하며, 오늘은 C코드에 MATLAB wrapper를 씌우는 방법을 소개한다. 설치과정만 제외하면 정말 10분ㅋ안에 감 잡을 수 있다. 아주 쉽다.

개인적으로 MATLAB은 윈도우 환경에 더 최적화 되어 있다는 생각이 든다. Ubuntu에서는 gcc 버전에 예민한데, R2013a 까지만 해도 gcc 4.4.x 버전까지 밖에 지원을 하지 않아서 (참조) 일부러 gcc를 downgrade해서 컴파일 해야했다. 항상 Ubuntu 기본 gcc 버전보다 늦다가, 다행히 R2013b 부터는 4.7.x 을 지원한다. R2013a 이전의 MATLAB을 사용하시는 분들은 (여기)를 참조하여 컴파일러 설정을 변경하면 된다.

윈도우 유저는 Visual Studio 2008이상을 설치하여 X64 컴파일러를 확보해야 한다. VS를 기본 설정으로 설치했다면 X64 컴파일러가 없는데, 이러한 경우에는 다시 설치파일을 실행한 뒤 아래와 같이 기능 추가/제거로 들어가서 X64 컴파일러 및 도구에 체크하여 설치를 마무리 할 수 있다.

w660p w660p

윈도우 유저가 R2014이상을 사용하는 경우 별도의 설정은 필요 없고, 그 이전 버전의 MATLAB인 경우에는 MATLAB shell에서 “mex -setup”을 입력하여 Microsoft Visual C++ 2008 SP1 혹은 최신버전을 선택하면 된다. Ubuntu도 아마 디폴트로 하면 될텐데 오래 전에 설치한 거라 기억이 가물가물하다.

아래와 같은 예제 코드를 사용하려고 한다. 설명은 뒤에서 하고 일단 붙여 넣어서 sum_mex.c로 저장하자.

#include "mex.h"

void mexFunction( int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[] )
{
  int i, m, n;
  double *input, *output;
 
  m = (int) mxGetM(prhs[0]);
  n = (int) mxGetN(prhs[0]);
  input = (double *) mxGetPr(prhs[0]);
 
  mexPrintf("%d %d\n", m, n);
 
  plhs[0] = mxCreateDoubleMatrix(1, 1, mxREAL);
  output = (double *) mxGetPr(plhs[0]);
 
  for ( i=0; i<m; i++ )
    output[0] += input[i];
}

컴파일을 위해 MATLAB에서 아래와 같은 명령어를 사용한다.

mex -O -largeArrayDims -O sum_mex.c

-O는 optimization flag인데 기본값은 -O2로 되어있다. 변경을 원하는 경우에는 각 MATLAB 환경 별로 mexopts.sh 파일을 찾아서 수정하면 된다. -largeArrayDims는 64bit OS에서는 반드시 주어져야 하는 flag이다. 컴파일이 완료되어 sum_mex.mexw64 (혹은 .mexa64) 바이너리 파일이 생성되면, 파일명 sum_mex가 함수명이 되어 MATLAB에서 호출 가능하다. 테스트를 위해 아래와 같은 코드를 수행해보자.

n = 10;
x = rand(1000, n);
tic;
for i = 1:n
  sum(x(:,i));
end
toc;
tic;
for i = 1:n
  sum_mex(x(:,i));
end
toc;

이 다음 섹션에서 설명하겠지만, 예제 MEX 파일은 sum 함수와 동일한 기능을 한다. 수행 시간은 아마도 MATLAB built-in 함수가 근소하게 앞설 것이다. 아무래도 기본적인 기능들은 MATLAB 내장 함수를 이기는 것이 쉬운 일은 아니다. 앞서 말한 것 처럼, 분기문과 같은 MATLAB 코드의 취약한 요소들을 만났을 때, 본 예제에 기반해서 MEX로 대체하면 성능을 대폭 끌어올릴 수 있을 것이다!

#include "mex.h"

MATLAB API 함수들을 선언하기 위해 mex.h를 include한다.

void mexFunction( int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[] )

C의 main() 함수와 같은 시작 함수이다. nlhs는 left-hand side 즉 output arguments의 개수이다. plhs 는 output arguments의 포인터 array이다. nrhs와 prhs도 마찬가지로 right-hand side (input) arguments의 개수와 포인터 array를 의미한다. 본 예제에서는 sum_mex(x(:,i))의 형태로 호출하므로, input, output이 모두 하나이다. 즉, nlhs=nrhs=1 이며, input vector는 prhs[0]으로 접근하고, output은 plhs[0]에 assign하면 된다.

m = (int) mxGetM(prhs[0]);
n = (int) mxGetN(prhs[0]);

prhs[0]이 input vector이며, 그것의 행과 열의 개수를 각각 m과 n으로 assign한다.

input = (double *) mxGetPr(prhs[0]);

input vector를 access하기 위해 mxGetPr함수를 사용한다. 이제 input[i]로 MATLAB input vector의 i번째 값을 접근할 수 있다.

mexPrintf("%d %d\n", m, n);

input vector의 행 열 크기를 출력한다. C의 printf함수와 usage는 같다. 예제에서는 1000 1이 출력될 것이다.

plhs[0] = mxCreateDoubleMatrix(1, 1, mxREAL);
output = (double *) mxGetPr(plhs[0]);

C의 dynamic memory allocation처럼 output을 위한 메모리를 먼저 할당해야 한다. 예제의 return 값은 스칼라이므로 1 x 1 행렬을 선언하였다. 그리고 이 변수를 위한 포인터 값을 output에 할당한다.

for ( i=0; i<m; i++ )
  output[0] += input[i];

m개의 input element를 모두 output[0]에 더한다. 즉, MATLAB의 sum과 같은 기능이다. 아마도 Intel 상용 컴파일러로 컴파일한다면 MATLAB built-in 보다 조금 빠르지 않을까 생각된다.

비슷한 예제를 MathWorks 공식 페이지에서도 찾아볼 수 있고 (링크), MEX library 함수들이 더 궁금하다면 아래 두 개 링크를 찾아가면 된다.

C/C++ Matrix Library API

MEX Library


comments powered by Disqus