12/08/2018, 16:58

Giới thiệu OpenMP trong C++ (Phần 1)

Giới thiệu chung Bài viết này nhằm mục đích đưa ra một cái nhìn cơ bản về OpenMP và sử dụng OpenMP với C++ và sử dụng GCC để biên dịch chương trình Nội dung Giới thiệu OpenMP trong C++ Cú pháp Offloading Teams Thread-safety Quản lý chia sẻ dữ liệu giữa các thread Đồng bộ hóa ...

Giới thiệu chung

Bài viết này nhằm mục đích đưa ra một cái nhìn cơ bản về OpenMP và sử dụng OpenMP với C++ và sử dụng GCC để biên dịch chương trình

Nội dung

  • Giới thiệu OpenMP trong C++
  • Cú pháp
  • Offloading
  • Teams
  • Thread-safety
  • Quản lý chia sẻ dữ liệu giữa các thread
  • Đồng bộ hóa
  • Thread cancellation
  • Performance

Tầm quan trọng của đa luồng

Khi mà tốc độ của CPU không còn có thể cải thiện thêm nhiều thì hệ thống multicore bắt đầu trở lên phổ biến hơn. Do đó, các lập trình viên cũng cần nắm bắt về parallel programming nhiều hơn, đưa ứng dụng thực hiện nhiều công việc hơn cùng lúc.

Bài viết này nhằm mục đích giới thiệu một cách cơ bản nhất về OpenMP, một bộ thư viện mở rộng C/C++/Fortran nhằm cho phép thực hiện song song vào source code hiện tại mà không cần phải viết lại nó.

Các trình biên dịch hỗ trợ OpenMP

  • GCC: Sử dụng -fopenmp
  • Clang++: Sử dụng -fopenmp
  • Solaris Studio: Sử dụng -xopenmp
  • Intel C Compiler: Sử dụng -openmp
  • Microsoft Visual C++: Sử dụng /openmp

OpenMP trong C++

OpenMP bao gồm một tập các chỉ dẫn biên dịch #pragma nhằm chỉ dẫn cho chương trình hoạt động. Các pragma được thiết kế nhằm mục đích nếu các trình biên dịch không hỗ trợ thì chương trình vẫn có thể hoạt động bình thường, nhưng sẽ không có bất kỳ tác vụ song song nào được thực hiện như khi sử dụng OpenMP

Dưới đây là một ví dụ đơn giản về OpenMP nhằm tăng tốc độ thực hiện vòng lặp for

Ví dụ sử dụng nhiều thread thực hiện vòng lặp for

#include <cmath>
int main(int argc, char** argv)
{
    const int size = 256;
    double sinTable[size];
    
    #pragma omp parallel for
    for(int n=0; n<size; ++n)
        sinTable[n] = std::sin(2 * M_PI * n / size);
  
    // the table is now initialized
    return 0;
}

Ví dụ sử dụng một thread thực hiện vòng lặp for

#include <cmath>
int main()
{
    const int size = 256;
    double sinTable[size];

    #pragma omp simd
    for(int n=0; n<size; ++n)
        sinTable[n] = std::sin(2 * M_PI * n / size);

    // the table is now initialized
    return 0;
}

Ví dụ khởi tạo bảng bằng thực hiện song song (sử dụng nhiều thread trên thiết bị khác)

Từ OpenMP 4.0 hỗ trợ việc offloading code trên các thiết bị khác, như GPU.

#include <cmath>
int main()
{
    const int size = 256;
    double sinTable[size];

    #pragma omp target teams distribute parallel for map(from:sinTable[0:256])
    for(int n=0; n<size; ++n)
        sinTable[n] = std::sin(2 * M_PI * n / size);

    // the table is now initialized
}

Ví dụ tính toán hệ số Mandelbrot trên host computer

#include <complex>
#include <cstdio>

typedef std::complex<double> complex;

int MandelbrotCalculate(complex c, int maxiter)
{
    // iterates z = z + c until |z| >= 2 or maxiter is reached,
    // returns the number of iterations.
    complex z = c;
    int n=0;
    for(; n<maxiter; ++n)
    {
        if( std::abs(z) >= 2.0) break;
        z = z*z + c;
    }
    return n;
}
int main()
{
    const int awidth = 78, height = 44, num_pixels = awidth*height;
    
    const complex center(-.7, 0), span(2.7, -(4/3.0)*2.7*height/awidth);
    const complex begin = center-span/2.0;//, end = center+span/2.0;
    const int maxiter = 100000;

    #pragma omp parallel for ordered schedule(dynamic)
    for(int pix=0; pix<num_pixels; ++pix)
    {
        const int x = pix%awidth, y = pix/awidth;
        
        complex c = begin + complex(x * span.real() / (awidth +1.0),
                                    y * span.imag() / (height+1.0));
        
        int n = MandelbrotCalculate(c, maxiter);
        if(n == maxiter) n = 0;
        
        #pragma omp ordered
        {
            char c = ' ';
            if(n > 0)
            {
                static const char charset[] = ".,c8M@jawrpogOQEPGJ";
                c = charset[n % (sizeof(charset)-1)];
            }
            std::putchar(c);
            if(x+1 == awidth) std::puts("|");
        }
    }
}

