Openmp là gì

Những người khác kể từ đó đã trả lời hầu hết câu hỏi nhưng tôi muốn chỉ ra một số trường hợp cụ thể trong đó một kiểu lập lịch cụ thể phù hợp hơn những kiểu khác. Lập lịch kiểm soát cách phân chia các lần lặp vòng lặp giữa các luồng. Chọn lịch trình phù hợp có thể có tác động lớn đến tốc độ của ứng dụng.

staticlịch trình có nghĩa là các khối lặp lại được ánh xạ tĩnh tới các luồng thực thi theo kiểu vòng lặp. Điều thú vị với lập lịch tĩnh là thời gian chạy OpenMP đảm bảo rằng nếu bạn có hai vòng lặp riêng biệt với cùng số lần lặp và thực thi chúng với cùng số luồng bằng cách sử dụng lập lịch tĩnh, thì mỗi luồng sẽ nhận được chính xác cùng một phạm vi lặp ( s) ở cả hai vùng song song. Điều này khá quan trọng trên hệ thống NUMA: nếu bạn chạm vào bộ nhớ nào đó trong vòng lặp đầu tiên, bộ nhớ đó sẽ nằm trên nút NUMA nơi chuỗi đang thực thi. Sau đó, trong vòng lặp thứ hai, cùng một luồng có thể truy cập cùng một vị trí bộ nhớ nhanh hơn vì nó sẽ nằm trên cùng một nút NUMA.

Hãy tưởng tượng có hai nút NUMA: nút 0 và nút 1, ví dụ như bảng mạch Intel Nehalem hai ổ cắm với CPU 4 nhân trong cả hai ổ cắm. Sau đó, các luồng 0, 1, 2 và 3 sẽ nằm trên nút 0 và các luồng 4, 5, 6 và 7 sẽ nằm trên nút 1:

| | core 0 | thread 0 | | socket 0 | core 1 | thread 1 | | NUMA node 0 | core 2 | thread 2 | | | core 3 | thread 3 | | | core 4 | thread 4 | | socket 1 | core 5 | thread 5 | | NUMA node 1 | core 6 | thread 6 | | | core 7 | thread 7 |

Mỗi lõi có thể truy cập bộ nhớ từ mỗi nút NUMA, nhưng truy cập từ xa chậm hơn (1,5x - chậm hơn 1,9 lần trên Intel) so với truy cập nút cục bộ. Bạn chạy một cái gì đó như thế này:

char *a = (char *)malloc(8*4096); #pragma omp parallel for schedule(static,1) num_threads(8) for (int i = 0; i < 8; i++) memset(&a[i*4096], 0, 4096);

4096 byte trong trường hợp này là kích thước tiêu chuẩn của một trang bộ nhớ trên Linux trên x86 nếu các trang lớn không được sử dụng. Mã này sẽ bằng không toàn bộ mảng 32 KiB a. Lời malloc()gọi chỉ dự trữ không gian địa chỉ ảo nhưng không thực sự "chạm" vào bộ nhớ vật lý (đây là hành vi mặc định trừ khi một số phiên bản khác của mallocđược sử dụng, ví dụ như một phiên bản làm trống bộ nhớ như calloc()vậy). Bây giờ mảng này liền kề nhưng chỉ trong bộ nhớ ảo. Trong bộ nhớ vật lý, một nửa của nó sẽ nằm trong bộ nhớ gắn với ổ cắm 0 và một nửa trong bộ nhớ gắn với ổ cắm 1. Điều này là do các phần khác nhau được làm 0 bởi các luồng khác nhau và các luồng đó nằm trên các lõi khác nhau và có một thứ gọi là lần chạm đầu tiên Chính sách NUMA có nghĩa là các trang bộ nhớ được cấp phát trên nút NUMA nơi chứa chuỗi đầu tiên "chạm vào" trang bộ nhớ.

| | core 0 | thread 0 | a[0] ... a[4095] | socket 0 | core 1 | thread 1 | a[4096] ... a[8191] | NUMA node 0 | core 2 | thread 2 | a[8192] ... a[12287] | | core 3 | thread 3 | a[12288] ... a[16383] | | core 4 | thread 4 | a[16384] ... a[20479] | socket 1 | core 5 | thread 5 | a[20480] ... a[24575] | NUMA node 1 | core 6 | thread 6 | a[24576] ... a[28671] | | core 7 | thread 7 | a[28672] ... a[32768]

