본문 바로가기
공부/프로그래밍

[Julia] 대용량 데이터 효율적으로 처리하기: MATLAB과 Julia 비교

by AlderaminH 2024. 7. 11.

Fortran, Matlab, 그리고 Julia는 대표적인 column-major order 프로그래밍 언어들이다.

이 중 Matlab과 Julia의 배열 쌓는 법과 계산 처리 과정에서 메모리 접근법에 대한 차이가 있어서 결과를 적어보자 한다.

 

친숙한 Matlab 부터 코드를 작성해본다.

코드 1

Matlab Code

function ConcatTest1()
    % 예시 데이터 생성
    X = 0:0.001:200;
    Y = 0:0.001:200;
    Z = 0:0.001:200;

    N = length(X);

    H = [];
    V = [];

    disp('-----cat-----');
    
    % 수평 결합 (H가 3x200000)
    tic;
    for i = 1:N
        H = [H, [X(i); Y(i); Z(i)]];
    end
    time_h = toc;
    fprintf('Horizontal concatenation time: %.6f seconds\n', time_h);

    % 수직 결합 (V가 200000x3)
    tic;
    for i = 1:N
        V = [V; [X(i), Y(i), Z(i)]];
    end
    time_v = toc;
    fprintf('Vertical concatenation time: %.6f seconds\n', time_v);

    disp(size(H));
    disp(size(V));

    HT = H';
    VT = V';

    disp('-----mean-----');
    
    % 평균 계산
    tic;
    mean_H = mean(H, 2);
    time_mean_H = toc;
    fprintf('Mean calculation time (H, dims=2): %.6f micro seconds\n', time_mean_H * 1e+6);
    
    tic;
    mean_V = mean(V, 1);
    time_mean_V = toc;
    fprintf('Mean calculation time (V, dims=1): %.6f micro seconds\n', time_mean_V * 1e+6);
    
    tic;
    mean_HT = mean(HT, 1);
    time_mean_HT = toc;
    fprintf('Mean calculation time (HT, dims=1): %.6f micro seconds\n', time_mean_HT * 1e+6);
    
    tic;
    mean_VT = mean(VT, 2);
    time_mean_VT = toc;
    fprintf('Mean calculation time (VT, dims=2): %.6f micro seconds\n', time_mean_VT * 1e+6);

    disp('-----size check-----');
    fprintf('H size: [%d, %d]\n', size(mean_H));
    fprintf('V size: [%d, %d]\n', size(mean_V));
    fprintf('HT size: [%d, %d]\n', size(mean_HT));
    fprintf('VT size: [%d, %d]\n', size(mean_VT));
    
    disp('-----value check-----');
    disp(mean_H)
    disp(mean_V)
    disp(mean_HT)
    disp(mean_VT)
end

 

결과 1

>> ConcatTest1

-----cat-----
Horizontal concatenation time: 0.118601 seconds
Vertical concatenation time: 134.157211 seconds

           3      200001

      200001           3

-----mean-----
Mean calculation time (H, dims=2): 689.500000 micro seconds
Mean calculation time (V, dims=1): 190.000000 micro seconds
Mean calculation time (HT, dims=1): 180.100000 micro seconds [v]
Mean calculation time (VT, dims=2): 583.900000 micro seconds

-----size check-----
H size: [3, 1]
V size: [1, 3]
HT size: [1, 3]
VT size: [3, 1]

-----value check-----
   100
   100
   100

   100   100   100

   100   100   100

   100
   100
   100

 

ConcatTest1 함수와 그 결과이다.

코드는 3 x 1 벡터의 수평으로 결합 속도, 1 x 3 벡터의 수직으로 결합 속도 , 그리고 결합된 행렬들의 mean 계산 속도를 측정한 것이다.

 

실행 결과, 똑같은 데이터여도 (3 x 1 행렬을 수평으로 결합하는 것)이 (1 x 3 행렬을 수직으로 결합하는 것)보다 1131배 빨랐다.

 

그렇다면, 수평으로 데이터를 쌓고나서 그대로 mean을 하는 것이 옳냐? 아니다.

데이터를 쌓는 것은 수평이 빨랐지만 mean() 계산동안 column-major에 의해서 메모리에 저장된 데이터를 접근할때 수평끼리 있는 데이터는 cache hit rate가 떨어지게 된다. 반대로 수직끼리 있는 데이터여야 cache hit rate가 상승하게 된다. 

 

이는 (H, dims=2) 와 (HT, dims=1) 의 mean 결과로 확인할 수 있다. 

수평으로 쌓은 데이터 행렬인 H를 그대로 mean() 하면 실행속도는 689.5 micro sec 였고, H를 transpose한 HT 행렬의 mean() 실행 속도는 180.1 micro sec 였다. mean 계산에서 데이터 접근을 위해 column major에 맞게 전치 행렬한 결과가 3.828배 더 빠른 것이다. 

 