Trong các ví dụ trên, khi remove #pragma, kết quả tính toán của chương trình vẫn đúng như kỳ vọng.

Chỉ khi trình biên dịch được biên dịch với #pragma, nó trở thành một chương trình chạy song song. Nó có thể thực hiện tính toán N giá trị đồng thời với N là số thread thực hiện.

Cú pháp

Tất cả các chỉ dẫn OpenMP trong C/C++ đều được dùng thông qua #pragma omp theo sau là các thông số và kết thúc bằng một ký hiệu xuống dòng. #pragma chỉ được áp dụng vào đoạn chương trình ngay sau nó, ngoại trừ lệnh barrierflush

Chỉ dẫn parrallel

Chỉ dẫn parrallel bắt đầu một đoạn code thực hiện song song. Nó tạo ra một team bao gồm N threads (N được chỉ định tại thời điểm chạy chương trình, thông thường bằng số nhân CPU, nhưng có thể bị ảnh hưởng bởi một số lý do khác), các lệnh xử lý tiếp theo ngay sau #pragma hoặc block tiêp theo (trong giới hạn {}) sẽ được thực hiện song song. Sau đoạn xử lý này, các thread sẽ được join lại về một

#pragma omp parallel
{
    // Code inside this region runs in parallel.
    printf("Hello!
");
}

Đoạn code này sẽ tạo ra một nhóm threads, mỗi thread thực hiện cùng một đoạn code in ra màn hình dòng chữ "Hello", số lần in ra màn hình bằng số thread dược tạo ra. Ví dụ hệ thóng dual-core sẽ in ra màn hình 2 lần "Hello". Chú ý rằng, nó có thể in ra màn hình đoạn chữ kiểu "HeHlellolo", tùy thuộc vào hệ thống bởi vì việc in ra màn hình được thực hiện song song. Sau dấu "}", các threads sẽ được join lại thành một như khi chạy một chương trình thông thường.

GCC thực hiện đoạn chương trình này bằng cách tạo ra một hàm ảo và đưa các đoạn code tương ứng vào hàm đó, do đó các biến được định nghĩa trong block trở thành biến cục bộ của hàm.

Có thể sử dụng điều kiện cho việc sử lý song song như sau

extern int parallelism_enabled;
#pragma omp parallel for if(parallelism_enabled)
for (int c=0; c<n; ++c)
    handle(c);

Nếu parallelism_enabled được đặt giá trị bằng 0 thì số thread được tạo ra cho việc xử lý vòng lặp for là 1.

Chỉ dẫn lặp for

Chỉ dẫn for sẽ chia vòng lặp for mà mỗi thread trong nhóm thread hiện tại thực hiện một phần của vòng lặp

#pragma omp for
for(int n = 0; n < 10; ++n)
{
    printf(" %d", n);
}
printf(".
");

Vòng lặp này sẽ in ra màn hình từ 0..9. Tuy nhiên nó có thể in ra không theo tứ tự, ví dụ

0 5 6 7 1 8 2 3 4 9.

Vòng lặp này có thể hình dung bằng đoạn code tương ứng như sau:

int this_thread = omp_get_thread_num(), num_threads = omp_get_num_threads();
int my_start = (this_thread  ) * 10 / num_threads;
int my_end   = (this_thread+1) * 10 / num_threads;
for(int n = my_start; n < my_end; ++n)
    printf(" %d", n);

Chú ý rằng #pragma omp for chia các phần của vòng lặp cho các thread khác nhau trong team. Mỗi team là một nhóm các thread thực hiện chương trình. Khi chương trình bắt đầu, team bao gồm duy nhất một thành viên là thread chính đang chạy chương trình.

Để tạo ra một team mới, cần chỉ định rõ từ khóa parrallel

#pragma omp parallel
{
    #pragma omp for
    for(int n=0; n<10; ++n) printf(" %d", n);
}
printf(".
");

Refs

https://bisqwit.iki.fi/story/howto/openmp/

0