Tại sao GCC tạo mã nhanh hơn 15-20% nếu tôi tối ưu hóa kích thước thay vì tốc độ?


445

Lần đầu tiên tôi nhận thấy vào năm 2009 rằng GCC (ít nhất là trong các dự án và trên máy của tôi) có xu hướng tạo mã nhanh hơn đáng kể nếu tôi tối ưu hóa kích thước ( -Os) thay vì tốc độ ( -O2hoặc -O3), và tôi đã tự hỏi tại sao.

Tôi đã quản lý để tạo mã (khá ngớ ngẩn) cho thấy hành vi đáng ngạc nhiên này và đủ nhỏ để được đăng ở đây.

const int LOOP_BOUND = 200000000;

__attribute__((noinline))
static int add(const int& x, const int& y) {
    return x + y;
}

__attribute__((noinline))
static int work(int xval, int yval) {
    int sum(0);
    for (int i=0; i<LOOP_BOUND; ++i) {
        int x(xval+sum);
        int y(yval+sum);
        int z = add(x, y);
        sum += z;
    }
    return sum;
}

int main(int , char* argv[]) {
    int result = work(*argv[1], *argv[2]);
    return result;
}

Nếu tôi biên dịch nó với -Os, phải mất 0,38 giây để thực hiện chương trình này và 0,44 giây nếu nó được biên dịch bằng -O2hoặc -O3. Những thời gian này thu được một cách nhất quán và thực tế không có tiếng ồn (gcc 4.7.2, x86_64 GNU / Linux, Intel Core i5-3320M).

(Cập nhật: Tôi đã chuyển tất cả mã lắp ráp sang GitHub : Họ đã làm cho bài đăng trở nên cồng kềnh và rõ ràng thêm rất ít giá trị cho các câu hỏi vì các fno-align-*cờ có cùng tác dụng.)

Đây là lắp ráp được tạo ra với -Os-O2.

Thật không may, sự hiểu biết của tôi về lắp ráp rất hạn chế, vì vậy tôi không biết liệu những gì tôi làm tiếp theo có đúng hay không: Tôi đã nắm lấy lắp ráp -O2và hợp nhất tất cả sự khác biệt của nó vào lắp ráp -Os ngoại trừ các .p2aligndòng, kết quả ở đây . Mã này vẫn chạy trong 0,38 giây và sự khác biệt duy nhất là .p2align công cụ.

Nếu tôi đoán chính xác, đây là những miếng đệm để căn chỉnh ngăn xếp. Theo lý do tại sao GCC pad hoạt động với NOP? nó được thực hiện với hy vọng rằng mã sẽ chạy nhanh hơn, nhưng rõ ràng việc tối ưu hóa này đã phản tác dụng trong trường hợp của tôi.

Có phải đó là phần đệm là thủ phạm trong trường hợp này? Lý do tại sao và làm thế nào?

Tiếng ồn mà nó tạo ra khá nhiều làm cho việc tối ưu hóa vi thời gian là không thể.

Làm cách nào tôi có thể chắc chắn rằng sự sắp xếp may mắn / không may mắn như vậy không can thiệp khi tôi thực hiện tối ưu hóa vi mô (không liên quan đến căn chỉnh ngăn xếp) trên mã nguồn C hoặc C ++?


CẬP NHẬT:

Theo câu trả lời của Pascal Cuoq, tôi đã sửa lại một chút với sự sắp xếp. Bằng cách chuyển -O2 -fno-align-functions -fno-align-loopsđến gcc, tất cả .p2alignđã biến mất khỏi hội đồng và thực thi được tạo trong 0,38 giây. Theo tài liệu gcc :

-Os cho phép tối ưu hóa tất cả -O2 [nhưng] -Os vô hiệu hóa các cờ tối ưu hóa sau:

  -falign-functions  -falign-jumps  -falign-loops
  -falign-labels  -freorder-blocks  -freorder-blocks-and-partition
  -fprefetch-loop-arrays

Vì vậy, nó có vẻ như là một vấn đề liên kết (mis).

Tôi vẫn còn hoài nghi về -march=nativeđề xuất trong câu trả lời của Marat Dukhan . Tôi không tin rằng nó không chỉ can thiệp vào vấn đề căn chỉnh (mis) này; Nó hoàn toàn không có tác dụng với máy của tôi. (Tuy nhiên, tôi đã nêu lên câu trả lời của anh ấy.)


CẬP NHẬT 2:

