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:
| Length | Storage Class | Declaration |
| Fixed | Static | static int a[16][128] |
| Auto | void foo(void) { int a[16][128]; } |
| Parameter | void bar(int a[16][128]); |
| Heap | int (*p2d)[128]; |
| Variable (C99) | Auto | void foo(int m, int n) { int a[m][n]; } |
| Parameter | void bar(int m, int n, int a[m][n]); |
| Heap | void 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 Prototypes | Descriptions |
__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);
}