그리고 여기서 눈 여겨 보아야할 결과는 행렬 V와 행렬 HT의 mean 실행 속도 차이가 미미하다는 것이다. 이 결과를 기억하자. 내가 오늘 말하고 싶은 Matlab과 Julia의 차이이다.

 

코드 2

Matlab Code

function ConcatTest2()
    % 예시 데이터 생성
    X = 0:0.001:200;
    Y = 0:0.001:200;
    Z = 0:0.001:200;

    N = length(X);

    H = zeros(3, 0);
    V = zeros(0, 3);

    disp('-----cat-----');
    
    % 수평 결합 (H가 3x200000)
    tic;
    for i = 1:N
        r = [X(i); Y(i); Z(i)];
        H = horzcat(H, r);
    end
    time_h = toc;
    fprintf('Horizontal concatenation time: %.6f seconds\n', time_h);

    % 수직 결합 (V가 200000x3)
    tic;
    for i = 1:N
        r = [X(i), Y(i), Z(i)];
        V = vertcat(V, r);
    end
    time_v = toc;
    fprintf('Vertical concatenation time: %.6f seconds\n', time_v);

    disp(size(H));
    disp(size(V));

    HT = H';
    VT = V';

    disp('-----mean-----');
    
    % 평균 계산
    tic;
    mean_H = mean(H, 2);
    time_mean_H = toc;
    fprintf('Mean calculation time (H, dims=2): %.6f micro seconds\n', time_mean_H * 1e+6);
    
    tic;
    mean_V = mean(V, 1);
    time_mean_V = toc;
    fprintf('Mean calculation time (V, dims=1): %.6f micro seconds\n', time_mean_V * 1e+6);
    
    tic;
    mean_HT = mean(HT, 1);
    time_mean_HT = toc;
    fprintf('Mean calculation time (HT, dims=1): %.6f micro seconds\n', time_mean_HT * 1e+6);
    
    tic;
    mean_VT = mean(VT, 2);
    time_mean_VT = toc;
    fprintf('Mean calculation time (VT, dims=2): %.6f micro seconds\n', time_mean_VT * 1e+6);

    disp('-----size check-----');
    fprintf('H size: [%d, %d]\n', size(mean_H));
    fprintf('V size: [%d, %d]\n', size(mean_V));
    fprintf('HT size: [%d, %d]\n', size(mean_HT));
    fprintf('VT size: [%d, %d]\n', size(mean_VT));

    disp('-----value check-----');
    disp(mean_H)
    disp(mean_V)
    disp(mean_HT)
    disp(mean_VT)
end

 

결과 2

>> ConcatTest2

-----cat-----
Horizontal concatenation time: 0.124189 seconds
Vertical concatenation time: 132.244169 seconds

           3      200001

      200001           3

-----mean-----
Mean calculation time (H, dims=2): 695.000000 micro seconds
Mean calculation time (V, dims=1): 188.100000 micro seconds
Mean calculation time (HT, dims=1): 170.100000 micro seconds [v]
Mean calculation time (VT, dims=2): 666.300000 micro seconds

-----size check-----
H size: [3, 1]
V size: [1, 3]
HT size: [1, 3]
VT size: [3, 1]

-----value check-----
   100
   100
   100

   100   100   100

   100   100   100

   100
   100
   100

 

H와 V 행렬를 빈 행렬이 아닌 일부 초기화된 행렬 + horzcat()과 vertcat() 함수를 사용하면 결과가 달라지나 싶어서 실행해봤다. 첫 번째 실험과 비교해서 유의미한 차이를 얻지는 못했다.

 

코드 3

Matlab Code