Chúng ta có thể lấy -Osra khỏi hình ảnh. Các lần sau có được bằng cách biên dịch với

  • -O2 -fno-omit-frame-pointer 0,37s

  • -O2 -fno-align-functions -fno-align-loops 0,37s

  • -S -O2sau đó tự di chuyển lắp ráp add()sau work()0,37s

  • -O2 0,44s

Dường như với tôi khoảng cách add()từ trang web cuộc gọi rất nhiều vấn đề. Tôi đã thử perf, nhưng đầu ra perf statperf reportrất ít ý nghĩa đối với tôi. Tuy nhiên, tôi chỉ có thể nhận được một kết quả nhất quán từ nó:

-O2:

 602,312,864 stalled-cycles-frontend   #    0.00% frontend cycles idle
       3,318 cache-misses
 0.432703993 seconds time elapsed
 [...]
 81.23%  a.out  a.out              [.] work(int, int)
 18.50%  a.out  a.out              [.] add(int const&, int const&) [clone .isra.0]
 [...]
       ¦   __attribute__((noinline))
       ¦   static int add(const int& x, const int& y) {
       ¦       return x + y;
100.00 ¦     lea    (%rdi,%rsi,1),%eax
       ¦   }
       ¦   ? retq
[...]
       ¦            int z = add(x, y);
  1.93 ¦    ? callq  add(int const&, int const&) [clone .isra.0]
       ¦            sum += z;
 79.79 ¦      add    %eax,%ebx

Dành cho fno-align-*:

 604,072,552 stalled-cycles-frontend   #    0.00% frontend cycles idle
       9,508 cache-misses
 0.375681928 seconds time elapsed
 [...]
 82.58%  a.out  a.out              [.] work(int, int)
 16.83%  a.out  a.out              [.] add(int const&, int const&) [clone .isra.0]
 [...]
       ¦   __attribute__((noinline))
       ¦   static int add(const int& x, const int& y) {
       ¦       return x + y;
 51.59 ¦     lea    (%rdi,%rsi,1),%eax
       ¦   }
