(번역) 최근 for 루프가 빨라진 것 같습니다.
본 포스트의 원문은 아래의 URL에서 확인하실 수 있습니다. 본 포스트는 원작자에게 동의를 구한 뒤 한국어로 번역하였습니다.
시작
“for 루프를 행렬 연산으로 변경하면 빨라집니다”
MATLAB을 사용해 본 적이 있는 분들은 한 번쯤 들어보셨을 것입니다. 최근 @t–shin 님이 게시한 글(Matlab이나 Python의 반복문은 느리다고 들었는데 진짜야?)에서도 for 루프를 사용하지 않는 것이 처리 속도가 빠르다는 결과가 보고되었습니다.
하지만 R2015b부터는 JIT(Just-in-Time) 컴파일 기능(공식 페이지)이 개선되어 상황이 변하고 있습니다.
그럼 얼마나 차이가 있는 걸까요? @t–shin 님의 글에 따르면, 버전에 따른 차이를 정리해 보았습니다. 연산 내용은 Matlab이나 Python의 반복문은 느리다고 들었는데 진짜야?를 확인해 주세요.
2020/10/16 추가
본 글에서 사용한 예제는 벡터화에 불리한 조건이어서 오해를 불러일으키고 있다는 지적을 받았습니다. 지적하신 대로 “벡터화는 for 문보다 느리다”라는 점이 아니라, 최근 for 루프가 빠르게 실행될 수 있도록 되었다는 점을 전달하는 내용임을 양해해 주십시오. FDTD 식은 conv2나 imfilter로 작성하는 것이 좋아보입니다. (참고 링크)
추가 끝
수행한 작업
코드는 마지막에 첨부 파일로 정리했지만, @t–shin 님의 파동 방정식 차분해법 코드를 약간 수정하여 차분 평가에 for 루프를 사용한 경우와 벡터화한 경우의 처리 시간을 다음 버전에서 조사했습니다. Windows 10 Intel(R) Core(TM) i7-8650U CPU @ 1.90GHz 기기를 사용하였습니다.
- R2015a
- R2015b
- R2017b
- R2019b
- R2020b (작성 당시의 최신 버전)
다음은 한 예입니다.
아니, 마지막에 있는 for 루프가 더 빠른데 말이야!
R2015a에서 R2020b까지 34배 빨라졌습니다.
R2015a 이후로 for 루프를 사용한 경우의 처리 시간이 상당히 개선되었으며, 이 경우에는 R2019b에서 역전(!)되었음을 알 수 있습니다.
JIT 컴파일의 성과가 나타나기 쉬운 루프였기 때문일 것으로 생각되지만, 코드를 꼼꼼히 다듬고 가독성을 희생시키지 않고 벡터화 처리해야 하는 필요성은 항상 그렇지 않을 수도 있습니다. 그러나 이는 한 예에 불과하며 모든 경우에 대해 for 루프로 구현하는 것이 더 빠르다는 것은 아니므로 유의해 주십시오.
숫자로 나타내면 다음과 같습니다 (단위는 초):
forLoop(평균) | vector(평균) | forLoop(std) | vector(std) | |
---|---|---|---|---|
R2015a | 9.21 | 0.35 | 0.34 | 0.015 |
R2015b | 1.59 | 0.38 | 0.07 | 0.034 |
R2017b | 0.48 | 0.33 | 0.016 | 0.032 |
R2019b | 0.27 | 0.34 | 0.023 | 0.039 |
R2020b | 0.27 | 0.33 | 0.020 | 0.045 |
사용한 코드는 모두 마지막에 첨부 파일로 정리되어 있으며, 비교한 for 루프 버전과 벡터 버전의 차이는 다음과 같습니다.
u2(2:xmesh-1,2:ymesh-1) = 2*u1(2:xmesh-1,2:ymesh-1)-u0(2:xmesh-1,2:ymesh-1)+c*c*dt*dt*(diff(u1(:,2:ymesh-1),2,1)/(dx*dx)+diff(u1(2:xmesh-1,:),2,2)/(dy*dy));
for j = 2:ymesh-1
for i = 2:xmesh-1
u2(i,j) = 2*u1(i,j)-u0(i,j) + c*c*dt*dt*((u1(i+1,j)-2*u1(i,j)+u1(i-1,j))/(dx*dx) +(u1(i,j+1)-2*u1(i,j)+u1(i,j-1))/(dy*dy) );
end
end
고려해야 할 사항
왜 벡터화가 권장되는 걸까요?
실제로 공식 페이지: 벡터화에서도 벡터화가 권장됩니다.
인터프리터 언어에서는 입력 인수의 유효성 검사 및 함수 호출의 오버헤드를 피할 수 없기 때문에 루프를 사용하여 함수 호출 횟수가 증가하면 처리 시간이 오래 걸립니다. 또한 요소 단위로 처리하는 것보다 행렬 단위로 계산하는 것이 행렬 연산 라이브러리 (MATLAB의 LAPACK, 공식 페이지)의 이점을 활용할 수 있기 때문입니다. (이 점에 대해서는 자세한 설명을 기다리고 있습니다.)
JIT (Just-in-Time) 컴파일?
MATLAB 코드는 실행 시 Just-In-Time 컴파일이 사용되지만, 특히 R2015b에서 JIT (Just-in-Time) 컴파일 기능 (공식 페이지)이 개선되었습니다. 결과적으로
- 함수 호출의 오버헤드 감소
- 객체 지향 처리 속도 향상
- 요소 단위 연산의 처리 속도 향상
등의 효과를 기대할 수 있습니다. 그러나 물론 함수나 스크립트의 첫 실행 시에는 두 번째 이후보다 시간이 더 걸린다는 점을 잊어서는 안됩니다.
스크립트 vs 함수
특정 작업을 실행할 때는 스크립트의 상태로 비교하는 경우가 많지만, 함수화하면 상황이 달라질 수 있습니다. 이번 경우에서는 함수화하면 for 루프 버전이 더 빨리 실행되는 경향이 있습니다.
비교 방법 상세
이번에 구체적으로 검증한 내용은 모두 마지막에 첨부 파일로 정리되어 있으므로 관심 있는 분들은 꼭 시도해보시기 바랍니다. 그래프 그리기 등은 R2020b에서 수행했습니다.
@t–shin 님의 글에서는 N = 400에서 진행했지만, 시간이 오래 걸리므로 비교를 N = 100과 N = 200에서 진행해 보았습니다.
- 함수 간 비교 (for 루프 vs 벡터 처리)
- 스크립트 간 비교 (for 루프 vs 벡터 처리)
구체적으로는 20번 처리 시간을 측정하고, 평균값과 표준 편차를 플롯합니다. N = 400에서는 다른 경향이 나타날 수 있습니다.
nchecks = 20;
T_forLoop = zeros(nchecks,1);
T_vector = zeros(nchecks,1);
for ii = 1:nchecks
tic
tmp1 = wave_forLoop_function(N);
T_forLoop(ii) = toc;
tic
tmp2 = wave_vector_function(N);
T_vector(ii) = toc;
end
vNumber = version('-release');
save(vNumber+"N"+string(N)+"_function.mat","T_forLoop", "T_vector");
이런 식의 함수 (speedcheck.m
, 첨부 파일 참조)를 작성했습니다.
각 버전을 열어 실행하는 것은 번거롭고, 처리 시간만으로 순수하게 비교할 수 있도록 명령 프롬프트에서 다음 명령을 사용하여 각 버전에서 실행했습니다.
> "C:\Program Files\MATLAB\R2020b\bin\matlab" -batch "speedcheck(200)"
> "C:\Program Files\MATLAB\R2020b\bin\matlab" -batch "speedcheck(100)"
> "C:\Program Files\MATLAB\R2017b\bin\matlab" -nodesktop -nojvm -nosplash -r "speedcheck(200)"
.
.
R2017b 이전은 -batch 옵션이 없으므로 명령이 약간 다르지만, 이를 통해 각 버전마다 결과가 mat 파일에 저장되어 plotResuts.m
(첨부 파일 참조)에서 플롯한 결과는 다음과 같습니다.
분할 수 N = 200으로 하여 비교
함수로 실행하면 R2019b즈음에서 역전 현상이 발생하네요.
분할 수 N = 100으로 하여 비교
N = 200의 케이스와 비교해보면 for 루프의 케이스가 유리합니다.
요약
for 루프도 그렇게 나쁘지 않네요.
가독성을 희생하지 않고까지 피해야 할 필요는 없는 것 같습니다. 다만, 반복해서 말하지만, 이는 단지 한 예시일 뿐이며 모든 경우에 for 루프로 구현하는 것이 더 빠르다는 것을 의미하지는 않으니 주의해주십시오.
만약 더 느린 다른 경우가 벡터화된 경우가 있다면 꼭 알려주세요!
첨부: 사용한 함수 평가
speedcheck.m
계산 처리 시간 측정용 함수입니다. for 루프 버전과 벡터화 버전을 각각 함수와 스크립트로 20번씩 측정합니다. 플롯에는 평균값과 표준 편차를 사용합니다. 결과는 각 버전별로 다른 이름의 mat 파일에 저장됩니다.
function speedcheck(N)
% 처리 시간 계산용 함수 (N은 그리드 수)
% 20회 측정하여 평균값과 표준편차를 사용합니다.
nchecks = 20;
% 스크립트로 실행
T_forLoop = zeros(nchecks,1);
T_vector = zeros(nchecks,1);
for ii = 1:nchecks
tic
wave_forLoop_script;
T_forLoop(ii) = toc;
tic
wave_vector_script;
T_vector(ii) = toc;
end
% 버전 마다 결과 저장
vNumber = version('-release');
save([vNumber,'N',num2str(N),'_script.mat'],'T_forLoop', 'T_vector');
% 함수로 실행
T_forLoop = zeros(nchecks,1);
T_vector = zeros(nchecks,1);
for ii = 1:nchecks
tic
tmp1 = wave_forLoop_function(N);
T_forLoop(ii) = toc;
tic
tmp2 = wave_vector_function(N);
T_vector(ii) = toc;
end
% 버전 마다 결과 저장
vNumber = version('-release');
save([vNumber,'N',num2str(N),'_function.mat'],'T_forLoop', 'T_vector');
wave_forLoop_script.m
@t–shin 님이 게시한 글 Matlab이나 Python의 반복문은 느리다고 들었는데 진짜야? 에 게재된 코드 (for 루프 버전)입니다. 분할 수를 자유롭게 설정할 수 있도록 변경했습니다.
c = 1.0;
xmin = 0.;
ymin = 0.;
xmax = 1.;
ymax = 1.; % 계산 영역은 [0,1],[0,1]
xmesh = N;
ymesh = N; % 분할수는 x,y축에 N개씩
dx = (xmax-xmin)/xmesh;
dy = (ymax-ymin)/ymesh;
dt = 0.2*dx/c;
u0 = zeros(xmesh,ymesh); % u^{n-1}
u1 = zeros(xmesh,ymesh); % u^n
u2 = zeros(xmesh,ymesh); % u^{n+1}
idx1 = round(0.25*N);
idx2 = round(0.75*N);
u1(idx1:idx2,idx1:idx2)=1e-6;% 일정 영역에 초기 속도를 부여
x = xmin+dx/2:dx:xmax-dx/2;
y = ymin+dy/2:dy:ymax-dy/2;
t=0.;
% tic %tic toc으로 시간을 측정할 수 있어서 편리하다
while t<1.0
for j = 2:ymesh-1
for i = 2:xmesh-1
u2(i,j) = 2*u1(i,j)-u0(i,j) + c*c*dt*dt*((u1(i+1,j)-2*u1(i,j)+u1(i-1,j))/(dx*dx) +(u1(i,j+1)-2*u1(i,j)+u1(i,j-1))/(dy*dy) );
end
end
u0=u1;
u1=u2;
t = t+dt;
% 디리클레 조건을 부여
for i=1:xmesh
u1(i,1)=0.;
u1(i,ymesh)=0.;
end
for j=1:ymesh
u1(1,j)=0.;
u1(xmesh,j)=0.;
end
end
wave_vector_script.m
@t–shin 님이 게시한 글 Matlab이나 Python의 반복문은 느리다고 들었는데 진짜야? 에 게재된 코드 (for 루프 버전)입니다. 분할 수를 자유롭게 설정할 수 있도록 변경했습니다.
c = 1.0;
xmin = 0.;
ymin = 0.;
xmax = 1.;
ymax = 1.; % 계산 영역은 [0,1],[0,1]
xmesh = N;
ymesh = N; % 분할수는 x,y축에 N개씩
dx = (xmax-xmin)/xmesh;
dy = (ymax-ymin)/ymesh;
dt = 0.2*dx/c;
u0 = zeros(xmesh,ymesh); % u^{n-1}
u1 = zeros(xmesh,ymesh); % u^n
u2 = zeros(xmesh,ymesh); % u^{n+1}
idx1 = round(0.25*N);
idx2 = round(0.75*N);
u1(idx1:idx2,idx1:idx2)=1e-6;% 일정 영역에 초기 속도를 부여
x = xmin+dx/2:dx:xmax-dx/2;
y = ymin+dy/2:dy:ymax-dy/2;
t=0.;
% tic %tic toc으로 시간을 측정할 수 있어서 편리하다
while t<1.0
u2(2:xmesh-1,2:ymesh-1) = 2*u1(2:xmesh-1,2:ymesh-1)-u0(2:xmesh-1,2:ymesh-1)+c*c*dt*dt*(diff(u1(:,2:ymesh-1),2,1)/(dx*dx)+diff(u1(2:xmesh-1,:),2,2)/(dy*dy));
u0=u1;
u1=u2;
t = t+dt;
% 디리클레 조건을 부여
u1(:,1)=0.;
u1(:,ymesh)=0.;
u1(1,:)=0.;
u1(xmesh,:)=0.;
end
wave_forLoop_function.m
아래는 함수 버전입니다. 가독성을 위해 내부에서 스크립트를 호출하고 있습니다.
function u1 = wave_forLoop_function(N)
wave_forLoop_script;
end
wave_vector_function.m
아래는 함수 버전입니다. 가독성을 위해 내부에서 스크립트를 호출하고 있습니다.
function u1 = wave_vector_function(N)
wave_vector_script;
end
plotResults.m
결과를 그려주기 위한 스크립트
nversion = ["2015a","2015b","2017b","2019b","2020b"];
T_script100 = zeros(5,4);
T_function100 = zeros(5,4);
T_script200 = zeros(5,4);
T_function200 = zeros(5,4);
for ii=1:5
load(nversion(ii) + "N100_script.mat");
T_script100(ii,1) = mean(T_forLoop);
T_script100(ii,2) = mean(T_vector);
T_script100(ii,3) = std(T_forLoop);
T_script100(ii,4) = std(T_vector);
load(nversion(ii) + "N100_function.mat");
T_function100(ii,1) = mean(T_forLoop);
T_function100(ii,2) = mean(T_vector);
T_function100(ii,3) = std(T_forLoop);
T_function100(ii,4) = std(T_vector);
load(nversion(ii) + "N200_script.mat");
T_script200(ii,1) = mean(T_forLoop);
T_script200(ii,2) = mean(T_vector);
T_script200(ii,3) = std(T_forLoop);
T_script200(ii,4) = std(T_vector);
load(nversion(ii) + "N200_function.mat");
T_function200(ii,1) = mean(T_forLoop);
T_function200(ii,2) = mean(T_vector);
T_function200(ii,3) = std(T_forLoop);
T_function200(ii,4) = std(T_vector);
end
array2table(T_function200,'VariableNames',["forLoop(mean)","vector(mean)","forLoop(std)","vector(std)"])
%%
hf = figure(1);
hf = plotResults(hf,T_function100,"함수 실행(N=100)");
hf = figure(2);
hf = plotResults(hf,T_function200,"함수 실행(N=200)");
hf = figure(3);
hf = plotResults(hf,T_script100,"스크립트 실행(N=100)");
hf = figure(4);
hf = plotResults(hf,T_script200,"스크립트 실행(N=200)");
function hf = plotResults(hf,data,figureTitle)
nversion = ["R2015a","R2015b","R2017b","R2019b","R2020b"];
errorbar(data(:,1),data(:,3),'LineWidth',2,'CapSize',10)
hold on
errorbar(data(:,2),data(:,4),'LineWidth',2,'CapSize',10)
hold off
legend('forLoop','vector');
title(figureTitle)
set(gca,'XTick',[1,2,3,4,5]);
set(gca,'XTickLabel',nversion)
ylabel('처리시간 (초)');
xlabel('MATLAB 버전');
set(gca,'FontSize',14);
grid on
end