Search code examples
fortranopenmptransposematrix-multiplicationintel-fortran

Speedup of calculation for symmetric matrix using OMP


My matrix calculation is: C=C-A*B

Here C is a symmetric matrix so I want to speed up this calculation by considering just the upper triangular and then take the opposite elelement. I used OMP and see that my implementation is slower than the normal calculation for the entire matrix C.

I also see that the calculation for C=C-AxB is slower than C=C+AxB.

My program is attached. Please advise me!

    Program testspeed
implicit none
integer nstate,nmeas,i,j,l
integer(kind=8) :: tclock1, tclock2, clock_rate
real(kind=8) :: elapsed_time
double precision, allocatable, dimension(:,:):: B,C,A
nstate =20000
nmeas=10000
allocate (B(nmeas,nstate),C(nstate,nstate),A(nstate,nmeas))
A=1d0
B=1d0
call system_clock(tclock1)
write(*,*) "1"
!$omp parallel do
do j = 1, nstate
    do l = 1,nmeas
        do i = 1, j
            C(j,i) = C(j,i) - A(j,l)*B(l,i)
            C(i,j)=C(j,i)
        end do
    end do
end do
!$omp end parallel do
write(*,*) "2"
call system_clock(tclock2, clock_rate)
elapsed_time = float(tclock2 - tclock1) / float(clock_rate)
write(*,*) elapsed_time
end Program testspeed