[...]
       ¦    __attribute__((noinline))
       ¦    static int work(int xval, int yval) {
       ¦        int sum(0);
       ¦        for (int i=0; i<LOOP_BOUND; ++i) {
       ¦            int x(xval+sum);
  8.20 ¦      lea    0x0(%r13,%rbx,1),%edi
       ¦            int y(yval+sum);
       ¦            int z = add(x, y);
 35.34 ¦    ? callq  add(int const&, int const&) [clone .isra.0]
       ¦            sum += z;
 39.48 ¦      add    %eax,%ebx
       ¦    }

Dành cho -fno-omit-frame-pointer:

 404,625,639 stalled-cycles-frontend   #    0.00% frontend cycles idle
      10,514 cache-misses
 0.375445137 seconds time elapsed
 [...]
 75.35%  a.out  a.out              [.] add(int const&, int const&) [clone .isra.0]                                                                                     ¦
 24.46%  a.out  a.out              [.] work(int, int)
 [...]
       ¦   __attribute__((noinline))
       ¦   static int add(const int& x, const int& y) {
 18.67 ¦     push   %rbp
       ¦       return x + y;
 18.49 ¦     lea    (%rdi,%rsi,1),%eax
       ¦   const int LOOP_BOUND = 200000000;
       ¦
       ¦   __attribute__((noinline))
       ¦   static int add(const int& x, const int& y) {
       ¦     mov    %rsp,%rbp
       ¦       return x + y;
       ¦   }
 12.71 ¦     pop    %rbp
       ¦   ? retq
 [...]
       ¦            int z = add(x, y);
       ¦    ? callq  add(int const&, int const&) [clone .isra.0]
       ¦            sum += z;
 29.83 ¦      add    %eax,%ebx

Có vẻ như chúng tôi đang trì hoãn cuộc gọi đến add()trong trường hợp chậm.

Tôi đã kiểm tra tất cả mọi thứperf -ecó thể nhổ ra trên máy tính của tôi; không chỉ các số liệu thống kê được đưa ra ở trên.

Đối với cùng thực thi, stalled-cycles-frontendhiển thị tương quan tuyến tính với thời gian thực hiện; Tôi đã không nhận thấy bất cứ điều gì khác sẽ tương quan rõ ràng như vậy. (So ​​sánh stalled-cycles-frontendvới các thực thi khác nhau không có ý nghĩa với tôi.)

Tôi đã bao gồm các lỗi nhớ cache khi nó xuất hiện như là bình luận đầu tiên. Tôi đã kiểm tra tất cả các lỗi bộ nhớ cache có thể đo được trên máy của mình perf, không chỉ các lỗi được nêu ở trên. Các lỗi bộ nhớ cache rất rất ồn ào và hiển thị rất ít hoặc không tương quan với thời gian thực hiện.


36
Đoán mù: đây có thể là một bộ nhớ cache bỏ lỡ?

@ H2CO3 Đó cũng là suy nghĩ đầu tiên của tôi, nhưng không được khuyến khích đủ để đăng bình luận mà không đọc và hiểu sâu câu hỏi của OP.
πάντα ῥεῖ

2
@ g-makulik Đó là lý do tại sao tôi cảnh báo rằng đó là "phỏng đoán mù" ;-) "TL; DR" được dành riêng cho các câu hỏi xấu. : P

3
Chỉ là một điểm dữ liệu thú vị: Tôi thấy rằng -O3 hoặc -Ofast nhanh gấp khoảng 1,5 lần so với -Os khi tôi biên dịch điều này với tiếng kêu trên OS X. (Tôi chưa thử sao chép bằng gcc.)
Rob Napier

2
Đây là cùng một mã. Hãy xem xét kỹ hơn địa chỉ của .L3, các mục tiêu chi nhánh bị sai lệch rất tốn kém.
Hans Passant

Câu trả lời:


503

Theo mặc định trình biên dịch tối ưu hóa cho bộ xử lý "trung bình". Do các bộ xử lý khác nhau ưu tiên các chuỗi lệnh khác nhau, tối ưu hóa trình biên dịch được kích hoạt -O2có thể có lợi cho bộ xử lý trung bình, nhưng làm giảm hiệu suất trên bộ xử lý cụ thể của bạn (và áp dụng tương tự cho -Os). Nếu bạn thử cùng một ví dụ trên các bộ xử lý khác nhau, bạn sẽ thấy rằng trên một số trong số chúng được hưởng lợi -O2trong khi các ứng dụng khác có lợi hơn cho việc -Ostối ưu hóa.

Dưới đây là kết quả cho time ./test 0 0một số bộ xử lý (thời gian người dùng báo cáo):

Processor (System-on-Chip)             Compiler   Time (-O2)  Time (-Os)  Fastest
AMD Opteron 8350                       gcc-4.8.1    0.704s      0.896s      -O2
AMD FX-6300                            gcc-4.8.1    0.392s      0.340s      -Os
AMD E2-1800                            gcc-4.7.2    0.740s      0.832s      -O2
Intel Xeon E5405                       gcc-4.8.1    0.603s      0.804s      -O2
Intel Xeon E5-2603                     gcc-4.4.7    1.121s      1.122s       -
Intel Core i3-3217U                    gcc-4.6.4    0.709s      0.709s       -
Intel Core i3-3217U                    gcc-4.7.3    0.708s      0.822s      -O2
Intel Core i3-3217U                    gcc-4.8.1    0.708s      0.944s      -O2
Intel Core i7-4770K                    gcc-4.8.1    0.296s      0.288s      -Os
Intel Atom 330                         gcc-4.8.1    2.003s      2.007s      -O2
ARM 1176JZF-S (Broadcom BCM2835)       gcc-4.6.3    3.470s      3.480s      -O2
ARM Cortex-A8 (TI OMAP DM3730)         gcc-4.6.3    2.727s      2.727s       -
ARM Cortex-A9 (TI OMAP 4460)           gcc-4.6.3    1.648s      1.648s       -
ARM Cortex-A9 (Samsung Exynos 4412)    gcc-4.6.3    1.250s      1.250s       -
ARM Cortex-A15 (Samsung Exynos 5250)   gcc-4.7.2    0.700s      0.700s       -
Qualcomm Snapdragon APQ8060A           gcc-4.8       1.53s       1.52s      -Os

Trong một số trường hợp, bạn có thể giảm bớt ảnh hưởng của tối ưu hóa bất lợi bằng cách yêu cầu gcctối ưu hóa cho bộ xử lý cụ thể của bạn (sử dụng tùy chọn -mtune=nativehoặc -march=native):

Processor            Compiler   Time (-O2 -mtune=native) Time (-Os -mtune=native)
AMD FX-6300          gcc-4.8.1         0.340s                   0.340s
AMD E2-1800          gcc-4.7.2         0.740s                   0.832s
Intel Xeon E5405     gcc-4.8.1         0.603s                   0.803s
Intel Core i7-4770K  gcc-4.8.1         0.296s                   0.288s

Cập nhật: trên Ivy Bridge dựa trên Core i3 ba phiên bản của gcc( 4.6.4, 4.7.34.8.1) binaries sản phẩm với hiệu suất khác nhau đáng kể, nhưng mã lắp ráp có biến thể chỉ tinh tế. Cho đến nay, tôi không có lời giải thích về thực tế này.

Hội từ gcc-4.6.4 -Os(thực hiện trong 0,709 giây):

00000000004004d2 <_ZL3addRKiS0_.isra.0>:
  4004d2:       8d 04 37                lea    eax,[rdi+rsi*1]
  4004d5:       c3                      ret

00000000004004d6 <_ZL4workii>:
  4004d6:       41 55                   push   r13
  4004d8:       41 89 fd                mov    r13d,edi
  4004db:       41 54                   push   r12
  4004dd:       41 89 f4                mov    r12d,esi
  4004e0:       55                      push   rbp
  4004e1:       bd 00 c2 eb 0b          mov    ebp,0xbebc200
  4004e6:       53                      push   rbx
  4004e7:       31 db                   xor    ebx,ebx
  4004e9:       41 8d 34 1c             lea    esi,[r12+rbx*1]
  4004ed:       41 8d 7c 1d 00          lea    edi,[r13+rbx*1+0x0]
  4004f2:       e8 db ff ff ff          call   4004d2 <_ZL3addRKiS0_.isra.0>
  4004f7:       01 c3                   add    ebx,eax
  4004f9:       ff cd                   dec    ebp
  4004fb:       75 ec                   jne    4004e9 <_ZL4workii+0x13>
  4004fd:       89 d8                   mov    eax,ebx
  4004ff:       5b                      pop    rbx
  400500:       5d                      pop    rbp
  400501:       41 5c                   pop    r12
  400503:       41 5d                   pop    r13
  400505:       c3                      ret

Hội từ gcc-4.7.3 -Os(thực hiện trong 0.822 giây):

00000000004004fa <_ZL3addRKiS0_.isra.0>:
  4004fa:       8d 04 37                lea    eax,[rdi+rsi*1]
  4004fd:       c3                      ret

00000000004004fe <_ZL4workii>:
  4004fe:       41 55                   push   r13
  400500:       41 89 f5                mov    r13d,esi
  400503:       41 54                   push   r12
  400505:       41 89 fc                mov    r12d,edi
  400508:       55                      push   rbp
  400509:       bd 00 c2 eb 0b          mov    ebp,0xbebc200
  40050e:       53                      push   rbx
  40050f:       31 db                   xor    ebx,ebx
  400511:       41 8d 74 1d 00          lea    esi,[r13+rbx*1+0x0]
  400516:       41 8d 3c 1c             lea    edi,[r12+rbx*1]
  40051a:       e8 db ff ff ff          call   4004fa <_ZL3addRKiS0_.isra.0>
  40051f:       01 c3                   add    ebx,eax
  400521:       ff cd                   dec    ebp
  400523:       75 ec                   jne    400511 <_ZL4workii+0x13>
  400525:       89 d8                   mov    eax,ebx
  400527:       5b                      pop    rbx
  400528:       5d                      pop    rbp
  400529:       41 5c                   pop    r12
  40052b:       41 5d                   pop    r13
  40052d:       c3                      ret

Hội từ gcc-4.8.1 -Os(thực hiện trong 0,994 giây):

00000000004004fd <_ZL3addRKiS0_.isra.0>:
  4004fd:       8d 04 37                lea    eax,[rdi+rsi*1]
  400500:       c3                      ret

0000000000400501 <_ZL4workii>:
  400501:       41 55                   push   r13
  400503:       41 89 f5                mov    r13d,esi
  400506:       41 54                   push   r12
  400508:       41 89 fc                mov    r12d,edi
  40050b:       55                      push   rbp
  40050c:       bd 00 c2 eb 0b          mov    ebp,0xbebc200
  400511:       53                      push   rbx
  400512:       31 db                   xor    ebx,ebx
  400514:       41 8d 74 1d 00          lea    esi,[r13+rbx*1+0x0]
  400519:       41 8d 3c 1c             lea    edi,[r12+rbx*1]
  40051d:       e8 db ff ff ff          call   4004fd <_ZL3addRKiS0_.isra.0>
  400522:       01 c3                   add    ebx,eax
  400524:       ff cd                   dec    ebp
  400526:       75 ec                   jne    400514 <_ZL4workii+0x13>
  400528:       89 d8                   mov    eax,ebx
  40052a:       5b                      pop    rbx
  40052b:       5d                      pop    rbp
  40052c:       41 5c                   pop    r12
  40052e:       41 5d                   pop    r13
  400530:       c3                      ret

186
Chỉ cần làm rõ: bạn đã thực sự đi và đo hiệu suất của mã OP trên 12 nền tảng khác nhau? (+1 cho ý nghĩ đơn thuần rằng bạn sẽ làm điều đó)
anatolyg

194
@anatolyg Vâng, tôi đã làm! (và sẽ bổ sung thêm vài lần nữa)
Marat Dukhan

43
Thật. Một +1 khác không chỉ lý thuyết hóa về các CPU khác nhau mà còn thực sự chứng minh điều đó. Không phải cái gì đó (than ôi) bạn thấy trong mọi câu trả lời liên quan đến tốc độ. Các thử nghiệm này có chạy cùng một hệ điều hành không? (Vì có thể điều này sẽ làm lệch kết quả ...)
usr2564301

7
@Ali Trên AMD-FX 6300 -O2 -fno-align-functions -fno-align-loopsgiảm thời gian 0.340s, vì vậy nó có thể được giải thích bằng căn chỉnh. Tuy nhiên, căn chỉnh tối ưu phụ thuộc vào bộ xử lý: một số bộ xử lý thích các vòng lặp và chức năng được căn chỉnh.
Marat Dukhan

13
@Jongware Tôi không thấy hệ điều hành sẽ ảnh hưởng đáng kể đến kết quả như thế nào; vòng lặp không bao giờ thực hiện cuộc gọi hệ thống.
Ali

186

Đồng nghiệp của tôi đã giúp tôi tìm thấy một câu trả lời hợp lý cho câu hỏi của tôi. Ông nhận thấy tầm quan trọng của ranh giới 256 byte. Anh ấy không đăng ký ở đây và khuyến khích tôi tự đăng câu trả lời (và lấy tất cả danh tiếng).


Câu trả lời ngắn:

Có phải đó là phần đệm là thủ phạm trong trường hợp này? Lý do tại sao và làm thế nào?

Tất cả sôi sục để liên kết. Sự sắp xếp có thể có tác động đáng kể đến hiệu suất, đó là lý do tại sao chúng ta có -falign-*cờ ở vị trí đầu tiên.

Tôi đã gửi báo cáo lỗi (không có thật?) Cho các nhà phát triển gcc . Hóa ra hành vi mặc định là "chúng tôi sắp xếp các vòng lặp thành 8 byte theo mặc định nhưng cố gắng căn chỉnh nó thành 16 byte nếu chúng tôi không cần điền vào hơn 10 byte." Rõ ràng, mặc định này không phải là lựa chọn tốt nhất trong trường hợp cụ thể này và trên máy của tôi. Clang 3.4 (thân cây) với -O3việc căn chỉnh phù hợp và mã được tạo không hiển thị hành vi kỳ lạ này.

Tất nhiên, nếu một sự liên kết không phù hợp được thực hiện, nó làm cho mọi thứ tồi tệ hơn. Một liên kết không cần thiết / xấu chỉ ăn hết byte mà không có lý do và có khả năng làm tăng các lỗi bộ nhớ cache, v.v.

Tiếng ồn mà nó tạo ra khá nhiều làm cho việc tối ưu hóa vi thời gian là không thể.

Làm cách nào tôi có thể chắc chắn rằng sự sắp xếp may mắn / không may mắn như vậy không can thiệp khi tôi thực hiện tối ưu hóa vi mô (không liên quan đến căn chỉnh ngăn xếp) trên mã nguồn C hoặc C ++?

Đơn giản bằng cách nói với gcc để thực hiện căn chỉnh đúng:

g++ -O2 -falign-functions=16 -falign-loops=16


Câu trả lời dài:

Mã sẽ chạy chậm hơn nếu:

  • một XXranh giới byte cắt add()ở giữa ( XXphụ thuộc vào máy).

  • nếu lệnh gọi phải add()nhảy qua một XXranh giới byte và mục tiêu không được căn chỉnh.

  • nếu add()không được căn chỉnh.

  • nếu vòng lặp không được căn chỉnh.

2 cái đầu tiên được hiển thị đẹp mắt trên các mã và kết quả mà Marat Dukhan vui lòng đăng . Trong trường hợp này, gcc-4.8.1 -Os(thực hiện trong 0,994 giây):

00000000004004fd <_ZL3addRKiS0_.isra.0>:
  4004fd:       8d 04 37                lea    eax,[rdi+rsi*1]
  400500:       c3   

một ranh giới 256 byte cắt add()ngay ở giữa và cả add()vòng lặp cũng không được căn chỉnh. Bất ngờ, ngạc nhiên, đây là trường hợp chậm nhất!

Trong trường hợp gcc-4.7.3 -Os(thực hiện trong 0.822 giây), ranh giới 256 byte chỉ cắt thành một phần lạnh (nhưng không phải là vòng lặp, cũng không add()bị cắt):

00000000004004fa <_ZL3addRKiS0_.isra.0>:
  4004fa:       8d 04 37                lea    eax,[rdi+rsi*1]
  4004fd:       c3                      ret

[...]

  40051a:       e8 db ff ff ff          call   4004fa <_ZL3addRKiS0_.isra.0>

Không có gì được căn chỉnh và lệnh gọi add()phải nhảy qua ranh giới 256 byte. Mã này là chậm thứ hai.

Trong trường hợp gcc-4.6.4 -Os(thực hiện trong 0,709 giây), mặc dù không có gì được căn chỉnh, lệnh gọi add()không phải nhảy qua ranh giới 256 byte và mục tiêu cách chính xác 32 byte:

  4004f2:       e8 db ff ff ff          call   4004d2 <_ZL3addRKiS0_.isra.0>
  4004f7:       01 c3                   add    ebx,eax
  4004f9:       ff cd                   dec    ebp
  4004fb:       75 ec                   jne    4004e9 <_ZL4workii+0x13>

Đây là nhanh nhất trong cả ba. Tại sao ranh giới 256 byte là không gian trên máy của anh ấy, tôi sẽ để nó cho anh ấy tìm ra. Tôi không có bộ xử lý như vậy.

Bây giờ, trên máy của tôi, tôi không nhận được hiệu ứng ranh giới 256 byte này. Chỉ có chức năng và căn chỉnh vòng lặp khởi động trên máy của tôi. Nếu tôi vượt qua g++ -O2 -falign-functions=16 -falign-loops=16thì mọi thứ sẽ trở lại bình thường: Tôi luôn nhận được trường hợp nhanh nhất và thời gian không còn nhạy cảm với -fno-omit-frame-pointercờ nữa. Tôi có thể vượt qua g++ -O2 -falign-functions=32 -falign-loops=32hoặc bất kỳ bội số nào của 16, mã cũng không nhạy cảm với điều đó.

Lần đầu tiên tôi nhận thấy vào năm 2009 rằng gcc (ít nhất là trong các dự án và trên máy của tôi) có xu hướng tạo mã nhanh hơn đáng kể nếu tôi tối ưu hóa kích thước (-Os) thay vì tốc độ (-O2 hoặc -O3) và tôi đã tự hỏi kể từ khi nào

Một lời giải thích có khả năng là tôi đã có các điểm nóng nhạy cảm với sự liên kết, giống như điểm nóng trong ví dụ này. Bằng cách làm rối những lá cờ (đi qua -Osthay vì -O2), những điểm nóng đó được căn chỉnh một cách may mắn một cách tình cờ và mã trở nên nhanh hơn. Nó không liên quan gì đến việc tối ưu hóa kích thước: Đó là do tai nạn tuyệt đối mà các điểm nóng được căn chỉnh tốt hơn. Từ bây giờ, tôi sẽ kiểm tra ảnh hưởng của sự liên kết đối với các dự án của tôi.

Oh, và một điều nữa. Làm thế nào các điểm nóng như vậy có thể phát sinh, như một trong những ví dụ trong ví dụ? Làm thế nào có thể nội tuyến của một chức năng nhỏ như vậy như add()thất bại?

Xem xét điều này:

// add.cpp
int add(const int& x, const int& y) {
    return x + y;
}

và trong một tệp riêng biệt:

// main.cpp
int add(const int& x, const int& y);

const int LOOP_BOUND = 200000000;

__attribute__((noinline))
static int work(int xval, int yval) {
    int sum(0);
    for (int i=0; i<LOOP_BOUND; ++i) {
        int x(xval+sum);
        int y(yval+sum);
        int z = add(x, y);
        sum += z;
    }
    return sum;
}

int main(int , char* argv[]) {
    int result = work(*argv[1], *argv[2]);
    return result;
}

và được biên dịch thành : g++ -O2 add.cpp main.cpp.

      gcc sẽ không nội tuyến add()!

Đó là tất cả, thật dễ dàng để vô tình tạo ra các điểm nóng như điểm nóng trong OP. Tất nhiên đó là một phần lỗi của tôi: gcc là một trình biên dịch xuất sắc. Nếu biên dịch ở trên là : g++ -O2 -flto add.cpp main.cpp, nghĩa là, nếu tôi thực hiện tối ưu hóa thời gian liên kết, mã sẽ chạy trong 0,19 giây!

(Nội tuyến bị vô hiệu hóa một cách giả tạo trong OP, do đó, mã trong OP chậm hơn 2 lần).


19
Wow ... Điều này chắc chắn vượt xa những gì tôi thường làm để khắc phục sự bất thường về điểm chuẩn.
Bí ẩn

@Ali Tôi đoán điều đó có ý nghĩa vì làm thế nào trình biên dịch có thể nội tuyến một cái gì đó mà nó không nhìn thấy? Đó có lẽ là lý do tại sao chúng ta sử dụng inlineđịnh nghĩa hàm + trong tiêu đề. Không chắc chắn làm thế nào trưởng thành lto là trong gcc. Kinh nghiệm của tôi với nó ít nhất là trong mingw là một hit hoặc bỏ lỡ.
Greatwolf

7
Tôi nghĩ rằng Truyền thông của ACM đã có một bài viết vài năm trước về việc chạy các ứng dụng khá lớn (perl, Spice, v.v.) trong khi chuyển toàn bộ hình ảnh nhị phân một byte mỗi lần bằng cách sử dụng các môi trường Linux có kích thước khác nhau. Tôi nhớ lại phương sai điển hình của 15% hoặc hơn. Tóm tắt của họ là nhiều kết quả điểm chuẩn là vô ích vì biến liên kết bên ngoài này không được tính đến.
Gene

1
đặc biệt là cho -flto. Thật là một cuộc cách mạng nếu bạn chưa từng sử dụng nó trước đây, nói về kinh nghiệm :)
underscore_d