Bây giờ chúng ta hãy chạy một vòng lặp khác như thế này:

#pragma omp parallel for schedule(static,1) num_threads(8) for (i = 0; i < 8; i++) memset(&a[i*4096], 1, 4096);

Mỗi luồng sẽ truy cập vào bộ nhớ vật lý đã được ánh xạ và nó sẽ có cùng một ánh xạ luồng tới vùng nhớ như trong vòng lặp đầu tiên. Nó có nghĩa là các luồng sẽ chỉ truy cập bộ nhớ nằm trong các khối bộ nhớ cục bộ của chúng, điều này sẽ nhanh chóng.

Bây giờ tưởng tượng rằng một đề án lập kế hoạch được sử dụng cho vòng lặp thứ hai: schedule(static,2). Thao tác này sẽ "cắt" không gian lặp thành các khối gồm hai lần lặp và tổng cộng sẽ có 4 khối như vậy. Điều gì sẽ xảy ra là chúng ta sẽ có luồng sau đây ánh xạ vị trí bộ nhớ (thông qua số lần lặp):

| | core 0 | thread 0 | a[0] ... a[8191] <- OK, same memory node | socket 0 | core 1 | thread 1 | a[8192] ... a[16383] <- OK, same memory node | NUMA node 0 | core 2 | thread 2 | a[16384] ... a[24575] <- Not OK, remote memory | | core 3 | thread 3 | a[24576] ... a[32768] <- Not OK, remote memory | | core 4 | thread 4 | | socket 1 | core 5 | thread 5 | | NUMA node 1 | core 6 | thread 6 | | | core 7 | thread 7 |

Hai điều tồi tệ xảy ra ở đây:

  • luồng 4 đến 7 vẫn ở chế độ chờ và một nửa khả năng tính toán bị mất;
  • luồng 2 và 3 truy cập bộ nhớ không phải cục bộ và chúng sẽ mất gấp đôi thời gian để kết thúc trong thời gian đó luồng 0 và 1 sẽ không hoạt động.

Vì vậy, một trong những lợi thế của việc sử dụng lập lịch tĩnh là nó cải thiện tính cục bộ trong truy cập bộ nhớ. Điểm bất lợi là sự lựa chọn không tốt của các thông số lập lịch có thể làm hỏng hiệu suất.

dynamiclập lịch hoạt động trên cơ sở "đến trước được phục vụ trước". Hai lần chạy với cùng một số luồng có thể (và rất có thể sẽ) tạo ra các ánh xạ "không gian lặp" -> "luồng" hoàn toàn khác nhau vì người ta có thể dễ dàng xác minh:

$ cat dyn.c #include #include int main (void) { int i; #pragma omp parallel num_threads(8) { #pragma omp for schedule(dynamic,1) for (i = 0; i < 8; i++) printf("[1] iter %0d, tid %0d\n", i, omp_get_thread_num()); #pragma omp for schedule(dynamic,1) for (i = 0; i < 8; i++) printf("[2] iter %0d, tid %0d\n", i, omp_get_thread_num()); } return 0; } $ icc -openmp -o dyn.x dyn.c $ OMP_NUM_THREADS=8 ./dyn.x | sort [1] iter 0, tid 2 [1] iter 1, tid 0 [1] iter 2, tid 7 [1] iter 3, tid 3 [1] iter 4, tid 4 [1] iter 5, tid 1 [1] iter 6, tid 6 [1] iter 7, tid 5 [2] iter 0, tid 0 [2] iter 1, tid 2 [2] iter 2, tid 7 [2] iter 3, tid 3 [2] iter 4, tid 6 [2] iter 5, tid 1 [2] iter 6, tid 5 [2] iter 7, tid 4

(hành vi tương tự được quan sát khi gccđược sử dụng thay thế)

