Cài dat thuan toán song song trong dev c++ năm 2024

Trước khi tìm hiểu thế nào là lập trình song song cũng như cách code thì mình phải biết 1 chút về lịch sử hình thành nên ở bài 1 mình sẽ giới thiệu sơ lược những điều bạn nên biết ở lĩnh vực này.

1 lưu ý nhỏ là nếu bạn nào muốn đọc bằng tiếng anh thì có thể ghé qua github, vì tiếng anh nên từ ngữ dùng sẽ chính xác hơn khi viết lại bằng tiếng việt

Những điều bạn nên biết về CPU-GPU

Chắc chắn ở đây phần lớn các bạn đều quen thuộc với 2 từ CPU và GPU, nhưng sẽ có 1 số ít thì không, nên mình sẽ đi nhanh qua 2 khái niệm này và lịch sử của chúng.

CPU là gì?

CPU(Central Processing Units) là bộ xử lý trung tâm của máy tính hay có thể nói là "não bộ" của máy tính. Và bộ não này có thể thực hiện các phép tính liên quan đến số học, đo lường, so sánh, logic, đồng thời nhập hoặc xuất dữ liệu từ các mã lệnh trên máy tính.

Làm thế nào để đánh giá đó 1 là con CPU chất lượng? chúng ta phần lớn sẽ dựa vào 2 thông số:

  • Clock speed - Tốc độ xung nhịp(cũng là "xung nhịp" hoặc "tần số") có thể hiểu đơn giản là tốc độ hoạt động của CPU, tốc độ càng nhanh thì CPU chạy càng lẹ
  • Cores - số lượng nhân(số lượng các đơn vị xử lý độc lập trong CPU).

Sự phát triển và các trở ngại

Cài dat thuan toán song song trong dev c++ năm 2024

biểu đồ mô tả sự biến đổi tần số CPU theo thời gian

Cài dat thuan toán song song trong dev c++ năm 2024

biểu đồ mô tả sự biến đổi công suất CPU theo thời gian

Qua 2 biểu đồ ta có thể thấy sự phát triển nhanh chóng của CPU qua từng giai đoạn nhưng vào giữa năm 2004-2005 đã có sự thụt lùi vậy thì nguyên nhân là gì?

Hai vấn đề chính mà CPU đối mặt vào thời điểm đó là "Power wall" và "Memory wall".

  • Để giải thích 1 cách đơn giản, việc tăng công suất có nghĩa là xử lý nhanh hơn từ các CPU mạnh hơn. Tuy nhiên, CPU mạnh hơn có thể yêu cầu điện áp cao hơn để duy trì tính ổn định ở tốc độ như vậy, do đó không thể tăng tốc độ xung nhịp của bộ xử lý hoài được.
  • Vấn đề thứ hai là độ trễ(latency) khi truy cập bộ nhớ. Nếu máy tính có CPU mạnh nhưng truy cập bộ nhớ kém, nó sẽ mất thời gian lớn, đó là lý do tại sao hiệu suất của nhiều công việc được xác định bởi thời gian truy cập bộ nhớ.

Và từ đó thời kì song song bắt đầu diễn ra với ý tưởng đầu tiên là dual core và càng về sau ta có khái niệm GPU

GPU là gì?

GPU (Graphics Processing Unit) là các thành phần phần cứng chuyên biệt được thiết kế để tăng tốc xử lý đồ họa và các nhiệm vụ tính toán song song. Ban đầu, chúng được phát triển để sử dụng trong các trò chơi máy tính và ứng dụng đồ họa, nhưng hiện nay chúng cũng được sử dụng rộng rãi trong các ứng dụng khoa học và kỹ thuật.

Cài dat thuan toán song song trong dev c++ năm 2024

Những biểu đồ này minh họa rõ ràng sự tính toán nhanh hơn của GPU so với CPU.

Parallel processing

parallel processing tức là xử lý song song vậy thì ở đây chúng ta song song điều gì?

Giả sử một giáo viên giao cho một lớp học 10 câu hỏi để trả lời. Giải pháp đơn giản nhất là để học sinh giỏi nhất trong lớp hoàn thành tất cả 10 câu hỏi. Tuy nhiên, nếu chúng ta có thể tìm và đào tạo 9 học sinh còn lại để trở nên giỏi như học sinh giỏi nhất, chúng ta có thể tăng tốc quá trình trả lời câu hỏi lên 10 lần và đó chính là cách GPU hoạt động.