2
Đây là một video tuyệt vời nói về cách căn chỉnh có thể ảnh hưởng đến hiệu suất và cách lập hồ sơ cho nó: youtube.com/watch?time_continue=1&v=r-TLSBdHe1A
Zhro

73

Tôi đang thêm bài chấp nhận này để chỉ ra rằng những ảnh hưởng của sự liên kết đến hiệu suất tổng thể của các chương trình - bao gồm cả những chương trình lớn - đã được nghiên cứu. Ví dụ, bài viết này (và tôi tin rằng một phiên bản này cũng xuất hiện trong CACM) cho thấy mức độ thay đổi thứ tự liên kết và kích thước môi trường hệ điều hành là đủ để thay đổi hiệu suất đáng kể. Họ gán thuộc tính này cho sự liên kết của "các vòng nóng".

Bài viết này, có tiêu đề "Sản xuất dữ liệu sai mà không làm gì rõ ràng là sai!" nói rằng sự thiên vị thử nghiệm vô tình do sự khác biệt gần như không thể kiểm soát được trong môi trường chạy chương trình có thể khiến nhiều kết quả điểm chuẩn trở nên vô nghĩa.

Tôi nghĩ rằng bạn đang gặp một góc độ khác nhau trên cùng một quan sát.

Đối với mã quan trọng về hiệu năng, đây là một đối số khá tốt cho các hệ thống đánh giá môi trường lúc cài đặt hoặc thời gian chạy và chọn địa phương tốt nhất trong số các phiên bản được tối ưu hóa khác nhau của các thói quen chính.