Nếu mã mẫu từ staticphần được chạy với dynamiclập lịch thay vào đó sẽ chỉ có 1/70 (1,4%) cơ hội rằng vị trí ban đầu sẽ được giữ nguyên và 69/70 (98,6%) khả năng xảy ra truy cập từ xa. Thực tế này thường bị bỏ qua và do đó đạt được hiệu suất dưới mức tối ưu.

Có một lý do khác để lựa chọn giữa staticvà dynamiclập lịch - cân bằng khối lượng công việc. Nếu mỗi lần lặp lại khác nhau rất nhiều so với thời gian hoàn thành trung bình thì sự mất cân bằng công việc cao có thể xảy ra trong trường hợp tĩnh. Lấy ví dụ về trường hợp thời gian để hoàn thành một lần lặp tăng tuyến tính với số lần lặp. Nếu không gian lặp được phân chia tĩnh giữa hai luồng, luồng thứ hai sẽ có công việc nhiều hơn gấp ba lần luồng đầu tiên và do đó trong 2/3 thời gian tính toán, luồng đầu tiên sẽ không hoạt động. Lịch trình động giới thiệu một số chi phí bổ sung nhưng trong trường hợp cụ thể đó sẽ dẫn đến việc phân phối khối lượng công việc tốt hơn nhiều. Một kiểu dynamiclập kế hoạch đặc biệt là guidednơi các khối lặp lại nhỏ hơn và nhỏ hơn được trao cho mỗi nhiệm vụ khi công việc tiến triển.

Vì mã được biên dịch trước có thể chạy trên nhiều nền tảng khác nhau, sẽ rất tuyệt nếu người dùng cuối có thể kiểm soát việc lập lịch trình. Đó là lý do tại sao OpenMP cung cấp schedule(runtime)điều khoản đặc biệt . Với runtimeviệc lập lịch, kiểu được lấy từ nội dung của biến môi trường OMP_SCHEDULE. Điều này cho phép kiểm tra các kiểu lập lịch khác nhau mà không cần biên dịch lại ứng dụng và cũng cho phép người dùng cuối tinh chỉnh cho nền tảng của họ.

Chào mọi người, gần đây Mark có nghiên cứu về lập trình xử lý song song openMP, muốn ghi chép lại những gì đã học được, mục đích là để khi nào quên thì lôi ra xem lại, đồng thời chia sẻ với những ai đang tìm hiểu ~

Hy vọng những ai hiện đang nghiên cứu về lĩnh vực này có thể chỉ giáo thêm ! ^^

-------------------------------------------------------------------------------------------------------------------------

-------------------------------------------------------------------------------------------------------------------------

Sơ Lược Về  "multi thread"

Phần lớn lập trình chỉ chạy với một nhân (thread), đến khi chạy hết công suất 100%, mới chuyển tiếp qua dùng nhân tiếp theo, gọi là lập trình single thread, nếu như thế đối với CPU nhiều nhân thì cũng không thể nâng cao hiệu năng của hệ thống ( ý nói về thời gian xử lý). Nhưng đối với lập trình multi thread, thì có thể đồng bộ hóa các nhân, xử lí tính toán cùng một lúc, như vậy thời gian xử lý sẽ nhanh hơn rất nhiều.

  Lấy một ví dụ đơn giản, nếu dùng single thread để giải quyết một sự việc, làm một lần hết 10s, nếu phải làm 10 lần thì 10s*10 = 100s, cần 100s mới có thể hoàn tất công việc; nhưng nếu dùng multi thread đối với CPU 2 nhân, chúng ta có thể chia công việc cho 2 nhân riêng biệt, mỗi nhân xử lý 5 lần, như vậy thời gian hoàn tất công việc chỉ cần khoảng 50s thôi;

 Nhưng trên thực tế, lập trình multi thread không đơn giản như mình nghĩ. Nào là phân chia công việc, phân chia dữ liệu, cũng tốn thêm thời gian, đối với CPU 2 nhân xử lý trong trường hợp lý tưởng nhất thì hiệu ứng hệ thống cũng không thể là 1 + 1 = 2 được. Hơn nữa, không phải loại công việc nào cũng có thể phân chia để đồng bộ hóa được ! rất nhiều loại công việc đều có sự liên kết với nhau, như vậy nếu phân chia cho các nhân để xử lý riêng biệt, kết quả cho ra chắc chắn có vấn đề. Ngoài ra multi thread khi lập trình và sửa chữa đều khó khăn và phức tạp hơn so với single thread.

   Hiện tại CPU máy tính ngày càng phát triển, 2 nhân, 4 nhân ... và cao nhất là 16 nhân, đồng thời có loại một bản mạch cao nhất có thể dùng 4 CPU 16,  4 * 16 = 64 nhân. Dùng nhiều nhân để tính toán, thời gian xử lý chắc chắn sẽ giảm được rất nhiều lần.!

