Tại sao một vòng lặp đơn giản được tối ưu hóa khi giới hạn là 959 mà không phải là 960?


131

Hãy xem xét vòng lặp đơn giản này:

float f(float x[]) {
  float p = 1.0;
  for (int i = 0; i < 959; i++)
    p += 1;
  return p;
}

Nếu bạn biên dịch với gcc 7 (ảnh chụp nhanh) hoặc clang (thân cây) với -march=core-avx2 -Ofastbạn sẽ nhận được một cái gì đó rất giống với.

.LCPI0_0:
        .long   1148190720              # float 960
f:                                      # @f
        vmovss  xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
        ret

Nói cách khác, nó chỉ đặt câu trả lời là 960 mà không lặp.

Tuy nhiên nếu bạn thay đổi mã thành:

float f(float x[]) {
  float p = 1.0;
  for (int i = 0; i < 960; i++)
    p += 1;
  return p;
}

Việc lắp ráp sản xuất thực sự thực hiện tổng vòng lặp? Ví dụ clang cho:

.LCPI0_0:
        .long   1065353216              # float 1
.LCPI0_1:
        .long   1086324736              # float 6
f:                                      # @f
        vmovss  xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
        vxorps  ymm1, ymm1, ymm1
        mov     eax, 960
        vbroadcastss    ymm2, dword ptr [rip + .LCPI0_1]
        vxorps  ymm3, ymm3, ymm3
        vxorps  ymm4, ymm4, ymm4
.LBB0_1:                                # =>This Inner Loop Header: Depth=1
        vaddps  ymm0, ymm0, ymm2
        vaddps  ymm1, ymm1, ymm2
        vaddps  ymm3, ymm3, ymm2
        vaddps  ymm4, ymm4, ymm2
        add     eax, -192
        jne     .LBB0_1
        vaddps  ymm0, ymm1, ymm0
        vaddps  ymm0, ymm3, ymm0
        vaddps  ymm0, ymm4, ymm0
        vextractf128    xmm1, ymm0, 1
        vaddps  ymm0, ymm0, ymm1
        vpermilpd       xmm1, xmm0, 1   # xmm1 = xmm0[1,0]
        vaddps  ymm0, ymm0, ymm1
        vhaddps ymm0, ymm0, ymm0
        vzeroupper
        ret

Tại sao điều này và tại sao nó giống hệt nhau cho clang và gcc?


Giới hạn cho cùng một vòng lặp nếu bạn thay thế floatbằng double479. Điều này giống với gcc và tiếng kêu một lần nữa.

Cập nhật 1

Hóa ra gcc 7 (ảnh chụp nhanh) và tiếng kêu (thân cây) hành xử rất khác nhau. clang tối ưu hóa các vòng lặp cho tất cả các giới hạn nhỏ hơn 960 theo như tôi có thể nói. gcc mặt khác nhạy cảm với giá trị chính xác và không có giới hạn trên. Ví dụ, nó không tối ưu hóa vòng lặp khi giới hạn là 200 (cũng như nhiều giá trị khác) nhưng nó thực hiện khi giới hạn là 202 và 20002 (cũng như nhiều giá trị khác).


3
Điều mà Netherhan có thể có nghĩa là 1) trình biên dịch hủy bỏ vòng lặp và 2) một khi nó không được kiểm soát thấy rằng các phép toán tổng có thể được nhóm thành một. Nếu vòng lặp không được kiểm soát, các hoạt động không thể được nhóm lại.
Jean-François Fabre

3
Có một số vòng lặp lẻ làm cho việc kiểm soát không phức tạp hơn, vài lần lặp cuối cùng phải được thực hiện đặc biệt. Điều đó cũng có thể đủ để đưa trình tối ưu hóa vào chế độ mà nó không còn có thể nhận ra phím tắt. Rất có khả năng, trước tiên nó phải thêm mã cho trường hợp đặc biệt và sau đó sẽ phải xóa nó một lần nữa. Sử dụng trình tối ưu hóa giữa hai tai luôn là tốt nhất :)
Hans Passant

3
@HansPassant Nó cũng được tối ưu hóa cho bất kỳ số nào nhỏ hơn 959.
eleanora

6
Điều này thường không được thực hiện với việc loại bỏ biến cảm ứng, thay vì bỏ ra một số tiền điên rồ? Không kiểm soát bởi một yếu tố 959 là điên rồ.
harold