33

Tôi nghĩ rằng bạn có thể có được kết quả giống như những gì bạn đã làm:

Tôi đã nắm lấy tổ hợp cho -O2 và hợp nhất tất cả các khác biệt của nó vào tổ hợp cho -Os ngoại trừ các dòng .p2align:

Bằng cách sử dụng -O2 -falign-functions=1 -falign-jumps=1 -falign-loops=1 -falign-labels=1. Tôi đã tổng hợp mọi thứ với các tùy chọn này, nhanh hơn -O2mọi lúc tôi bận tâm để đo, trong 15 năm.

Ngoài ra, đối với một bối cảnh hoàn toàn khác (bao gồm một trình biên dịch khác), tôi nhận thấy rằng tình huống này tương tự nhau : tùy chọn được cho là tối ưu hóa kích thước mã thay vì tốc độ tối ưu hóa cho kích thước và tốc độ mã.

Nếu tôi đoán chính xác, đây là những miếng đệm để căn chỉnh ngăn xếp.

Không, điều này không liên quan gì đến ngăn xếp, các NOP được tạo theo mặc định và các tùy chọn -falign - * = 1 ngăn chặn để căn chỉnh mã.

Theo lý do tại sao GCC pad hoạt động với NOP? nó được thực hiện với hy vọng rằng mã sẽ chạy nhanh hơn nhưng rõ ràng việc tối ưu hóa này đã phản tác dụng trong trường hợp của tôi.