Solution

  • One of the basic rules I have taught my students is that nobody should be writing dense matrix multiplies themselves nowadays - and should not have been doing for 30 years +. You should use the BLAS library instead. Below I compare using the BLAS library against your loop ordering and a better loop ordering, and also against the Fortran intrinsic function matmul which I use as a reference to check the results are correct. BLAS and matmul don't take advantage of the symmetry of C, yet they still are the fastest routines - BLAS is about 200-300 times quicker than the loop ordering you have written. Note I have also cut the matrix size down somewhat as I got bored waiting for the original to run for larger cases:

    ijb@ijb-Latitude-5410:~/work/stack$ cat mm.f90
    Program testspeed
    
      Use, Intrinsic :: iso_fortran_env, Only : wp => real64, li => int64
    
      Use omp_lib, Only : omp_get_max_threads
      
      Implicit None
      Integer nstate,nmeas,i,j,l
      Integer(li) :: tclock1, tclock2, clock_rate
      Real(wp) :: elapsed_time
      Real( wp ), Allocatable, Dimension(:,:):: B,C,A
      Real( wp ), Allocatable, Dimension(:,:):: C_test
      Real( wp ), Allocatable, Dimension(:,:):: C_start
    
      Write( *, * ) 'Using ', omp_get_max_threads(), ' threads'
      
    !!$  nstate =2000
    !!$  nmeas=1000
      nstate = 5000
      nmeas  = 2500
      Allocate (B(nmeas,nstate),C(nstate,nstate),A(nstate,nmeas))
      Allocate( C_test, Mold = C )
      Allocate( C_start, Mold = C )
    !!$  A=1.0_wp
    !!$  B=1.0_wp
      ! Random numbers are a much better test
      Call Random_number( A )
      B = Transpose( A ) ! make sure result is symmetric
      Call Random_number( C_start )
      ! Make Initial C Symmetric
      C_start = 0.5_wp * ( C_start + Transpose( C_start ) )
    
      Write( *, * ) 'Matix sizes ', Shape( A ), Shape( B ), Shape( C )
      
      C_test = C_start
      Call system_Clock(tclock1)
      C_test = C_test -  Matmul( A, B )
      Call system_Clock(tclock2, clock_rate)
      elapsed_time = Real(tclock2 - tclock1,wp) / Real(clock_rate,wp)
      Write( *,'( a, t20, f8.3 )' ) 'Matmul', elapsed_time
    
      C = C_start
      Call system_Clock(tclock1)
      !$omp parallel do
      Do j = 1, nstate
         Do l = 1,nmeas
            Do i = 1, j
               C(j,i) = C(j,i) - A(j,l)*B(l,i)
               C(i,j)=C(j,i)
            End Do
         End Do
      End Do
      !$omp end parallel do
      Call system_Clock(tclock2, clock_rate)
      elapsed_time = Real(tclock2 - tclock1,wp) / Real(clock_rate,wp)
      Write(*,'( a, t20, f8.3, t30, "Max error ", g20.12 )' ) &
           'Orig loops', elapsed_time, Maxval( Abs( C_test - C ) )
    
      C = C_start
      Call system_Clock(tclock1)
      !$omp parallel default( none ) shared ( nstate, nmeas, A, B, C ), private( i, j, l )
      !$omp do 
      Do i = 1, nstate
         Do l = 1,nmeas
            Do j = 1, i
               C(j,i) = C(j,i) - A(j,l)*B(l,i)
            End Do
         End Do
      End Do
      !$omp end do
      !$omp do
      Do i = 1, nstate
         Do j = 1, i
            C( i, j ) = C( j, i )
         End Do
      End Do
      !$omp end do
      !$omp end parallel
      Call system_Clock(tclock2, clock_rate)
      elapsed_time = Real(tclock2 - tclock1,wp) / Real(clock_rate,wp)
      Write(*,'( a, t20, f8.3, t30, "Max error ", g20.12 )' ) &
           'Sensible loops', elapsed_time, Maxval( Abs( C_test - C ) )
    
      C = C_start
      Call system_Clock(tclock1)
      Call dgemm( 'N', 'N', nstate, nstate, nmeas, -1.0_wp, A, Size( A, Dim = 1 ), &
                                                            B, Size( B, Dim = 1 ), &
                                                   +1.0_wp, C, Size( C, Dim = 1 ) )
      Call system_Clock(tclock2, clock_rate)
      elapsed_time = Real(tclock2 - tclock1,wp) / Real(clock_rate,wp)
      Write(*,'( a, t20, f8.3, t30, "Max error ", g20.12 )' ) &
           'BLAS ', elapsed_time, Maxval( Abs( C_test - C ) )
      
    End Program testspeed
    ijb@ijb-Latitude-5410:~/work/stack$ gfortran --version
    GNU Fortran (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
    Copyright (C) 2019 Free Software Foundation, Inc.
    This is free software; see the source for copying conditions.  There is NO
    warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
    
    ijb@ijb-Latitude-5410:~/work/stack$ gfortran -fopenmp -Wall -Wextra -std=f2018 -O3  mm.f90 -lopenblas
    ijb@ijb-Latitude-5410:~/work/stack$ export OMP_NUM_THREADS=1
    ijb@ijb-Latitude-5410:~/work/stack$ ./a.out
     Using            1  threads
     Matix sizes         5000        2500        2500        5000        5000        5000
    Matmul                4.793
    Orig loops          421.564  Max error   0.488853402203E-11
    Sensible loops       20.742  Max error   0.488853402203E-11
    BLAS                  2.185  Max error   0.682121026330E-12
    ijb@ijb-Latitude-5410:~/work/stack$ export OMP_NUM_THREADS=2
    ijb@ijb-Latitude-5410:~/work/stack$ ./a.out
     Using            2  threads
     Matix sizes         5000        2500        2500        5000        5000        5000
    Matmul                4.968
    Orig loops          324.319  Max error   0.466116034659E-11
    Sensible loops       17.656  Max error   0.466116034659E-11
    BLAS                  1.161  Max error   0.682121026330E-12
    ijb@ijb-Latitude-5410:~/work/stack$ export OMP_NUM_THREADS=3
    ijb@ijb-Latitude-5410:~/work/stack$ ./a.out
     Using            3  threads
     Matix sizes         5000        2500        2500        5000        5000        5000
    Matmul                4.852
    Orig loops          243.268  Max error   0.500222085975E-11
    Sensible loops       15.802  Max error   0.500222085975E-11
    BLAS                  0.852  Max error   0.682121026330E-12
    ijb@ijb-Latitude-5410:~/work/stack$ export OMP_NUM_THREADS=4
    ijb@ijb-Latitude-5410:~/work/stack$ ./a.out
     Using            4  threads
     Matix sizes         5000        2500        2500        5000        5000        5000
    Matmul                4.994
    Orig loops          189.189  Max error   0.477484718431E-11
    Sensible loops       14.245  Max error   0.477484718431E-11
    BLAS                  0.707  Max error   0.682121026330E-12
    

    For BLAS I have used openblas - which is freely available. On Linux system a simple apt get or similar should be enough.

    Please also note

    1. If you have to write your own loops your inner most loop should go, if possible, over the first index of your array. This is because Fortran orders its arrays as column major. This is what I have done in the "sensible" loop ordering
    2. Real( 8 ) is not portable, not guaranteed to be supported by your compiler, and not guaranteed to do what you expect, and shouldn't be used. Similar for Integer( 8 ). Please see what I have done for a better way that should work with all compilers.
    3. Float is not a standard intrinsic - use Real as I have done
    4. As benchmarks are meaningless if the results are incorrect you should always include a way to test the results. Here I use the Fortran intrinsic matmul to provide a reference version. Your original code does not initialise C, so the results can not be trusted - but as you don't check you get the correct values for C you can't know this.
    5. I personally dislike !$omp parallel do intensely, I think it a mistake that such short cuts are in OpenMP. Instead separate them into !$omp parallel and !$omp do - it is very important to understand that thread creation and work sharing are different things, convoluting them in one line is not a good way to learn OpenMP.