Tóm lại, việc tiết kiệm thời gian bằng cách chia nhỏ nhiệm vụ lớn thành những nhiệm vụ nhỏ hơn có thể được xử lý đồng thời là lợi ích tốt nhất của xử lý song song.

Tuy nhiên, có một số hạn chế khi chia nhỏ nhiệm vụ, ví dụ như khi giáo viên giao cho lớp N câu hỏi nhưng chúng ta không thể tìm được đủ N học sinh để chuẩn bị và hoàn thành các nhiệm vụ đó hoặc là các bài toán đều liên quan đến nhau, đáp án câu 1 là lời giải cho câu 2 chẳng hạn.

Tóm lại qua bài viết này bạn đã hiểu 1 số khái niệm về cpu-gpu và xin chúc mừng là bạn bắt đầu nhập môn vào lập trình song song trên gpu bằg cuda-C 😆😆

OpenMP. Món này rất dễ nhưng khá ít người biết và sử dụng, nếu các bạn biết áp dụng thì cũng rất thú vị và làm cho code của chúng ta “chất” và “ngầu” vl luôn 😎.


OpenMP là gì ?

OpenMP là tên một tính năng built-in sẵn trong hầu hết các C++ compiler, bạn chỉ cần chỉ định option build khi compile code là có thể sử dụng ngay mà không cần link thêm bất cứ thư viện nào khác. OpenMP có thể được sử dụng để các LTV truyền đạt cho compiler biết rằng một số đoạn code nào đó là độc lập vào cần chạy song song với nhau ở các thread khác nhau để tăng tốc độ xử lý cho chương trình.


Những compiler nào support OpenMP ?

  • GCC (GNU Compiler Collection) support OpenMP 4.5 từ GCC version 6.1, OpenMP 4.0 từ GCC version 4.9, OpenMP 3.1 từ GCC version 4.7, OpenMP 3.0 từ GCC version 4.4, và OpenMP 2.5 từ GCC version 4.2. Thêm option -fopenmp để enable OpenMP.
  • Clang++ support OpenMP 4.5 từ version 3.9 (without offloading), OpenMP 4.0 từ version 3.8 và OpenMP 3.1 từ version 3.7. Thêm option -fopenmp để enable OpenMP.
  • Microsoft Visual C++ (cl) support OpenMP 2.0 từ version 2005. Thêm option /openmp để enable OpenMP.
  • Ngoài ra còn Solaris Studio, Intel C Compiler (icc) cũng support OpenMP

Làm thế nào để có thể sử dụng tính năng OpenMP trong C++ ?

  • Enable tính năng OpenMP của compiler:
    • Với Visual Studio: Có thể enable trên IDE bằng cách đi đến Properties → C/C++ → Language → OpenMP và set là Yes (/openmp)

Cài dat thuan toán song song trong dev c++ năm 2024

  • * Với GCC, Clang++: Thêm option -fopenmp vào câu lệnh compile source code. Ví dụ:
  • Include file header omp.h
  • Sử dụng các cú pháp của OpenMP: cụ thể ở phần sau ↓

Sử dụng Parallel Sections trong OpenMP

Các xử lý mà bạn muốn chỉ thị cho compiler biết rằng chúng cần được chạy song song ở các thread khác nhau thì cần đặt vào trong các

pragma omp section bên trong

pragma omp parallel sections như code sample bên dưới →

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

include

include

int main()

{

std::cout << "begin ";

pragma omp parallel sections

{

pragma omp section

{

std::cout << "hello ";

std::cout << "hello ";

std::cout << "hello ";

}

pragma omp section

{

std::cout << "world ";

std::cout << "world ";

std::cout << "world ";

}

}

std::cout << "end" << std::endl;

return 0;

}

2 section với “

pragma omp section” ở đằng trước này sẽ được chạy song song ở 2 thread khác nhau. Chính vì vậy nếu bạn chạy code ở trên thì mỗi lần chạy có thể sẽ ra một kết quả khác nhau. Bên dưới lài kết quả một số lần chạy của mình (trên Visual Studio 2015) →

begin hello hello hello world world world end # lần 1

begin hello world hello hello world world end # lần 2

begin hello hello hello world world world end # lần 3

begin hello hello world world world hello end # lần 4


Sử dụng Parallel For Loop trong OpenMP

Khi các bạn muốn xử lý đặt bên trong một vòng for nào đó được tự động phân chia và chạy song song ở các thread khác nhau thì bạn dùng “

pragma omp parallel for” →

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

include

include

int main()