Có phải đó là phần đệm là thủ phạm trong trường hợp này? Lý do tại sao và làm thế nào?

Rất có khả năng phần đệm là thủ phạm. Lý do đệm được cảm thấy là cần thiết và hữu ích trong một số trường hợp là mã thường được tìm nạp trong các dòng 16 byte (xem tài nguyên tối ưu hóa của Agner Fog để biết chi tiết, thay đổi theo mô hình bộ xử lý). Căn chỉnh một hàm, vòng lặp hoặc nhãn trên ranh giới 16 byte có nghĩa là cơ hội được tăng lên theo thống kê rằng sẽ cần ít hơn một dòng để chứa hàm hoặc vòng lặp. Rõ ràng, nó phản tác dụng vì các NOP này làm giảm mật độ mã và do đó hiệu quả của bộ đệm. Trong trường hợp các vòng lặp và nhãn, các NOP thậm chí có thể cần được thực thi một lần (khi thực thi đến vòng lặp / nhãn thông thường, trái ngược với bước nhảy).


Điều buồn cười là: -O2 -fno-omit-frame-pointercũng tốt như vậy -Os. Vui lòng kiểm tra câu hỏi cập nhật.
Ali

11

Nếu chương trình của bạn bị giới hạn bởi bộ đệm CODE L1, thì việc tối ưu hóa kích thước đột nhiên bắt đầu được thanh toán.