4
@eleanora Tôi đã chơi với nhà thám hiểm compilre đó và dường như sau đây (chỉ nói về ảnh chụp nhanh gcc): Nếu số vòng lặp là bội số của 4 và ít nhất là 72, thì vòng lặp không được kiểm soát (hay đúng hơn là không được kiểm soát bởi hệ số 4); mặt khác, toàn bộ vòng lặp được thay thế bằng một hằng số - ngay cả khi số vòng lặp là 2000000001. Sự nghi ngờ của tôi: tối ưu hóa sớm (như trong một "sớm, bội số của 4, điều đó tốt cho việc hủy đăng ký" ngăn chặn tối ưu hóa hơn nữa so với kỹ lưỡng hơn "Dù sao thì thỏa thuận với vòng lặp này là gì?")
Hagen von Eitzen

Câu trả lời:


88

TL; DR

Theo mặc định, ảnh chụp nhanh GCC 7 hiện tại hoạt động không nhất quán, trong khi các phiên bản trước có giới hạn mặc định do PARAM_MAX_COMPLETELY_PEEL_TIMES, đó là 16. Nó có thể bị ghi đè từ dòng lệnh.

Lý do của giới hạn là ngăn chặn việc không kiểm soát vòng lặp quá tích cực, đó có thể là con dao hai lưỡi .

Phiên bản GCC <= 6.3.0

Tùy chọn tối ưu hóa có liên quan cho GCC là -fpeel-loops, được bật gián tiếp cùng với cờ -Ofast(nhấn mạnh là của tôi):

Lột các vòng mà có đủ thông tin mà chúng không cuộn nhiều (từ phản hồi hồ sơ hoặc phân tích tĩnh ). Nó cũng bật bóc vòng lặp hoàn chỉnh (nghĩa là loại bỏ hoàn toàn các vòng lặp với số lần lặp không đổi nhỏ ).

Kích hoạt với -O3và / hoặc -fprofile-use.

Thêm chi tiết có thể thu được bằng cách thêm -fdump-tree-cunroll:

$ head test.c.151t.cunroll 

;; Function f (f, funcdef_no=0, decl_uid=1919, cgraph_uid=0, symbol_order=0)

Not peeling: upper bound is known so can unroll completely

Tin nhắn là từ /gcc/tree-ssa-loop-ivcanon.c:

if (maxiter >= 0 && maxiter <= npeel)
    {
      if (dump_file)
        fprintf (dump_file, "Not peeling: upper bound is known so can "
         "unroll completely\n");
      return false;
    }

do đó try_peel_loophàm trả về false.

Có thể đạt được nhiều đầu ra dài hơn với -fdump-tree-cunroll-details:

Loop 1 iterates 959 times.
Loop 1 iterates at most 959 times.
Not unrolling loop 1 (--param max-completely-peeled-times limit reached).
Not peeling: upper bound is known so can unroll completely

Có thể điều chỉnh các giới hạn bằng cách pla với max-completely-peeled-insns=nmax-completely-peel-times=nparams:

max-completely-peeled-insns

Số lượng tối đa của một vòng lặp bóc hoàn toàn.

max-completely-peel-times

Số lần lặp tối đa của một vòng lặp là phù hợp để lột hoàn toàn.

Để tìm hiểu thêm về insns, bạn có thể tham khảo Hướng dẫn sử dụng GCC Internals .

Ví dụ: nếu bạn biên dịch với các tùy chọn sau:

-march=core-avx2 -Ofast --param max-completely-peeled-insns=1000 --param max-completely-peel-times=1000

sau đó mã biến thành:

f:
        vmovss  xmm0, DWORD PTR .LC0[rip]
        ret
.LC0:
        .long   1148207104

Kêu vang

Tôi không chắc Clang thực sự làm gì và làm thế nào để điều chỉnh giới hạn của nó, nhưng như tôi đã quan sát, bạn có thể buộc nó đánh giá giá trị cuối cùng bằng cách đánh dấu vòng lặp bằng pragma không kiểm soát và nó sẽ loại bỏ hoàn toàn:

#pragma unroll
for (int i = 0; i < 960; i++)
    p++;

kết quả thành:

.LCPI0_0:
        .long   1148207104              # float 961
f:                                      # @f
        vmovss  xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
        ret

Cảm ơn bạn cho câu trả lời rất tốt đẹp này. Như những người khác đã chỉ ra, gcc dường như nhạy cảm với kích thước giới hạn chính xác. Ví dụ, nó không thể loại bỏ vòng lặp cho 912 godbolt.org/g/EQJHvT . Fdump-tree-cunroll-chi tiết nói gì trong trường hợp đó?
eleanora

Trong thực tế, thậm chí 200 có vấn đề này. Đây là tất cả trong một ảnh chụp nhanh của gcc 7 mà godbolt cung cấp. godbolt.org/g/Vg3SVs Điều này hoàn toàn không áp dụng cho tiếng kêu.
eleanora

13
Bạn giải thích các cơ chế của việc bóc vỏ, nhưng không phải sự liên quan của 960 là gì hoặc tại sao thậm chí còn có một giới hạn
MM