function ConcatTest3()
    % 예시 데이터 생성
    X = 0:0.001:200;
    Y = 0:0.001:200;
    Z = 0:0.001:200;

    N = length(X);

    H = zeros(3, 200000);
    V = zeros(200000, 3);

    disp('-----cat-----');
    % 수평 결합 (H가 3x200000)
    tic;
    for i = 1:N
        H(:, i) = [X(i); Y(i); Z(i)];
    end
    time_h = toc;
    fprintf('Horizontal concatenation time: %.6f seconds\n', time_h);

    % 수직 결합 (V가 200000x3)
    tic;
    for i = 1:N
        V(i, :) = [X(i), Y(i), Z(i)];
    end
    time_v = toc;
    fprintf('Vertical concatenation time: %.6f seconds\n', time_v);

    disp(size(H));
    disp(size(V));

    HT = H';
    VT = V';

    disp('-----mean-----');
    
    % 평균 계산
    tic;
    mean_H = mean(H, 2);
    time_mean_H = toc;
    fprintf('Mean calculation time (H, dims=2): %.6f micro seconds\n', time_mean_H * 1e+6);
    
    tic;
    mean_V = mean(V, 1);
    time_mean_V = toc;
    fprintf('Mean calculation time (V, dims=1): %.6f micro seconds\n', time_mean_V * 1e+6);
    
    tic;
    mean_HT = mean(HT, 1);
    time_mean_HT = toc;
    fprintf('Mean calculation time (HT, dims=1): %.6f micro seconds\n', time_mean_HT * 1e+6);
    
    tic;
    mean_VT = mean(VT, 2);
    time_mean_VT = toc;
    fprintf('Mean calculation time (VT, dims=2): %.6f micro seconds\n', time_mean_VT * 1e+6);

    disp('-----size check-----');
    fprintf('H size: [%d, %d]\n', size(mean_H));
    fprintf('V size: [%d, %d]\n', size(mean_V));
    fprintf('HT size: [%d, %d]\n', size(mean_HT));
    fprintf('VT size: [%d, %d]\n', size(mean_VT));

    disp('-----value check-----');
    disp(mean_H)
    disp(mean_V)
    disp(mean_HT)
    disp(mean_VT)
end

 

결과 3

>> ConcatTest3

-----cat-----
Horizontal concatenation time: 0.015545 seconds
Vertical concatenation time: 0.014688 seconds

           3      200001

      200001           3

-----mean-----
Mean calculation time (H, dims=2): 675.200000 micro seconds
Mean calculation time (V, dims=1): 207.700000 micro seconds
Mean calculation time (HT, dims=1): 180.400000 micro seconds [v]
Mean calculation time (VT, dims=2): 588.000000 micro seconds

-----size check-----
H size: [3, 1]
V size: [1, 3]
HT size: [1, 3]
VT size: [3, 1]

-----value check-----
   100
   100
   100

   100   100   100

   100   100   100

   100
   100
   100

 

세 번째 Matlab 코드는 전체 행렬에 대한 크기를 먼저 초기화 하고 데이터를 저장하였다. Matlab에서 권장하는 정적할당 형식이다. H와 V의 결합 속도는 첫 번째와 두 번째에 비해 굉장히 빨랐다. 대략 10배 정도 빠르다. 하지만 mean 계산 속도는 다른 것들과 큰 차이는 없었다. 

 

다음은 Julia 코드를 실행해본다.

 

코드 4

Julia Code

using BenchmarkTools

# 예시 데이터 생성
function ConcatTest()
        X = 0:0.001:200
        Y = 0:0.001:200
        Z = 0:0.001:200

        N = length(X)

        H = []
        V = []

        @time for (x,y,z) in zip(X,Y,Z)
                r = [x, y, z]
                push!(H, r)
        end

        @time for (x,y,z) in zip(X,Y,Z)
                r = [x y z]
                push!(V, r)
        end

        H = hcat(H...)  # 벡터의 벡터를 행렬로 변환
        V = vcat(V...)  # 벡터의 벡터를 행렬로 변환

        println(size(H))
        println(size(V))

        HT = transpose(H)
        VT = transpose(V)

        println("-----mean-----")
        @btime mean($H, dims=2)
        @btime mean($V, dims=1)
        @btime mean($HT, dims=1)
        @btime mean($VT, dims=2)

        println("-----size check-----")
        mean_H = mean(H, dims=2)
        mean_V = mean(V, dims=1)
        mean_HT = mean(HT, dims=1)
        mean_VT = mean(VT, dims=2)

        println("H", size(mean_H))
        println("V", size(mean_V))
        println("HT", size(mean_HT))
        println("VT", size(mean_VT))

        println(mean_H)
        println(mean_V)
        println(mean_HT)
        println(mean_VT)
end

ConcatTest()

 

결과 4

-----cat-----
0.010417 seconds (200.01 k allocations: 18.216 MiB)
0.026441 seconds (200.01 k allocations: 18.216 MiB, 67.41% gc time)

(3, 200001)
(200001, 3)

-----mean-----
  517.600 μs (11 allocations: 704 bytes)
  46.400 μs (7 allocations: 624 bytes) [v]
  518.000 μs (14 allocations: 816 bytes)
  47.400 μs (18 allocations: 896 bytes)
  
-----size check-----
H(3, 1)
V(1, 3)
HT(1, 3)
VT(3, 1)

-----value check-----
[100.0; 100.0; 100.0;;]
[100.0 100.0 100.0]
[100.0 100.0 100.0]
[100.0; 100.0; 100.0;;]

 

Julia 코드와 결과이다. 

 