Khi tôi kiểm tra lần cuối, trình biên dịch không đủ thông minh để tìm ra điều này trong mọi trường hợp.

Trong trường hợp của bạn, -O3 có thể tạo mã đủ cho hai dòng bộ đệm, nhưng -O phù hợp với một dòng bộ đệm.


1
Bao nhiêu bạn muốn đặt cược những align = tham số liên quan đến kích thước của các dòng bộ đệm?
Joshua

Tôi không thực sự quan tâm nữa: Nó không hiển thị trên máy của tôi. Và bằng cách chuyền -falign-*=16cờ, mọi thứ trở lại bình thường, mọi thứ đều hoạt động ổn định. Theo tôi thấy, câu hỏi này đã được giải quyết.
Ali

7

Tôi không có nghĩa là một chuyên gia trong lĩnh vực này, nhưng tôi dường như nhớ rằng các bộ xử lý hiện đại khá nhạy cảm khi nói đến dự đoán chi nhánh . Các thuật toán được sử dụng để dự đoán các nhánh là (hoặc ít nhất là trở lại trong những ngày tôi viết mã trình biên dịch mã) dựa trên một số thuộc tính của mã, bao gồm khoảng cách của mục tiêu và hướng.

Kịch bản mà đến với tâm trí là các vòng nhỏ. Khi nhánh rẽ về phía sau và khoảng cách không quá xa, dự đoán nhánh sẽ tối ưu hóa cho trường hợp này vì tất cả các vòng nhỏ được thực hiện theo cách này. Các quy tắc tương tự có thể có hiệu lực khi bạn trao đổi vị trí addworktrong mã được tạo hoặc khi vị trí của cả hai thay đổi một chút.