1
@MM: Hành vi lột hoàn toàn khác nhau giữa GCC 6.3.0 và snaphost mới nhất. Trong trường hợp trước đây, tôi cực kỳ nghi ngờ rằng giới hạn mã hóa cứng được thi hành bởi PARAM_MAX_COMPLETELY_PEEL_TIMESparam, được xác định /gcc/params.def:321bằng giá trị 16.
Grzegorz Szpetkowski

14
Bạn có thể muốn đề cập tại sao GCC cố tình giới hạn bản thân theo cách này. Cụ thể, nếu bạn hủy kiểm soát các vòng lặp của mình quá mạnh, nhị phân sẽ lớn hơn và bạn ít có khả năng phù hợp với bộ đệm L1. Lỗi bộ nhớ cache có khả năng khá tốn kém liên quan đến việc lưu một vài bước nhảy có điều kiện, giả sử dự đoán nhánh tốt (mà bạn sẽ có, cho một vòng lặp điển hình).
Kevin

19

Sau khi đọc bình luận của Sulthan, tôi đoán rằng:

  1. Trình biên dịch sẽ hủy hoàn toàn vòng lặp nếu bộ đếm vòng lặp không đổi (và không quá cao)

  2. Khi nó không được kiểm soát, trình biên dịch sẽ thấy rằng các phép toán tổng có thể được nhóm thành một.

Nếu một số lý do vòng lặp không được kiểm soát (ở đây: nó sẽ tạo ra quá nhiều câu lệnh với 1000), các hoạt động không thể được nhóm lại.

Trình biên dịch có thể thấy rằng việc hủy đăng ký 1000 câu lệnh cho một bổ sung duy nhất, nhưng bước 1 & 2 được mô tả ở trên là hai tối ưu hóa riêng biệt, do đó, nó không thể có "rủi ro" của việc hủy đăng ký, không biết liệu các hoạt động có thể được nhóm lại không (ví dụ: một cuộc gọi chức năng không thể được nhóm lại).

Lưu ý: Đây là trường hợp góc: Ai sử dụng vòng lặp để thêm điều tương tự lại? Trong trường hợp đó, đừng dựa vào trình biên dịch có thể hủy đăng ký / tối ưu hóa; trực tiếp viết các hoạt động thích hợp trong một hướng dẫn.


1
sau đó bạn có thể tập trung vào not too highphần đó ? Tôi có nghĩa là tại sao rủi ro không có trong trường hợp 100? Tôi đã đoán được điều gì đó ... trong nhận xét của tôi ở trên..tôi có thể là lý do cho điều đó?
dùng2736738

Tôi nghĩ rằng trình biên dịch không nhận thức được sự thiếu chính xác của dấu phẩy động mà nó có thể kích hoạt. Tôi đoán nó chỉ là một giới hạn kích thước hướng dẫn. Bạn có max-unrolled-insnsbên cạnhmax-unrolled-times
Jean-François Fabre

Ah đó là loại suy nghĩ hoặc phỏng đoán của tôi ... muốn có được một lý do rõ ràng hơn.
dùng2736738

5
Thật thú vị nếu bạn thay đổi floatthành một int, trình biên dịch gcc có thể tăng cường giảm vòng lặp bất kể số lần lặp, do tối ưu hóa biến cảm ứng của nó ( -fivopts). Nhưng những người dường như không làm việc cho floats.
Tavian Barnes

1
@CortAmmon Phải, và tôi nhớ lại việc đọc một số người ngạc nhiên và buồn bã rằng GCC sử dụng MPFR để tính toán chính xác các số rất lớn, cho kết quả khá khác so với các phép toán dấu phẩy động tương đương có lỗi tích lũy và mất độ chính xác. Cho thấy rằng nhiều người tính điểm nổi sai cách.
Zan Lynx

12

Câu hỏi rất hay!

Bạn dường như đã đạt đến một giới hạn về số lần lặp hoặc hoạt động mà trình biên dịch cố gắng nội tuyến khi đơn giản hóa mã. Theo tài liệu của Grzegorz Szpetkowski, có những cách cụ thể của trình biên dịch để điều chỉnh các giới hạn này bằng các tùy chọn pragmas hoặc dòng lệnh.

Bạn cũng có thể chơi với Trình khám phá trình biên dịch của Godbolt để so sánh mức độ khác nhau của trình biên dịch và các tùy chọn ảnh hưởng đến mã được tạo: gcc 6.2icc 17vẫn chạy mã cho 960, trong khi clang 3.9không (với cấu hình Godbolt mặc định, nó thực sự dừng ở mức 73).


Tôi đã chỉnh sửa câu hỏi để làm rõ các phiên bản gcc và clang tôi đang sử dụng. Xem godbolt.org/g/FfwWjL . Tôi đang sử dụng -Ofast chẳng hạn.
eleanora
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.