지금 코드는 for 문에서 push!() 함수로 데이터를 1차원적으로 결합한 후, for 문이 종료되고 1차원 벡터를 hcat()이나 vcat() 함수를 사용하여 2차원 행렬로 만들었다. 이 코드 뿐만 아니라, for문 안에서 hcat()과 vcat()을 사용하는 코드도 작성하였지만 for문 안에서 데이터를 결합할 때는 push!() 함수를 사용하는 것이 가장 속도가 빨랐다. 

 

Julia 실행 결과, 똑같은 데이터일 때 (3 x 1 벡터을 수평으로 결합하는 것)이 (1 x 3 행렬을 수직으로 결합하는 것)보다 대략 2배 빠르지만, Matlab 만큼의 격차 (1131배)를 보이지는 않았다. 똑같은 column major order  언어여도 Matlab과 Julia는 물리적인 메모리에 변수를 저장하는 방식이 서로 다른 것 같다. 

 

Matlab과 결과와 비교하자면, Matlab에서 결과가 가장 잘 나온 ConcatTest3와 비교했을 때 결합 속도는 Julia가 Matlab보다 대략 1.5배 빨랐다. Julia가 빠르지만 유의미한 결과라고 보기는 힘들다. 

 

주목해야할 점은 mean()의 실행 속도 결과이다.

Julia에서 mean() 실행 속도가 가장 빠를 경우는 수직 결합의 46.4 micro sec로 수평 결합의 517.600 micro sec보다 대략 11.15배 빠르다. 또한, Matlab의 가장 빠른 경우인 (수평 결합의 transpose , "HT" )의 180.4 micro sec 보다 약 3.888배 빠르다. 내가 눈여겨 보는 결과는 줄리아에서 (3 x 1 벡터을 수평으로 결합한 후 transpose, "HT")의 mean 실행 속도가 나쁘다는 것이다. 이는 Matlab과 상반되는 결과이다. 

 

 

위 결과들을 종합하여 한 가지 결론을 추측하려 한다.

Column major order 언어인 Matlab과 Julia는 메모리 저장 방식에서 다른 작동 원리를 가지고 있다.

Matlab의 메모리 작동 방식은 유동적인, 소프트웨어에 의해 물리적인 메모리 접근 방식이 달라질 수 있다. (tranpose 결과를 봤을 때) 

Julia의 메모리 작동 방식은 고정적이면서도 결정론적인 모습을 보인다.

따라서, Matlab을 먼저 사용하고 Julia를 두 번째로 사용하려는 유저들은 두 언어간 메모리 접근 방식 차이에 대한 이해가 필요하다. 

 

 

한 가지 시나리오를 가정해보자,

컴퓨터 구조적으로 작동원리는 자세히 모르지만, column-major order 언어에서 수평으로 데이터를 결합하는 것이 메모리 저장 속도면에서 효율적이다! 라고 생각하는 유저가 있다고 가정하자. 그가 대용량 데이터를 Matlab에서 수평으로 결합하고 (3 x 1 => 3 x 200000) mean() 뿐만 아니라 다른 어떤 계산을 위해 데이터 간의 사칙연산을 수행하였다고 하자. 만약 transpose() 없이 그대로 작업했다면 ConcatTest1에 따라 689.5 micro sec 만큼의 나쁜 실행 속도 결과를 맞이하게 된다. 이 후, 개선을 위해 cache hit rate를 고려하여 transpose() 후 계산 작업을 수행하면 그가 예상했던 대로 180.1 micro sec 만큼의 결과를 얻어, 실행 속도 향상의 기쁨을 누릴 것이다.

 

문제가 발생하는 것은 이 사용자가 Julia 코드를 작성할 때 Matlab에서 하던 습관을 똑같이 Julia에 적용할 때이다. 평소처럼 수평으로 결합하고 (3 x 1 => 3 x 200000) cache hit rate를 고려하여 transpose() 후 계산 작업을 수행하면 그의 의도와는 달리 518 micro sec 만큼의 나쁜 결과를 맞이한다. 오히려 결합할 때는 실행 속도면에서 조금 손해보더라도 수직으로 결합 (1 x 3 => 200000 x 3) 후 계산 작업을 수행하는 것이 최종적으로는 실행 속도면에서 더 효율적일 수도 있다. 데이터 개수가 적다면 그 차이는 적겠지만, 데이터가 많으면 많을 수록 cache hit rate가 높아짐에 따라 실행 속도의 차이는 더 커질 것이다.

 

 

요약.

Matlab 사용자가 Julia에서 데이터를 다룰 때는 더 주의가 필요하다.

 

Matlab : 수평 결합(n x 1 => n x (1+m) ) 후 transpose -> 계산

Julia :
수평 결합(n x 1 => n x (1+m) ) 후 -> 계산, (10,417 + 517) micro sec
수직 결합(1 x n => (1+m) x n) 후 -> 계산, (26,441 + 46) micro sec

 

감사합니다.

 

댓글