Điều đó nói rằng, tôi không biết làm thế nào để xác minh điều đó và tôi chỉ muốn cho bạn biết rằng đây có thể là một cái gì đó bạn muốn xem xét.


Cảm ơn. Tôi đã chơi với nó: Tôi chỉ tăng tốc bằng cách hoán đổi add()work()nếu -O2được thông qua. Trong tất cả các trường hợp khác, mã trở nên chậm hơn đáng kể bằng cách hoán đổi. Vào cuối tuần, tôi cũng đã phân tích các thống kê dự đoán / dự đoán sai chi nhánh perfvà tôi không nhận thấy bất cứ điều gì có thể giải thích cho hành vi kỳ lạ này. Kết quả nhất quán duy nhất là trong trường hợp chậm perfbáo cáo 100.0 in add()và một giá trị lớn trên dòng ngay sau khi gọi đến add()trong vòng lặp. Có vẻ như chúng tôi đang bị đình trệ vì một số lý do add()trong trường hợp chậm nhưng không phải là chạy nhanh.
Ali

Tôi đang suy nghĩ về việc cài đặt VTune của Intel trên một trong các máy của mình và tự làm hồ sơ. perfchỉ hỗ trợ một số thứ giới hạn, có lẽ công cụ của Intel tiện dụng hơn một chút trên bộ xử lý của riêng họ.
Ali
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.