Bài viết này nhằm mục đích làm nổi bật tiềm năng của việc tối ưu hóa trình biên dịch, tập trung vào trình biên dịch Intel C++ — nổi tiếng về tính phổ biến và được sử dụng rộng rãi.
Điểm nổi bật: Tối ưu hóa trình biên dịch là gì? | -Bật | Kiến trúc được nhắm mục tiêu | Tối ưu hóa liên thủ tục | -fno-bí danh | Báo cáo Tối ưu hóa trình biên dịch
Bất kỳ trình biên dịch nào cũng thực hiện một loạt các bước để chuyển đổi mã nguồn cấp cao sang mã máy cấp thấp. Chúng liên quan đến phân tích từ vựng, phân tích cú pháp, phân tích ngữ nghĩa, tạo mã trung gian (hoặc IR), tối ưu hóa và tạo mã.
Trong giai đoạn tối ưu hóa, trình biên dịch tìm kiếm một cách tỉ mỉ các cách để chuyển đổi chương trình, nhằm đạt được đầu ra tương đương về mặt ngữ nghĩa, sử dụng ít tài nguyên hơn hoặc thực thi nhanh hơn. Các kỹ thuật được sử dụng trong quy trình này bao gồm nhưng không giới hạn ở việc gấp liên tục, tối ưu hóa vòng lặp, nội tuyến chức năng và loại bỏ mã chết .
Các nhà phát triển có thể chỉ định một bộ cờ trình biên dịch trong quá trình biên dịch, một cách làm quen thuộc với những người sử dụng các tùy chọn như “ -g” hoặc “-pg” với GCC để gỡ lỗi và lập hồ sơ thông tin. Khi tiếp tục, chúng ta sẽ thảo luận về các cờ trình biên dịch tương tự mà chúng ta có thể sử dụng khi biên dịch ứng dụng của mình bằng trình biên dịch Intel C++. Những điều này có thể giúp bạn cải thiện hiệu quả và hiệu suất của mã.
u(x,y,t) là nhiệt độ tại điểm (x,y) tại thời điểm t.
Về cơ bản, chúng tôi có mã hóa C++ thực hiện các lần lặp Jacobi trên các lưới có kích thước thay đổi (mà chúng tôi gọi là độ phân giải). Về cơ bản, kích thước lưới 500 có nghĩa là giải quyết ma trận có kích thước 500x500, v.v.
/* * One Jacobi iteration step */ void jacobi(double *u, double *unew, unsigned sizex, unsigned sizey) { int i, j; for (j = 1; j < sizex - 1; j++) { for (i = 1; i < sizey - 1; i++) { unew[i * sizex + j] = 0.25 * (u[i * sizex + (j - 1)] + // left u[i * sizex + (j + 1)] + // right u[(i - 1) * sizex + j] + // top u[(i + 1) * sizex + j]); // bottom } } for (j = 1; j < sizex - 1; j++) { for (i = 1; i < sizey - 1; i++) { u[i * sizex + j] = unew[i * sizex + j]; } } }
MFLOP/s là viết tắt của “Triệu phép tính dấu phẩy động mỗi giây”. Nó là một đơn vị đo lường được sử dụng để định lượng hiệu suất của máy tính hoặc bộ xử lý dưới dạng các phép toán dấu phẩy động. Các phép toán dấu phẩy động bao gồm các phép tính toán học với số thập phân hoặc số thực được biểu diễn dưới dạng dấu phẩy động.
Lưu ý 1: Để cung cấp kết quả ổn định, tôi chạy tệp thực thi 5 lần cho mỗi độ phân giải và lấy giá trị trung bình của các giá trị MFLOP/s.
Lưu ý 2: Điều quan trọng cần lưu ý là tối ưu hóa mặc định trên trình biên dịch Intel C++ là -O2. Vì vậy, điều quan trọng là phải chỉ định -O0 khi biên dịch mã nguồn.
Đây là một số cờ trình biên dịch được sử dụng phổ biến nhất khi bắt đầu tối ưu hóa trình biên dịch. Trong trường hợp lý tưởng, hiệu suất của Ofast > O3 > O2 > O1 > O0 . Tuy nhiên, điều này không nhất thiết phải xảy ra. Điểm quan trọng của các tùy chọn này như sau:
-O1:
-O2:
-O3:
-Ofast:
Rõ ràng là tất cả những tối ưu hóa này đều nhanh hơn nhiều so với mã cơ sở của chúng tôi (với “-O0”). Thời gian chạy thực thi thấp hơn 2–3 lần so với trường hợp cơ bản. Còn MFLOP/s thì sao??
Nhìn chung, mặc dù chỉ một chút nhưng “-O3” hoạt động tốt nhất.
Các cờ bổ sung được sử dụng bởi “- Ofast ” (“ -no-prec-div -fp-model fast=2 ”) không mang lại bất kỳ sự tăng tốc bổ sung nào.
Câu trả lời nằm ở các cờ biên dịch chiến lược. Thử nghiệm với các tùy chọn như “ -xHost ” và chính xác hơn là “ -xCORE-AVX512 ” có thể cho phép chúng tôi khai thác toàn bộ tiềm năng của máy và điều chỉnh tối ưu hóa để có hiệu suất tối ưu.
-xHost:
-xCORE-AVX512:
Mục tiêu: Hướng dẫn rõ ràng trình biên dịch tạo mã sử dụng bộ hướng dẫn Intel Advanced Vector Extensions 512 (AVX-512).
Các tính năng chính: AVX-512 là bộ lệnh SIMD (Một lệnh, Nhiều dữ liệu) nâng cao cung cấp các thanh ghi vectơ rộng hơn và các hoạt động bổ sung so với các phiên bản trước như AVX2. Việc bật cờ này cho phép trình biên dịch tận dụng các tính năng nâng cao này để có hiệu suất được tối ưu hóa.
Cân nhắc: Tính di động một lần nữa là thủ phạm ở đây. Các tệp nhị phân được tạo bằng lệnh AVX-512 có thể không chạy tối ưu trên các bộ xử lý không hỗ trợ bộ lệnh này. Chúng có thể không hoạt động chút nào!
Theo mặc định, “ -xCORE-AVX512 ” giả định rằng chương trình sẽ không được hưởng lợi từ việc sử dụng thanh ghi zmm. Trình biên dịch tránh sử dụng các thanh ghi zmm trừ khi đảm bảo đạt được hiệu suất.
Nếu một người có kế hoạch sử dụng các thanh ghi zmm mà không bị hạn chế, “ ” có thể được đặt ở mức cao. Đó cũng là điều chúng tôi sẽ làm.
Tuyệt vời!
Điều đáng chú ý là chúng tôi đã đạt được những kết quả này mà không cần bất kỳ sự can thiệp thủ công đáng kể nào — chỉ bằng cách kết hợp một số cờ trình biên dịch trong quá trình biên dịch ứng dụng.
Lưu ý: Đừng lo lắng nếu phần cứng của bạn không hỗ trợ AVX-512. Trình biên dịch Intel C++ hỗ trợ tối ưu hóa cho AVX, AVX-2 và thậm chí cả SSE. có mọi thứ bạn cần biết!
IPO là một quy trình gồm nhiều bước tập trung vào sự tương tác giữa các chức năng hoặc quy trình khác nhau trong một chương trình. IPO có thể bao gồm nhiều loại tối ưu hóa khác nhau, bao gồm Thay thế chuyển tiếp, Chuyển đổi cuộc gọi gián tiếp và Nội tuyến.
-ipo:
Mục tiêu: Cho phép tối ưu hóa liên thủ tục, cho phép trình biên dịch phân tích và tối ưu hóa toàn bộ chương trình, ngoài các tệp nguồn riêng lẻ, trong quá trình biên dịch.
Các tính năng chính:- Tối ưu hóa toàn bộ chương trình: “ -ipo ” thực hiện phân tích và tối ưu hóa trên tất cả các tệp nguồn, xem xét sự tương tác giữa các chức năng và quy trình trong toàn bộ chương trình.- Tối ưu hóa chức năng chéo và mô-đun chéo: Cờ tạo điều kiện cho các chức năng nội tuyến, đồng bộ hóa tối ưu hóa và phân tích luồng dữ liệu trên các phần chương trình khác nhau.
Cân nhắc: Nó yêu cầu một bước liên kết riêng. Sau khi biên dịch bằng “ -ipo ”, cần có một bước liên kết cụ thể để tạo tệp thực thi cuối cùng. Trình biên dịch thực hiện các tối ưu hóa bổ sung dựa trên toàn bộ chế độ xem chương trình trong quá trình liên kết.
-ip:
Mục tiêu: Cho phép truyền bá phân tích liên thủ tục, cho phép trình biên dịch thực hiện một số tối ưu hóa liên thủ tục mà không yêu cầu bước liên kết riêng.
Các tính năng chính:- Phân tích và truyền bá: “ -ip ” cho phép trình biên dịch thực hiện nghiên cứu và truyền dữ liệu qua các chức năng và mô-đun khác nhau trong quá trình biên dịch. Tuy nhiên, nó không thực hiện tất cả các tối ưu hóa yêu cầu xem toàn bộ chương trình.- Biên dịch nhanh hơn: Không giống như “ -ipo ”, “ -ip ” không cần một bước liên kết riêng biệt, dẫn đến thời gian biên dịch nhanh hơn. Điều này có thể có lợi trong quá trình phát triển khi phản hồi nhanh là cần thiết.
Cân nhắc: Chỉ xảy ra một số tối ưu hóa liên thủ tục có giới hạn, bao gồm cả nội tuyến hàm.
-ipo thường cung cấp khả năng tối ưu hóa liên thủ tục rộng rãi hơn vì nó bao gồm một bước liên kết riêng biệt nhưng phải trả giá bằng thời gian biên dịch lâu hơn. [ ] -ip là một giải pháp thay thế nhanh hơn, thực hiện một số tối ưu hóa liên thủ tục mà không yêu cầu bước liên kết riêng, giúp nó phù hợp cho các giai đoạn phát triển và thử nghiệm.[ ]
Vì chúng tôi chỉ nói về hiệu suất và các cách tối ưu hóa khác nhau, thời gian biên dịch hoặc kích thước của tệp thực thi không phải là mối quan tâm của chúng tôi nên chúng tôi sẽ tập trung vào “ -ipo ”.
/* * One Jacobi iteration step */ void jacobi(double *u, double *unew, unsigned sizex, unsigned sizey) { int i, j; for (j = 1; j < sizex - 1; j++) { for (i = 1; i < sizey - 1; i++) { unew[i * sizex + j] = 0.25 * (u[i * sizex + (j - 1)] + // left u[i * sizex + (j + 1)] + // right u[(i - 1) * sizex + j] + // top u[(i + 1) * sizex + j]); // bottom } } for (j = 1; j < sizex - 1; j++) { for (i = 1; i < sizey - 1; i++) { u[i * sizex + j] = unew[i * sizex + j]; } } }
Hàm jacobi() lấy một vài con trỏ để nhân đôi làm tham số và sau đó thực hiện điều gì đó bên trong các vòng lặp for lồng nhau. Khi bất kỳ trình biên dịch nào nhìn thấy hàm này trong tệp nguồn, nó phải hết sức cẩn thận.
Biểu thức để tính unew bằng u bao gồm giá trị trung bình của 4 giá trị u lân cận. Điều gì sẽ xảy ra nếu cả bạn và unew đều trỏ đến cùng một vị trí? Điều này sẽ trở thành vấn đề cổ điển của con trỏ bí danh [ ].
Các trình biên dịch hiện đại rất thông minh và để đảm bảo an toàn, họ cho rằng có thể tạo ra bí danh. Và đối với những tình huống như thế này, họ tránh mọi tối ưu hóa có thể ảnh hưởng đến ngữ nghĩa và đầu ra của mã.
Trong trường hợp của chúng tôi, chúng tôi biết rằng u và unew là các vị trí bộ nhớ khác nhau và có mục đích lưu trữ các giá trị khác nhau. Vì vậy, chúng ta có thể dễ dàng cho trình biên dịch biết rằng sẽ không có bất kỳ bí danh nào ở đây.
Có hai phương pháp. Đầu tiên là từ khóa C “ ” . Nhưng nó đòi hỏi phải thay đổi mã. Chúng tôi không muốn điều đó vào lúc này.
Có gì đơn giản không? Hãy thử “ -fno-alias ”.
-fno-bí danh:
Mục tiêu: Hướng dẫn trình biên dịch không giả định bí danh trong chương trình.
Các tính năng chính: Giả sử không có bí danh, trình biên dịch có thể tối ưu hóa mã một cách tự do hơn, có khả năng cải thiện hiệu suất.
Cân nhắc: Nhà phát triển phải cẩn thận khi sử dụng cờ này vì trong trường hợp có bất kỳ bí danh không chính đáng nào, chương trình có thể đưa ra kết quả đầu ra không mong muốn.
Chà, bây giờ chúng ta có một cái gì đó !!!
Việc kiểm tra kỹ hơn mã hợp ngữ (mặc dù không được chia sẻ ở đây) và báo cáo tối ưu hóa biên dịch được tạo (xem bên dưới ) cho thấy ứng dụng hiểu biết của trình biên dịch về và . Những chuyển đổi này góp phần mang lại hiệu suất được tối ưu hóa cao, cho thấy tác động đáng kể của các chỉ thị của trình biên dịch đối với hiệu quả của mã.
Trình biên dịch Intel C++ cung cấp một tính năng có giá trị cho phép người dùng tạo báo cáo tối ưu hóa tóm tắt tất cả các điều chỉnh được thực hiện cho mục đích tối ưu hóa [ ]. Báo cáo toàn diện này được lưu ở định dạng tệp YAML, trình bày danh sách chi tiết các tối ưu hóa được trình biên dịch áp dụng trong mã. Để biết mô tả chi tiết, hãy xem tài liệu chính thức về “ ”.
Tương tự, các trình biên dịch Intel C++ (và tất cả các trình biên dịch phổ biến) cũng hỗ trợ các lệnh pragma, đây là những tính năng rất hay. Bạn nên kiểm tra một số pragma như ivdep, Parallel, simd, vector, v.v. trên .