{

int arr[16];

// parallel for loop

printf("Assign value of arr[]:\n");

pragma omp parallel for

for (int i \= 0; i < 15; ++i) {

arr[i] \= i;

printf(" Thread %d: arr[%d] = %d\n", omp_get_thread_num(), i, arr[i]);

}

// normal for loop

printf("=====================\n");

printf("Check value of arr[]:\n");

for (int i \= 0; i < 15; ++i) {

arr[i] \= i;

printf(" arr[%d] = %d\n", i, arr[i]);

}

return 0;

}

Ở ví dụ trên thì code bên trong vòng for sẽ được tư động chia ra chạy trên nhiều thread, số lần lặp trên mỗi thread sẽ được chia đều nhất có thể giữa các thread. Ở trên do không chỉ định số lượng thread nên số lượng thread sẽ được allocate tự động. Chương trình trên mỗi lần chạy khác nhau có thể print ra màn hình khác nhau nhưng kết quả cuối cùng là giá trị của array arr thì sẽ giống nhau. Dưới đây là kết quả một trong số các lần chạy của chương trình →

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

Assign value of arr[]:

Thread 0: arr[0] \= 0

Thread 0: arr[1] \= 1

Thread 1: arr[4] \= 4

Thread 1: arr[5] \= 5

Thread 1: arr[6] \= 6

Thread 2: arr[8] \= 8

Thread 2: arr[9] \= 9

Thread 2: arr[10] \= 10

Thread 2: arr[11] \= 11

Thread 0: arr[2] \= 2

Thread 0: arr[3] \= 3

Thread 1: arr[7] \= 7

Thread 3: arr[12] \= 12

Thread 3: arr[13] \= 13

Thread 3: arr[14] \= 14

\=====================

Check value of arr[]:

arr[0] \= 0

arr[1] \= 1

arr[2] \= 2

arr[3] \= 3

arr[4] \= 4

arr[5] \= 5

arr[6] \= 6

arr[7] \= 7

arr[8] \= 8

arr[9] \= 9

arr[10] \= 10

arr[11] \= 11

arr[12] \= 12

arr[13] \= 13

arr[14] \= 14

Ta có thể thấy vòng for được chia ra và chạy trên 4 thread có id lần lượt là 0, 1, 2 và 3. Nếu muốn chỉ định chính xác số lượng thread sử dụng thì có thể dùng hàm omp_set_num_threads(), code dưới đây sẽ chỉ định sử dụng 2 thread →

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

include

include

int main()

{

int arr[16];

omp_set_num_threads(2);

// parallel for loop

printf("Assign value of arr[]:\n");

pragma omp parallel for

for (int i \= 0; i < 15; ++i) {

arr[i] \= i;

printf(" Thread %d: arr[%d] = %d\n", omp_get_thread_num(), i, arr[i]);

}

// normal for loop

printf("=====================\n");

printf("Check value of arr[]:\n");

for (int i \= 0; i < 15; ++i) {

arr[i] \= i;

printf(" arr[%d] = %d\n", i, arr[i]);

}

return 0;

}

Chạy thử và ra kết quả như sau, vòng for chỉ chạy trên 2 thread (id = 0, 1) nhưng kết quả cuối cùng của arr[] thì vẫn vậy →

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

Assign value of arr[]:

Thread 0: arr[0] \= 0

Thread 0: arr[1] \= 1

Thread 0: arr[2] \= 2

Thread 1: arr[8] \= 8

Thread 0: arr[3] \= 3

Thread 0: arr[4] \= 4

Thread 0: arr[5] \= 5

Thread 0: arr[6] \= 6

Thread 0: arr[7] \= 7

Thread 1: arr[9] \= 9

Thread 1: arr[10] \= 10

Thread 1: arr[11] \= 11

Thread 1: arr[12] \= 12

Thread 1: arr[13] \= 13

Thread 1: arr[14] \= 14

\=====================

Check value of arr[]:

arr[0] \= 0

arr[1] \= 1

arr[2] \= 2

arr[3] \= 3

arr[4] \= 4

arr[5] \= 5

arr[6] \= 6

arr[7] \= 7

arr[8] \= 8

arr[9] \= 9

arr[10] \= 10

arr[11] \= 11

arr[12] \= 12

arr[13] \= 13

arr[14] \= 14


Thực ra OpenMP còn nhiều cú pháp khác nữa nhưng mình xin tạm dừng về topic ở đây vì không muốn làm nó phức tạp thêm, nên sử dụng ở mức simple như thế này là đủ, mình nghĩ vậy. Nếu anh em nào muốn tìm hiểu thêm thì cứ Google Search keyword “OpenMP C++” sẽ ra cả mớ.