Введение в оптимизацию приложений с помощью инструментов Intel

Optimizing compiler. Auto parallelization

Показывать лекцию целиком

Multicore and multiprocessor is de facto standard

The presentation can be downloaded here.

Intel® Pentium® Processor Extreme Edition (2005-2007)

Dual core introduced

Intel® Xeon® Processor 5100, 5300 Series

Intel® Core™2 Processor Family (2006-)

Dual-processor and quad-core

Intel® Xeon® Processor 5200, 5400, 7400 Series

Intel® Core™2 Processor Family (2007-)

2-4 processors, up to 6 cores

Intel® Atom™ Processor Family (2008-)

Energy Efficiency has high priority

Intel® Core™i7 Processor Family (2008-)

Hyperthreading technology. System with non-uniform memory access.

CPU core is a complete computer system that shares some of the processing resources with other cores

(рис 6.1) Dual CPU Core Chip

Multiprocessor systems are

  • Massively parallel computers or systems with distributed memory (MPP systems).
  • Each processor is completely autonomous
  • There is a communications medium.
  • Advantages: good scalability
  • Disadvantages: slow inter processor communication/li>
  • Shared memory systems (SMP systems)
  • All processors are equidistant from the memory. Communication with the memory via a common data bus.
  • Advantages: good inter processor communication
  • Disadvantages:
  • poor scalability
  • the high cost of cache subsystem synchronization
  • Systems with non-uniform memory access (NUMA)
  • Memory is physically distributed among processors. Single address space is supported at the hardware level.
  • Advantages: good inter processor communication and scalability
  • Disadvantages: different latency for the different parts of memory.
  • (рис 6.2) Intel QuickPath Architecture

    Instability of the application performance on multiprocessor machines with non-uniform memory access.

    OS periodically moves application from one core to another with different access speed to the memory used.

    (рис 6.3)

    Pros and cons of the multi-threaded applications

  • + +:
  • Computational resources are increased according to the kernel count.
  • - -:
  • The increasing complexity of design
  • Thread synchronization overhead
  • Data races/ resource concurrency
  • Thread creation overhead
  • Conclusion:
  • If you are developing business applications, clearly aware of the goals and price of parallelism in your application.
  • Auto parallelization is a compiler optimization which automatically converts sequential code into multi-threaded in order to utilize multiple cores simultaneously. The purpose of the automatic parallelization is to free programmer from the difficult and tedious manual parallelization.

    /Qparallel

    enable the auto-parallelization to generate multi-threaded code for loops that can be safely executed in parallel

    Profitability of auto parallelization

    Let’s consider simple fortran test:

    REAL :: a(1000,1000),b(1000,1000),
            c(1000,1000)
    integer   i,j,rep_factor 
    DO I=1,1000
      DO J=1,1000
        A(J,I) = I
        B(J,I) = I+J
        C(J,I) = 0
      END DO
    END DO
    DO rep_factor=1,1000
      C=B/A+rep_factor 
    END DO
    END 
    
    (рис 6.4) (рис 6.5)

    This slide demonstrates auto parallelization speedup. We have improvement on real time from 2.29 s. to 1.67 s. User time has been changed from 2.23 s. to 6.02 s. It means that amount of work was increased.

    void matrix_mul_matrix(int n, 
    float C[n][n], float A[n][n], 
    float B[n][n]) {
     int i,j,k;
     for (i=0; i<n; i++) 
      for (j=0; j<n; j++) {
       C[i][j]=0;
           for(k=0;k<n;k++)
             C[i][j]+=A[i][k]*B[k][j];
        }
    }
    
    (рис 6.6) Algorithm scalability

    CPU cores may compete for some processor resources, for example for cash subsystem and for system bus. Bus bandwidth can be bottleneck for some algorithms. The right picture demonstrates the relation between time and number of threads for matrix multiplication loop. It is an example of highly scalable algorithm.

    void matrix_add(int n, float Res[n][n],float A1[n][n], float A2[n][n],
    float A3[n][n],float A4[n][n], float A5[n][n], float A6[n][n],
    float A7[n][n], float A8[n][n]) {
      int i,j;
      for (i=0; i<n; i++) 
        for (j=1; j<n-1; j++) 
        Res[i][j]=A1[i][j]+A2[i][j]+A3[i][j]+A4[i][j]+
          A5[i][j]+A6[i][j]+A7[i][j]+A8[i][j]+
          A1[i][j+1]+A2[i][j+1]+A3[i][j+1]+A4[i][j+1]+
          A5[i][j+1]+A6[i][j+1]+A7[i][j+1]+A8[i][j+1];
    
    (рис 6.7) Some poorly scalable algorithm

    Admissibility of auto parallelization

    Auto parallelization is a loop permutation optimization.

    Initial order of instruction execution becomes unspecified.

    A necessary conditions for its applicability are:

  • Loop is identified and its class match the requirements
  • The absence of dependencies within the loop
  • Heuristic model considers parallelization as profitable
  • /Qpar-report{0|1|2|3} control the auto-parallelizer diagnostic level

    /Qpar-report3 option informs if the compiler parallelizes or doesn’t parallelize a loop, including reports which problem prevents auto parallelization. For example:

  • par.c(11): (col. 1) remark: LOOP WAS AUTO-PARALLELIZED
  • par.c(101): (col. 3) remark: loop was not parallelized: existence of parallel dependence.
  • par.c(27) (col. 1): remark: loop was not parallelized: insufficient computational work.
  • par.c(34): (col. 3) remark: loop was not parallelized: insufficient inner loop.
  • par.c(77): (col. 1) remark: DISTRIBUTED LOOP WAS AUTO-PARALLELIZED.
  • par.c(133): (col. 1) remark: FUSED LOOP WAS AUTO-PARALLELIZED.
  • Loop identification
  • Loop optimization can be performed only for loops with a certain number of iterations with successively changing iteration variables that have no hits outside of the loop and calls the unknown functions.
  • recommendations
  • Use -Qpar_report3 to identify a problem. The absence of loop in the report or unsupported loop structure is the reason for the following steps:
  • Avoid loops with an uncertain number of iterations, the conversion outside the loop, unknown functions calls
  • use local variables to hold the boundaries of the loop instead of global variables and structure members
  • do not use a massive operation in Fortran for allocable arrays and array pointers.
  • Interprocedural analysis can help with additional information. (-Qipo )
  • Use -ansi-alias for C/C++
  • Use "restrict" attribute inside C/C++ code (-Qstd = c99)
  • Dependencies
  • One of hardest problems for compiler is resolving the memory disambiguation task or performing the alias analysis.
  • The compiler assumptions are conservative. It means that different location of memory objects must be proved, they assumed as aliased by default.
  • recommendations:
  • Use -Qpar_report3 to identify a problem.
  • Use -ansi-alias for C/C++
  • Use attribute to restrict inside C/C++ code (-Qstd=c99)
       int sub (int *a, float *b, int n) =>
       int sub (int *restrict a, float *restrict b, int n)
    
  • -Qipo
  • Directives #pragma ivdep
  • The ivdep pragma tells the compiler to ignore assumed vector dependencies.
  • Profitability of auto parallelization
  • /Qpar_report3: loop was not parallelized: insufficient computational work.
  • To estimate optimization profit is a difficult task. There are performance effects that are difficult to predict at compile time. Number of loop iteration can be unknown.
  • For instance, unknown probability of branches inside the loop can lead to wrong work estimation.
  • recommendations:
  • Use compiler parallelization directives
    # pragma parallel, # pragma parallel always, # pragma noparallel
    
  • Compiler Options
  • / Qpar-threshold [n] threshold for avtoparallelizatsii from 0 to 100
  • / Qpar-runtime-control [n] the level of generating run-time checks from 0 to 3
  • / Qpar-num-threads = <n> installation of various numbers of threads
  • Use –O3 . Some loop optimizations can make loops more profitable for parallelization. (fusion, distribution)
  • -Qprof_gen/-Qprof_use Compile with profiling could open more optimizations
  • Automatic parallelization is done using the OpenMP interface. OpenMP (Open Multi-Processing) is a software interface that supports multi-platform programming for multiprocessor computation systems with shared memory on C/C++ and Fortran.

    The number of used threads is defined by the environment variable OMP_NUM_THREADS and can be modified before application launch (by default application will use all available cores)

    (рис 6.8) 8 Threads (рис 6.9) 16 Threads (рис 6.10) Loop automatic parallelization

    Loop parallelization looks like creating a function taking all used objects and loop iteration space part as arguments.

    The multiple instances of loop thread function are executed in different streams with different values of the boundaries. Iterative loop space is divided into several parts and each is given to the separate thread.

    Optimizing compiler is able to create several versions of code by adding some run-time checks to resolve dependences issues, to make estimation of loop iterations, etc.

    /Qpar-runtime-control[n]

    Control parallelizer to generate runtime check code for effective automatic parallelization.

  • n=0 no runtime check based auto-parallelization
  • n=1 generate runtime check code under conservative mode (DEFAULT when enabled)
  • n=2 generate runtime check code under heuristic mode
  • n=3 generate runtime check code under aggressive mode
  • Interaction with other loop optimizations

    An optimizing compiler makes loop parallelization together with other optimizations. Because thread creation has its price it is more profitable to has large loops for processing. So order of optimizations canbe:

  • Loop fusion (creation of large loops).
  • Loop interchange and other loop optimizations for improving memory access.
  • Auto parallelization.
  • Loop optimizations in the threaded functions in accordance with the usual considerations. (loop distribution, loop unrolling, auto vectorization, etc.).
  • These considerations can be used during creating a program design to simplify following threadization with usage of compiler autoparallelization or OPENMP directives.

    Software prefetching

    Prefetching is loading data from relatively slow memory into the cache before the memory is required by processor. Software prefetching is insertion of the special prefetch instructions to the code.

    There are several methods of prefetch usage:

  • Explicit instruction insertion.
  • Implicit insertion with compiler option –prefetch, known as auto prefetch compiler feature.
  • Prefetch intrinsic functions are defined in xmmintrin.h and has the form

    # include <xmmintrin.h>
    enum _mm_hint {_MM_HINT_T0 = 3, (L1)
       _MM_HINT_T1 = 2, (L2)
       _MM_HINT_T2 = 1, (L3)
       _MM_HINT_NTA = 0};
    void _mm_prefetch (void * p, enum _mm_hint h);
    

    It loads a cache line from the address specified (size of the cache line is 64 bytes)

    Use CALL mm_prefetch (P, HINT) inside the fortran programs

    Why software prefetching can be useful

    There is hardware prefetch mechanism which tries to identify the memory access pattern to choose the appropriate preloading scheme. It works fine when the memory is accessed with constant stride and this stride is relatively small.

    Software prefetch instructions have its price. Computing system can ignore software prefetching instructions when the system bus is busy.

    Don’t use software prefetching instructions

  • in case when hardware prefetching mechanism is able to help
  • if there are many memory requests and the system bus is busy
  • all needed memory is already cached
  • There are many cases when programmer could help to preload the memory required:

  • large constant stride
  • work with chains
  • variable stride access to memory
  • many different memory objects (?)
  • The VTUNE usage can help to identify slowdowns, caused by the inefficient memory access.

    SUBROUTINE CALC(A,B,C,N,K,SEC)
    INTEGER N,K,SEC,I,J
    REAL A(K,N),B(K,N),C(K,N)
    DO I=1,K
      DO J=1,N
        A(I,J)=A(I,J)/(A(I,J)+ B(I,J)*C(I,J))
    #ifdef PERF
          CALL mm_prefetch(A(I,J+SEC),3)
          CALL mm_prefetch(B(I,J+SEC),3)
          CALL mm_prefetch(C(I,J+SEC),3)
    #endif
      END DO
    END DO
    END SUBROUTINE CALC
    

    Idea of this example:

    Memory is accessed with large constant stride.

    Can we obtain performance gain?

    Would it be different for different SEC values?

    INTEGER N,K
    REAL, ALLOCATABLE :: A(:,:),B(:,:),C(:,:),D(:,:)
    REAL T1,T2
    INTEGER REP,SEC
    READ *,N,SEC
    READ *,K
    ALLOCATE(A(K,N),B(K,N),C(K,N))
    ALLOCATE(D(10000,10000))
    A=1
    B=1
    C=1
    D=0
    CALL CPU_TIME(T1)
    CALL CALC(A,B,C,N,K,SEC)
    CALL CPU_TIME(T2)
    PRINT *,T2-T1 
    END
    

    Execution results

     ifort /fpp  -DPERF -Od pref.f90 -Feperf_pref.exe 
     ifort /fpp         -Od pref.f90 -Feperf.exe  
    

    Data for input:

     4000 SEC
     4000
    

    without SP: 0.48s.

      SEC == 1 with SP:   0.56s.
      SEC == 4 with SP:   0.48s    
    

    ?? Price of prefetch instructions exceed gain from prefetch.

    Let’s enlarge calculations inside loop

    A(I,J)=A(I,J)/(A(I,J)+ B(I,J)*C(I,J)) => 
    A(I,J) = (EXPONENT(A(I,J))+EXPONENT(B(I,J))+EXPONENT(C(I,J)))/(A(I,J)*B(I,J)*C(I,J))
    

    without SP: 1.45s.

      SEC == 1 with SP:   1.07s.
      SEC == 4 with SP:   0.98s 
    

    Conclusion

    It is hard to determine if the prefetch instruction can be helpful for all computing systems. The performance of the memory subsystems depend on many different factors such as, amount of cash memory, memory latency, bandwidth, etc. Prefetch instruction call has its price and increases amount of data which should be passed through the system bus. Therefore result of software prefetch can be different for the different computing systems.

    Auto software prefetching options

    /Qopt-prefetch[:n]

  • 1-4 Enables different levels of software prefetching. If you do not specify a value for n, the default is 2 on IA-32 and Intel® 64 architecture; the default is 3 on IA-64 architecture. Use lower values to reduce the amount of prefetching.
  • 0 Disables software prefetching. This is the same as specifying -no-opt-prefetch (Linux and Mac OS X) or /Qopt-prefetch- (Windows).
  • C/C++ extended array notation

    C/C++ language extension for array notations is an Intel-specific language extension that is a part of Intel® Cilk™ Plus feature supported by the Intel® compiler.

    The C/C++ extension provides data parallel array notations with the following major benefits:

  • Allows you to use array notation to program parallel operations in a familiar language
  • Achieves predictable performance based on mapping parallel constructs to the underlying multi-threaded and SIMD hardware
  • Enables compiler parallelization and vectorization with less reliance on alias and dependence analysis
  • When you use the array notations, the Intel® compiler implements them using vector code.

    Usage Recommendations

    Use the array notations when your algorithm requires operations on arrays and where it does not require a specific order of operations among the elements of the array(s).

    To use the array notations in your application, keep the following sequence of steps in mind:

    Insert the array notations language extensions into your application source code.

    Compile the application at optimization level –O1 and above to enable vectorization. By default, the compiler generates SIMD vector instructions in the SSE2 instruction set. To generate SIMD vector instructions beyond SSE2, you can add target/architecture-specific compiler options to the compile command.

    By default, the Intel® compiler accepts the array notations language extensions to generate vector and multi-threaded code based on the data parallel constructs in the program.

    CEAN (C/C++ Extensions for Array Notations Programming Model)

    Array declarations:
    LengthStorage ClassDeclaration
    FixedStaticstatic int a[16][128]
    Auto void foo(void) { int a[16][128]; }
    Parametervoid bar(int a[16][128]);
    Heapint (*p2d)[128];
    Variable (C99)Autovoid foo(int m, int n) { int a[m][n]; }
    Parametervoid bar(int m, int n, int a[m][n]);
    Heapvoid bar(int m, int n) { int (*p2d)[n]; }

    Declaration of the array sections

      section_operator :: = [<lower bound>:<length>:<stride>]
      a[0:3][0:4]
      b[0:2:3]
    

    You must use –std=c99 (Linux и MAC OS) or /Qstd=c99 compiler options

    Example:

    typedef int (*p2d)[128];
      p2d p = (p2d) malloc (sizeof(int)*rows*128);
      p[0:rows][:]
    

    Most of C/C++ operators are available for array sections.

    a[:]*b[:]                  // element-wise multiplication
    a[3:2][2:2] + b[5:2][5:2]  // matrix addition
    a[0:4]+c                   // adds scalar to an array section
    a[:][:] = b[:][1][:] + c   // array assignment
    
    Function prototypes
    Function PrototypesDescriptions
    __sec_reduce(fun, identity, a[:])Generic reduction function. Reduces fun across the array a[:] using identity as the initial value.
    __sec_reduce_add(a[:])Built-in reduction function. Adds values passed as arrays
    __sec_reduce_mul(a[:])Built-in reduction function. Multiplies values passed as arrays
    __sec_reduce_all_zero(a[:])Built-in reduction function. Tests that array elements are all zero
    __sec_reduce_all_nonzero(a[:])Built-in reduction function. Tests that array elements are all non-zero
    __sec_reduce_any_nonzero(a[:])Built-in reduction function. Tests for any array element that is non-zero
    __sec_reduce_min(a[:])Built-in reduction function. Determines the minimum value of array elements
    __sec_reduce_max(a[:])Built-in reduction function. Determines the maximum value of array elements
    __sec_reduce_min_ind(a[:])Built-in reduction function. Determines the index of minimum value of array elements
    __sec_reduce_max_ind(a[:])Built-in reduction function. Determines the index of maximum value of array elements
    #include <stdio.h>
    #include <stdlib.h>
    #define N 2000
    typedef double (*p2d)[];
    void matrix_mul(int n, double a[n][n], 
            double b[n][n],double c[n][n]) {
     int i,j;
     a[:][:] =1;
     b[:][:] =-1;
     for(i=0;i<n;i++)
      for(j=0;j<n;j++)
          c[i][j]=c[i][j]+ 
    __sec_reduce_add(a[i][:]*b[:][j]);
     return;
    }
    
    int main() {
     p2d a= (p2d)malloc(N*N*sizeof(double)) ;
     p2d b= (p2d)malloc(N*N*sizeof(double)) ;
     p2d c= (p2d)malloc(N*N*sizeof(double));
     matrix_mul(N,a,a,a);
     matrix_mul(N,a,b,c);
     free(a);
     free(b);
     free(c);
    }
    

    Вернуться к учебному плану