Theo dõi hồi quy 25% trên LLVM RISC-V
Tracking down a 25% Regression on LLVM RISC-V
Tương tự như bài đăng trước, bài đăng này trình bày phân tích của tôi về điểm chuẩn trên các mục tiêu RISC-V. Không giống như bài đăng trước, tôi đã có thể đưa ra một bản vá để loại bỏ khoảng cách về hiệu suất so với GCC (đối với điểm chuẩn này)!
Tương tự như bài đăng trước, bài đăng này trình bày phân tích của tôi về điểm chuẩn trên các mục tiêu RISC-V. Không giống như bài đăng trước, tôi đã có thể tạo ra một bản vá để loại bỏ khoảng cách về hiệu suất so với GCC (đối với điểm chuẩn này)!
TLDR
Một LLVM commit gần đây đã cải thiện isKnownExactCastIntToFP để gấp fpext(sitofp x to float) thành nhân đôi thành uitofp x trực tiếp để nhân đôi, nhưng điều này vô tình phá vỡ tối ưu hóa thu hẹp xuôi dòng trong visitFPTrunc dựa vào fpext để thu hẹp gấp đôi thành nổi, gây ra hiệu suất hồi quy ~24% đối với các mục tiêu RISC-V, trong đó fdiv.d (độ trễ 33 chu kỳ) được phát ra thay vì fdiv.s (19 chu kỳ độ trễ).
sửa lỗi của tôi mở rộng getMinimumFPType với phân tích phạm vi để nhận ra rằng fptrunc(uitofp x double) để nổi có thể giảm xuống uitofp x nổi , khôi phục thu hẹp tối ưu hóa.
Phân tích
Tôi đang xem trang web của Igalia để so sánh hiệu suất của LLVM sang GCC trên các mục tiêu RISCV và tôi nhận thấy điểm chuẩn.
cụ thể này![]()
Như được hiển thị trong hình ảnh bên dưới, LLVM yêu cầu chu kỳ nhiều hơn khoảng ~8% so với GCC cho điểm chuẩn cụ thể đó trên CPU SiFive P550.
Tôi đã bao gồm các đoạn của tập hợp khối cơ bản có liên quan. Trên thực tế, tất cả các chu trình đều được dành cho việc lắp ráp bên dưới.
LLVM
![]()
GCC
![]()
Từ hai cuộc họp, tôi không thấy rõ ngay tại sao GCC lại hoạt động tốt hơn. Chúng có vẻ gần như giống hệt nhau và nếu có thì LLVM đã có thể tối ưu hóa logic nhánh ở đây. Điểm khác biệt lớn nhất mà tôi nhận thấy là LLVM đang thực hiện phép chia fdiv.d, một phép chia có dấu phẩy động có độ chính xác gấp đôi hoặc f64.
Điều này có vẻ đầy hứa hẹn nên tôi quyết định chạy llvm-mca trên mã nguồn để hiểu rõ hơn về những gì đang xảy ra. Lưu ý rằng tôi đang sử dụng llvm-mca được xây dựng từ nguồn cách đây vài ngày từ thượng nguồn. Từ phân tích do llvm-mca thực hiện, tôi nhận thấy rằng lệnh fdiv.d không xuất hiện trong vòng lặp. Mặc dù tôi không hiển thị ở trên nhưng cả GCC và LLVM đều chứa lệnh fdiv.d trong khối cơ bản sau này nhưng lệnh này nằm ngoài vòng lặp chính và do đó không liên quan đến sự khác biệt về hiệu suất.
Thông tin
llvm-mca là một công cụ trong bộ llvm có thể dùng để đo tĩnh hiệu suất của mã máy cho một CPU cụ thể.
$LLVM_BUILD_DIR/bin/llvm-mca -mtriple=riscv64 -mcpu=sif-p550 pi.s ![]()
Xung quanh khu vực lẽ ra phải có fdiv.d, có hai fdiv.s như GCC. Từ đó, tôi kết luận rằng lệnh fdiv.d trong vòng lặp chắc chắn là một sự hồi quy gần đây. LLVM trước đây có thể thu hẹp double thành float nhưng các bản dựng mới nhất không còn có thể chuyển double thành float nữa.
Tôi xác nhận đây thực sự là một sự hồi quy bằng cách so sánh LLVM với bản dựng trước đó cho cùng một CPU và điểm chuẩn.
https://cc-perf.igalia.com/db_default/v4/nts/profile/260/406/4
![]()
Dưới đây là tập hợp được tạo bởi bản dựng LLVM trước đó.
![]()
Không có hướng dẫn fdiv.d trên bản dựng trước. Thay vào đó, fdiv.s được sử dụng.
Thực thi không theo thứ tự
Nếu bạn thắc mắc tại sao thứ tự của cụm trên và cụm từ bản dựng LLVM mới trông khác nhau, thì đó là do CPU mục tiêu, SiFive P550, là CPU không đúng thứ tự. Không giống như CPU trong Banana Pi được đề cập ở bài trước là CPU theo thứ tự, mục tiêu này có thể thực thi các lệnh theo thứ tự có thể tạo ra thông lượng cao hơn.
Tôi không chắc chắn chính xác lý do tại sao fdiv.d lại cao hơn nhưng tôi nghi ngờ rằng do phép chia kép có độ trễ cao hơn đáng kể nên CPU đang cố gắng gửi các lệnh khác để 'ẩn' độ trễ này. Lệnh fcvt.s.d sử dụng giá trị của fdiv.d, ft1, sẽ cần đợi 33 chu kỳ, vì vậy có lẽ CPU đã quyết định lên lịch các lệnh giữa chúng.
Sự việc này đang diễn ra ở đâu?
Tại thời điểm này, chúng tôi không biết tại sao điều này lại xảy ra nhưng một bước đi đúng hướng sẽ là tìm ra nó xảy ra ở đâu. Có lẽ đó là một số thay đổi trong phần phụ trợ RISCV? Lệnh bên dưới cung cấp cho chúng tôi các cam kết liên quan đến RISC-V.
git log --after="2026-04-01 00:11" --trước="2026-04-04 00:10" | grep -E "RISC"[RISCV] Chọn add(vec, splat(scalar)) to PADD_*S cho phần mở rộng P (#190303)
[RISCV] Cho phép liên kếtVSETVLI di chuyển LI nếu nó cho phép biến đổi vsetvli. (#190287)
[RISCV][TTI] Cập nhật chi phí và ngăn chặn việc vượt quá m8 cho vector.extract.last.active (#188160)
[RISCV] Kiểm tra EnsureWholeVectorRegisterMoveValidVTYPE trong RISCVInsertVSETVLI::transferBefore. (#190022)
[RISCV] Loại bỏ codegen cho vp_ctlz, vp_cttz, vp_ctpop (#189904)
Một phần công việc nhằm loại bỏ nội tại VP tầm thường khỏi RISC-V
[RISCV] Di chuyển hướng dẫn chưa ghép nối trở lại trong RISCVLoadStoreOptimizer (#189912)
`RISCVLoadStoreOptimizer` di chuyển lệnh liền kề với hướng dẫn khác
[RISCV] Sửa kích thước NOP cắt bóng stackmap cho các mục tiêu bị nén (#189774)
[RISCV] Giảm bớt ràng buộc VL trong ConvertSameMaskVMergeToVMv (#189797)
[RISCV] Thêm SATI_RV64/USATI_RV64 tới RISCVOptWInstrs. (#190030)
và mã hóa RISCVISD::SATI sử dụng độ rộng loại trừ đi một.
[RISCV][MCA] Cập nhật các bài kiểm tra si-fi-p670 để sử dụng các tệp đầu vào thay thế (#189785)
[RISCV] Loại bỏ codegen cho vp_minnum, vp_maxnum (#189899)
Một phần công việc nhằm loại bỏ nội tại VP tầm thường khỏi RISC-V
[RISCV] Thêm RISCVISD::USATI/SATI để tính toánKnownBitsForTargetNode/ComputeNumSignBitsForTargetNode. (#189702)
[RISCV] Thêm xác nhận vào VSETVLIInfo::hasSEWLMULRatioOnly(). NFC (#189799)
[RISCV] Combine-is_fpclass.ll - thêm các thử nghiệm ban đầu cho thấy lỗi đối với các nút ISD :: IS_FPCLASS liên tục (#189940)
[RISCV] Loại bỏ codegen cho nội tại làm tròn float VP (#189896)
Một phần công việc nhằm loại bỏ nội tại VP tầm thường khỏi RISC-V
[RISCV] Loại bỏ codegen cho vp_lrint, vp_llrint (#189714)
Một phần công việc nhằm loại bỏ những nội dung tầm thường của VP khỏi RISC-V
[RISCV] Thêm hỗ trợ codegen cho SATI và USATI. (#189532)Không có cam kết nào trong số này gây ấn tượng ngay lập tức với tôi nên tôi đã loại bỏ chúng. Đang điều tra cấp trung, tôi quyết định xem xét LLVM IR cuối cùng được tạo bởi opt với phiên bản LLVM địa phương đã được vài ngày của tôi. Hãy nhớ rằng bản dựng của tôi là bản dựng 'đang hoạt động'. Đây là LLVM IR được tạo ở đầu quy trình (ngay sau tiếng kêu) và ở cuối quy trình.
Nhận LLVM IR ngay sau quy trình:
$LLVM_BUILD_DIR/bin/clang -O3 \
--target=riscv64-unknown-linux-gnu \
-march=rv64gc_zba_zbb \
--sysroot=/usr/riscv64-linux-gnu \
-Xclang -disable-llvm-passes \
-S -emit-llvm pi.c -o pi_raw_old.llNhận LLVM IR ở cuối quy trình tối ưu hóa:
$LLVM_BUILD_DIR/bin/clang -O3 \
--target=riscv64-unknown-linux-gnu \
-march=rv64gc_zba_zbb \
--sysroot=/usr/riscv64-linux-gnu \
-S -emit-llvm pi.c -o pi.ll biểu đồ LR
%% Luồng trình biên dịch chính
Nguồn[Mã nguồn<br/><b>pi.c</b>] --> FE[<b>Giao diện người dùng</b><br/[[ TAG_2053]]<i>Clang/Lexer/Parser</i>]
sơ đồ phụ MiddleEnd ["<b>Trung cấp</b> (Quy trình tối ưu hóa)"]
hướng LR
ME_Start(<b>Bắt đầu ở mức trung cấp</b><br/><i>Không được tối ưu hóa IR</i>) --> Chọn[<i>InstCombine, LoopUnroll, GVN, v.v.</i>]
Chọn --> ME_End(<b>Cuối Trung cấp</b><br/><i>Được tối ưu hóa IR</i>)
kết thúc
FE --> ME_Start
ME_End --> BE[<b>Backend</b><br/><i>Trình tạo mã/Hướng dẫn Lựa chọn</i>]
ĐƯỢC --> Asm(<b>Assembly</b><br/><i>RISC-V</i>)
%% Đường ống chính tạo kiểu
phong cách Nguồn điền:none,đột quỵ:#888,độ rộng nét:1px
kiểu điền FE:none,đột quỵ:#888,độ rộng nét:1px
kiểu ME_Start điền:none,đột quỵ:#1e88e5,độ rộng nét:2px
kiểu ME_End điền:none,đột quỵ:#1e88e5,độ rộng nét:2px
phong cách điền:none,đột quỵ:#888,độ rộng nét:1px,dấu gạch ngang nét: 5 5
kiểu BE điền:none,đột quỵ:#888,độ rộng nét:1px
kiểu Asm điền:none,đột quỵ:#888,độ rộng nét:1px
kiểu MiddleEnd điền:none,đột quỵ:#444,độ rộng nét:1px,dấu gạch ngang nét: 3 3
Hộp lệnh thu gọn %%
%% Chúng tôi sử dụng <div> và <code> để đảm bảo mọi thứ được chặt chẽ và ngăn chặn khoảng cách cấp khối
Cmd_Raw["<div style='line-height:1.2;'><b>Lệnh 1: Mã IR</b><br/><code style='background:none;color:inherit;'>-Xclang -disable-llvm-passes</code><br/><small>Sản xuất <b>pi_raw_old.ll</b></small></div>"] Cmd_Opt["<div style='line-height:1.2;'><b>Lệnh 2: Đã tối ưu hóa IR</b><br/><code style='background:none;color:inherit;'>-O3 -S -emit-llvm</code><br/><small>Sản xuất <b>pi.ll</b></small></div>"]
%% Kết nối
Cmd_Raw -.-> ME_Start
Cmd_Opt -.-> ME_End
Ghi chú Lệnh Kiểu %%
kiểu Cmd_Raw điền:#fff3e011,đột quỵ:#f57c00,độ rộng nét:1px,rx:10,ry:10
kiểu Cmd_Opt điền:#fff3e011,đột quỵ:#f57c00,độ rộng nét:1px,rx:10,ry:10
Nếu bạn bối rối về những gì tôi đang làm, hy vọng sơ đồ trên minh họa điều này tốt hơn. Nếu cấp trung gian chịu trách nhiệm thu hẹp gấp đôi thành số float thì tôi đang cố gắng biết ý tưởng về những tối ưu hóa đang diễn ra với IR gây ra điều này.
Thay vì thực hiện việc này, bạn cũng có thể sử dụng print-Before và print-after trên một thẻ cụ thể để xem thẻ đó đang làm gì với IR.
Đây là đoạn mã có liên quan của IR từ đầu quy trình tối ưu hóa.
%conv = sitofp i64 %5 đến float ; int -> thả nổi
[[TAG_337] thích gấp đôi ; float -> double
%div3 = fdiv double %conv2, 7.438300e+04 ; fdiv.d
%conv4 = fptrunc double %div3 đến float ; gấp đôi -> floatĐã được chuyển đổi vào phần sau ở cuối kênh.
%conv = uitofp nneg i64 %0 đến float ; int -> float
%conv4 = fdiv float %conv, 7.438300e+04 ; fdiv.s
Chúng ta có thể thấy rằng ở phần cuối của phần giữa, phần gấp đôi đã được thu hẹp thành một phần nổi. Tôi hy vọng từ những đoạn trích này, rõ ràng rằng chính cấp trung gian chịu trách nhiệm thu hẹp giá trị kép ban đầu này thành giá trị nổi.
Nếu bạn bối rối không hiểu tại sao ngay từ đầu những thao tác truyền có vẻ dư thừa này lại được tạo ra thì việc xem nhanh mã nguồn có thể hữu ích.
int main(int argc, char *argv[]) {
float ztot, yran, ymult, ymod, x, y, z, pi, prod;
long int thấp, ixran, itot, j, iprod;
...
cho(j=1; j<=itot;++) {
iprod = 27611 * ixran;
ixran = iprod - 74383*(dài int)(iprod/74383);
x = (float)ixran / 74383.0;
...
}
...
Dòng cụ thể này cho thấy rằng 74383.0 là mã kép trong mã nguồn nhưng có thể vừa với một số float.
x = (float)ixran / 74383.0;Đây là lý do tại sao LLVM IR có fpext chuyển đổi số float thành double và sau đó chuyển đổi fptrunc của double trở lại float.
Điều này có thể hiển nhiên nhưng tôi vẫn sẽ nói rõ điều đó. Nếu chữ ở trên ban đầu có dạng float như bên dưới thì fdiv.d sẽ không bao giờ được tạo ra.
x = (float)ixran / 74383.0f;Với mức độ tối ưu hóa đủ, trình biên dịch vẫn phải có thể nắm bắt được điều gì đó như thế này, nhưng vẫn thật tuyệt khi thấy một thay đổi nhỏ trong mã như vậy lại có thể tạo ra sự khác biệt lớn như vậy. Trong trường hợp này, trên +19% theo chu kỳ!
Bảng bên dưới hiển thị ánh xạ của LLVM IR tới mã nguồn C, cũng như ghi chú ngắn gọn về chức năng của IR được chỉ định.
| LLVM IR | C Nguồn | Ghi chú |
|---|---|---|
%mul = mul nuw nsw i64 %ixran.053, 27611 |
iprod = 27611 * ixran |
nhân số nguyên |
%0 = urem i64 %mul, 74383 |
ixran = iprod - 74383*(long int)(iprod/74383) |
modulo được tối ưu hóa trình biên dịch thông qua urem |
%conv2 = uitofp nneg i64 %0 thành gấp đôi |
(float)ixran |
truyền tới gấp đôi đầu tiên là do 74383.0 là một chữ kép |
%div3 = fdiv kép %conv2, 7.438300e+04 |
/ 74383.0 |
phép chia có độ chính xác gấp đôi vì 74383.0 là một chữ kép trong C |
%conv4 = fptrunc double %div3 thành float |
x = (thả nổi)... |
rõ ràng (float) truyền các đoạn cắt ngắn dẫn đến kết quả nổi |
Bản dựng LLVM tại thời điểm đó đang tạo ra những thứ sau.
%mul = mul nuw nsw i64 %ixran.053, 27611
%0 = urem i64 %mul, 74383
%conv2 = uitofp nneg i64 %0 đến gấp đôi
%div3 = fdiv double %conv2, 7.438300e+04
%conv4 = fptrunc double %div3 to thả nổiChúng ta có thể thấy rằng toán hạng của các thao tác div3 là %conv2 và là giá trị thập phân. %conv2 là kết quả của thao tác truyền chuyển đổi %0 thành gấp đôi nhưng giá trị tối đa của %0, 74383, có thể nằm gọn trong một nổi.
Nhìn vào kết quả từ llvm-mca, chúng ta có thể thấy những điều sau đây cho fdiv.d.
1 33 32,00 fdiv.d ft1, ft1, fa3Điều này cho thấy rằng fdiv.d có độ trễ là 33 chu kỳ, dài hơn đáng kể so với 19 chu kỳ của fdiv.s. Trong so sánh hiệu suất của chúng tôi, bản dựng LLVM cũ hơn đã báo cáo Thông lượng đối ứng (RThroughput) là 86,0, trong khi bản dựng mới hơn đã tăng lên 100,0. RThông lượng biểu thị số chu kỳ xung nhịp mà bộ xử lý phải đợi trước khi nó có thể bắt đầu thực hiện một lệnh khác cùng loại. Vì vậy, càng thấp thì càng tốt.
Điều này cho thấy phép chia kép khá đắt so với phép chia thực.
Trong danh sách các cam kết trong vài ngày qua, tôi thấy cam kết bên dưới ngay lập tức nổi bật.
[InstCombine] Sử dụng CalculateNumSignBits trong isKnownExactCastIntToFP (#190235)
Đối với các phiên bản int-to-FP đã ký, tính toánNumSignBits có thể chứng minh tính chính xác where
computeKnownBits cannot -- e.g. through ashr(shl x, a), b where sign propagation is
tracked precisely but individual known bits are all unknown.InstCombine is an LLVM middle-end optimization pass that combines adjacent or related instructions into single, more efficient operations. It’s broad in its duties, so examples of InstCombine optimizations can vary. For example, InstCombine can reduce x = x * 2 to x = x << 1;
Given that the newest build can no longer cast that integer to a float, this commit message with the int-to-FP casts (sitofp/uitofp) immediately roused my suspicions, and they were confirmed when I built LLVM before and after that commit. Trước thay đổi này, mọi thứ đều hoạt động và sau đó, hướng dẫn fdiv.d xuất hiện trong tập hợp.
Tôi cũng cần lưu ý rằng bản vá này là một cải tiến - thẻ InstCombiner có sẵn thêm thông tin - nhưng đôi khi các cải tiến có thể gây ra (các) sự hồi quy ở nơi khác theo những cách không thể đoán trước. Tôi cho rằng điều này mang lại cơ hội cho những người như tôi đóng góp 😅
Tại sao điều này lại xảy ra?
Nhìn vào sự khác biệt trong cam kết này, tôi có thể thấy rằng tác giả của bản vá đã thêm phân tích bit đã biết vào hàm isKnownExactCastIntToFP.
static bool isKnownExactCastIntToFP(CastInst &I, InstCombinerImpl &IC) {
...
// Đối với sitofp, dấu ánh xạ tới bit dấu FP, do đó chỉ các bit cường độ
// (BitWidth - NumSignBits) tiêu thụ mantissa.
if (Đã ký) {
SigBits =
(int)SrcTy->getScalarSizeInBits() - IC.ComputeNumSignBits(Src, &I);
if (SigBits <= DestNumSigBits)
return đúng;
}
return sai;
Bạn không cần hiểu sâu về điều gì đã thay đổi điều này đã làm, nhưng chúng tôi có thể suy ra rằng thay đổi này đang khiến isKnownExactCastIntToFP trả về true, trong khi trước đó nó đã trả về false. Vì vậy, tôi quyết định xem xét tất cả các trang web cuộc gọi của isKnownExactCastIntToFP để hiểu rõ hơn về cách điều này có thể dẫn đến hồi quy.
Hàm sau gọi isKnownExactCastIntToFP. Nó được gọi để giảm lệnh fpext nếu lệnh trước chuyển một số nguyên thành FP như sau: itofp i64 x to float -> fpext float x to double. Thay vào đó, điều này chỉ có thể được giảm xuống thành itofp i64 x thành gấp đôi.
Hướng dẫn *InstCombinerImpl::visitFPExt(CastInst &FPExt) {
// Nếu toán hạng nguồn được chuyển từ số nguyên sang FP và được biết chính xác, sau đó
// truyền trực tiếp toán hạng số nguyên sang loại đích.
Nhập *Ty = FPExt.getType();
Giá trị *Src = FPSxt.getOperand(0);
if (isa<SIToFPInst>(Src) || isa<UIToFPInst>(Src)) {
tự động *FPCast = diễn viên<CastInst>(Src);
if (isKnownExactCastIntToFP(*FPCast))
trở lại CastInst::Tạo(FPCast->getOpcode(), FPcast->getOperand(0), Ty);
}
return commonCastTransforms(FPExt);
Và điều này kết hợp chúng lại với nhau.
Tham chiếu LLVM IR được tạo ở đầu quy trình.
%conv = sitofp i64 %5 đến float ; int -> phao
%conv2 = fpext float %conv đến gấp đôi ; phao -> gấp đôi
%div3 = fdiv double %conv2, 7.438300e+04 ; fdiv.d
%conv4 = fptrunc gấp đôi %div3 đến float ; double -> phaoVà LLVM IR ở cuối quy trình.
%conv2 = uitofp nneg i64 %0 đến gấp đôi
%div3 = fdiv double %conv2, 7.438300e+04
%conv4 = fptrunc double %div3 đến phao đồ thị TD
Định nghĩa nút %%
N1["%conv = sitofp i64 %5 tới float<br/><i>int -> float</i>"]
N2["%conv2 = fpext float %conv thành double<br/><i>float -> double</i>"]
N3["%div3 = fdiv double %conv2, 7.438300e+04<br/><i>fdiv.d</i>"]
N4["%conv4 = fptrunc double %div3 to float<br/><i>double -> float</i>"]
Luồng dữ liệu %%
N1 --> N2
N2 --> N3
N3 --> N4
Biểu đồ con %%
sơ đồ con CastGroup ["Diễn viên ban đầu"]
phương hướng TB
N1
N2
kết thúc
Nút chú thích %% AnnotateNode["<b>InstCombinerCast</b><br/>Giảm mẫu này thành %conv2 = uitofp nneg i64 %0 thành gấp đôi"]
%% Tạo kiểu - Sử dụng các nét trung tính hoạt động ở cả hai chế độ
kiểu AnnotateNode fill:#0288d122,đột quỵ:#0288d1,độ rộng nét:2px,rx:10,ry:10
kiểu CastGroup điền:none,đột quỵ:#888,đột quỵ-dasharray: 5 5
%% Các cạnh vô hình cho bố cục
AnnotateNode -.-> N1
AnnotateNode -.-> N2
Vé InstCombiner sau bản vá đó có thể tối ưu hóa sitofp i64 thành float, tiếp theo là fpext float thành double bằng cách giảm nó thành một uitofp nneg i64 %0 tăng gấp đôi.
Tuy nhiên, visitFPTrunc đã tối ưu hóa mẫu sau như đã nhận xét trong phần mã:
// Nếu chúng tôi có fptrunc(OpI (fpextend x), (fpextend y)), chúng tôi muốn
// đơn giản hóa biểu thức này để tránh một hoặc nhiều phần cắt bớt/mở rộng
// các phép tính nếu chúng ta có thể thực hiện mà không làm thay đổi kết quả bằng số.
//
// Cách chính xác mà độ rộng của toán hạng tương tác với giới hạn
// những việc chúng tôi có thể và không thể thực hiện một cách an toàn sẽ khác nhau tùy theo từng hoạt động và
// được giải thích bên dưới trong các tuyên bố tình huống khác nhau.
LLVM IR được tối ưu hóa hiện không còn có hướng dẫn fpext đó nữa, do đó, InstCombiner pass và cụ thể là visitFPTrunc không còn có thể thu hẹp gấp đôi thành một nổi.
Đưa ra giải pháp
Vì vậy, chúng tôi đã chẩn đoán sự cố - bản vá gần đây cho InstCombine đã cải thiện logic, khiến thẻ không thể thực hiện tối ưu hóa khác nữa. Chúng ta cần hướng dẫn InstCombiner thu hẹp thao tác trước đó bằng uitofp/sitofp nếu có fptrunc sau này.
biểu đồ TD
%% Xác định cột sống lệnh
Luồng sơ đồ con ["Luồng dữ liệu LLVM"]
phương hướng TB
N0["%0 = urem i64 %mul, 74383<br/><i>Nhà cung cấp phạm vi: 0 to 74,382</i>"]
N1["%conv2 = uitofp nneg i64 %0 to double<br/><i>Integer to Double</i>"]
N2["%div3 = fdiv double %conv2, 7.438300e+04<br/><i>Double Division</i>"]
N3["%conv4 = fptrunc double %div3 to float<br/><i>Double to Nổi</i>"]
%% Xác định chuỗi dọc
N0 --> N1
N1 --> N2
N2 --> N3
kết thúc
%% Xác định Nút tối ưu hóa ở bên cạnh
IC["<b>InstCombiner</b><br/>Giảm xuống độ chính xác float<br/>toán nếu đầu vào phù hợp."]
Tạo kiểu %%: Sử dụng logic #AARRGGBB hoặc #RRGGBBAA cho Hextra %% fill:#1b5e2022 có màu xanh lục rất trong suốt (độ mờ khoảng 13%)
kiểu điền IC:#1b5e2022,đột quỵ:#4caf50,độ rộng nét:2px,rx:10,ry:10
%% Kiểu dáng đồ thị con để giữ nó ở trạng thái trung tính
kiểu Dòng chảy: không có, nét vẽ: #888888, nét gạch ngang: 5 5
%% Kết nối mũi tên chấm
IC -.-> N0
IC -.-> N1
IC -.-> N3
Lần đầu tiên tôi nêu vấn đề trên Github. Mặc dù tôi tự tin rằng đây là sự cố hợp lệ nhưng tôi muốn kiểm tra kỹ xem vấn đề này có đảm bảo được khắc phục hay không. Tôi muốn cảm ơn tác giả của bản vá mà tôi đã đề cập trước đó, @SavchenkoValeriy, vì anh ấy đã có thể cung cấp cho tôi hướng dẫn về cách giải quyết vấn đề này cũng như đưa ra các đánh giá cho hoạt động PR của tôi. Giải pháp ban đầu của tôi có thể khá phức tạp nhưng anh ấy có thể đưa ra cho tôi một cách tiếp cận đơn giản hơn nhiều.
Vấn đề về GitHub: https://github.com/llvm/llvm-project/issues/190503
PR: https://github.com/llvm/llvm-project/pull/190550
Như người đánh giá của tôi đã chỉ ra, vấn đề hiện tại với isKnownExactCastIntToFP hiện chỉ kiểm tra xem hướng dẫn truyền CastInst[[TAG_1306] hay không.
%0 = urem i64 %mul, 74383
%conv2 = uitofp nneg i64 %0 đến gấp đôi
%div3 = fdiv double %conv2, 7.438300e+04
%conv4 = fptrunc double %div3 to phaoMã bên dưới là hàm visitFPTrunc Bạn có thể xem câu lệnh chuyển đổi với các thao tác khác nhau, bao gồm cả Fdiv và điều này tương ứng với fdiv.d/fdiv.s. FPT tương ứng với lệnh fptrunc, BO tương ứng với FPT.getOperand(0), vì vậy nó sẽ đề cập đến %conv2. Thay vào đó, chúng tôi cần xem liệu chúng tôi có thể chuyển đổi uitofp này thành float hay không, vì vậy, chúng tôi cần sửa đổi getMinimumFPType để kiểm tra các thao tác truyền cũng như hướng dẫn fpext.
Hướng dẫn *InstCombinerImpl::visitFPTrunc(FPTruncInst &FPT) {
if (Hướng dẫn *I = commonCastTransforms(FPT))
trở lại I;
...
Loại *Ty = FPT.getType();
tự động *BO = dyn_cast<BinaryOperator>(FPT.getOperand(0));
if (BO && BO->hasOneUse()) {
Loại *LHSMinType = getMinimumFPType(BO->getOperand(0), PreferBFloat);
Loại *RHSMinType = getMinimumFPType(BO->getOperand(1), PreferBFloat);
switch (BO->getOpcode()) {
default: break;
trường hợp Hướng dẫn::FThêm:
trường hợp Hướng dẫn::FSub:
...
...
Một trong những ý tưởng ban đầu của tôi là sửa đổi isKnownExactCastIntToFP để chấp nhận một tham số có giá trị khác Nhập (f32 trong trường hợp của tôi) nhưng có giá trị nullptr mặc định. Điều này sẽ cho phép chúng tôi chỉ sửa đổi định nghĩa tiêu đề và cách triển khai nó. Thay vào đó, người đánh giá của tôi đã đề xuất ý tưởng tạo một biến thể của isKnownExactCastIntToFP, canBeCastedExactlyIntToFP. Điều này sẽ thực hiện phân tích thực tế với loại được cung cấp cho nó và isKnownExactCastIntToFP có thể gọi nó. Tôi đề cập đến điều này để chứng tỏ rằng việc tương tác với cộng đồng và tìm kiếm ý tưởng từ người khác là điều tốt. Họ có thể đưa ra những ý tưởng có thể tốt hơn vì nhiều lý do.
Dưới đây là git diff cuối cùng. Chúng tôi tách isKnownExactCastIntToFP và tạo canBeCastedExactlyIntToFP để thực hiện phân tích thực tế và isKnownExactCastIntToFP gọi nó. Sau đó, chúng ta có getMinimumFPType gọi canBeCastedExactlyIntToFP.
chỉ mục bc52bf1168d4..2688891c1509 100644
--- a/llvm/include/llvm/Transforms/InstCombine/InstCombiner.h
+++ b/llvm/include/llvm/Transforms/InstCombine/InstCombiner.h
@@ -481,6 +481,8 @@ công khai:
/// Trả về true nếu việc truyền từ số nguyên sang FP có thể được chứng minh là chính xác
/// cho tất cả các đầu vào có thể (chuyển đổi không bị mất bất kỳ độ chính xác nào).
bool isKnownExactCastIntToFP(CastInst &I) const;
+ bool canBeCastedExactlyIntToFP(Value *V, Type *FPTy, bool IsSigned,
+ Hướng dẫn const *CxtI = nullptr) hằng số;
OverflowResult tínhOverflowForUnsignedMul(const Value *LHS,
Giá trị hằng số *RHS,
diff --git a/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp b/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
[[TAG_1637] thích
--- a/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
@@ -2039,10 +2039,17 @@ Loại tĩnh *shrinkFPConstantVector(Value *V, bool PreferBFloat) {
}
/// Tìm FP tối thiểu loại chúng ta có thể cắt ngắn một cách an toàn.
-loại tĩnh *getMinimumFPType(Giá trị *V, bool PreferBFloat) {
+loại tĩnh *getMinimumFPType(Giá trị *V, Loại *PreferredTy, InstCombiner &IC) {
if (tự động *FPExt = dyn_cast<FPExtInst>(V))
trả về FPSExt->getOperand(0)->getType();
+ Giá trị *Src;
+ if (match(V, m_IToFP(m_Value(Src))) &&
+ IC.canBeCastedExactlyIntToFP(Src, PreferredTy, isa<SIToFPInst>(V),
+ diễn viên<Hướng dẫn>(V)))
+ trở lại Ưu tiênTy;
+
+ bool PreferBFloat = PreferredTy->getScalarType()->isBFloatTy();
Hãy chú ý cách hiện tại chúng tôi gọi canBeCastedExactlyIntToFP với Loại của Lệnh fptrunc đang được truyền vào. Src trong trường hợp này là đầu vào cho BO->getOperand(0) và BO là toán hạng 0th của lệnh fptrunc. Hãy nhớ llvm ir trước đó:
%0 = urem i64 %mul, 74383
%conv2 = uitofp nneg i64 %0 tới gấp đôi
%div3 = fdiv double %conv2, 7.438300e+04
%conv4 = fptrunc double %div3 to phaoCái fptrunc đang chuyển đổi đầu vào thành float, vì vậy loại Ty là f32. BO là %div3, BO->getOperand(0) là %conv2 và m_IToFP(m_Value(Src)) đặt đầu vào của BO->getOperand(0) vào Src. Bảng bên dưới hiển thị ánh xạ của mã visitFPTrunc tới các biến LLVM IR.
truy cậpFPTrunc Biến |
Biến IR LLVM | Giá trị |
|---|---|---|
BO |
%div3 |
fdiv gấp đôi %conv2, 7.438300e+04 |
BO->getOperand(0) |
%conv2 |
uitofp nneg i64 %0 tăng gấp đôi |
BO->getOperand(1) |
7.438300e+04 |
hằng số kép |
Src |
%0 | urem i64 %mul, 74383 |
Kết quả
Cách này có hiệu quả không? Bằng cách chạy lệnh bên dưới, chúng ta có thể thấy LLVM IR sau bản vá của tôi.
$LLVM_BUILD_DIR/bin/clang -O3 \
--target=riscv64-unknown-linux-gnu \
-march=rv64gc_zba_zbb \
--sysroot=/usr/riscv64-linux-gnu \
-S -emit-llvm pi.c -o pi_fixed.llVà đây là LLVM IR có liên quan.
%mul = mul nuw nsw i64 %ixran.053, 27611
%0 = urem i64 %mul, 74383
%1 = uitofp nneg i64 %0 đến thả nổi
%conv4 = fdiv float %1, 7.438300e+04Đã thành công! 🥹
fptrunc không còn nữa và thao tác truyền uitofp giờ đây chuyển nó thành một nổi.
Hiển thị bên dưới là kết quả sau khi bản vá của tôi được hợp nhất. Chúng ta có thể thấy rằng mục tiêu có thể thực hiện điểm chuẩn trong chu kỳ 1,67 Bn, cải thiện khoảng 25%.
https://cc-perf.igalia.com/db_default/v4/nts/profile/260/426/422
![]()
Tác giả: luu