--------------------------------------------------------------------------------------------------------------------------

Nếu bạn đang dùng Visual C++, chỉ cần vào Project -> projecties ->C/C++ ->Language->OpenMP Support mở lên (tham số /openmp), như vậy VC++ khi biên dịch có thể sử dụng ngữ pháp của OpenMP rồi;


Khi lập trình với OpenMP, các bạn nên include OpenMP vào trong header file nhé !

Lập trình OpenMP có thể thông qua những dòng lệnh cấp cao, rất đơn giản để chuyển thành dạng xử lý song song, đạt kết quả tăng tốc cho hệ thống bằng cách dùng multi thread API; Trong trường hợp đơn giản nhất, chỉ cần một dòng lệnh là có thể khống chế các vòng lặp thành xử lý song song !

Nếu muốn chuyển vòng lặp for thành dạng xử lý song song , phải làm thế nào ?

Rất đơn giản , chỉ cần thêm vào trước vòng lặp một dòng lệnh :

như vậy là OK rồi ! Để hiểu rõ hơn, chúng ta cùng xem đoạn lập trình đơn giản này nhé !



Openmp là gì













    Ở đoạn code trên, trong hàm main() là một vòng lặp đơn giản, chạy 10 lần, mỗi lần đều gọi hàm show() và in ra các giá trị của i .

    Nếu lập trình trên giao diện Ubuntu, chúng ta có thể biên dịch như sau :
Ví dụ file chương trình là test.c
Ta có thể biên dịch :     gcc  test.c   -o  test
Sau đó chạy chương trình :     ./test

Đương nhiên ta sẽ được kết quả :



Openmp là gì




Nếu muốn dùng OpenMP để chuyển vòng lặp for thành xử lý song song, chúng ta chỉ cần sửa thêm vào như sau :



Openmp là gì


Đơn giản, từ đầu đến cuối chỉ cần thêm vào 2 dòng (trong khung màu xanh), và chúng ta bắt đầu chạy chương trình. Đối với hệ thống Ubuntu, ta cần phải include thêm thư viện của OpenMP vào nữa, chúng ta biên dịch như sau :

   biên dịch  :    gcc   -fopenmp   test.c   -o   testOMP


Openmp là gì




Từ kết quả trên, chúng ta có thể thấy, hệ thống không chạy theo thứ tự từ 0 đến 9 nữa ! mà OpenMP đã phân chia vòng lặp từ 0 - 9 thành 2 phần là 0 - 4, và 5 - 9, sau đó giao cho từng thread xử lý, bởi vậy chúng ta mới có kết quả như vậy.


Ở đây chỉ là ví dụ đơn giản, chưa hiện rõ sự khác biệt về tốc độ xử lý, nếu như cần phải tính toán với lượng dữ liệu lớn , và mức tính toán phức tạp hơn thì các bạn có thể thấy rõ hơn về công dụng của OpenMP trong trình xử lý song song. Khi cho chương trình chạy, chúng ta có thể thông qua trình quản lý để xem các nhân đang hoạt động như thế nào. Khi chạy với lập trình OpenMP,  thông thường các nhân đều chạy với công suất 100%...hehe...Nhưng các bạn cứ yên tâm, có lẽ sẽ không bị đứng máy đâu !



--------------------------------------------------------------------------------------------------------------------------


Qua bài viết này, chúng ta mới chỉ học được 2 dòng lệnh , thực tế thì OpenMP còn nhiều lệnh khác, nhưng thực sự dùng đến cũng không nhiều. Bài viết sau, mình sẽ giới thiệu thêm !


Nếu có chỗ nào còn chưa đúng, xin các bạn góp ý chỉ bảo  !