std :: hồi quy hiệu suất vector khi bật C ++ 11


235

Tôi đã tìm thấy một hồi quy hiệu suất thú vị trong một đoạn C ++ nhỏ, khi tôi bật C ++ 11:

#include <vector>

struct Item
{
  int a;
  int b;
};

int main()
{
  const std::size_t num_items = 10000000;
  std::vector<Item> container;
  container.reserve(num_items);
  for (std::size_t i = 0; i < num_items; ++i) {
    container.push_back(Item());
  }
  return 0;
}

Với g ++ (GCC) 4.8.2 20131219 (phát hành trước) và C ++ 03 tôi nhận được:

milian:/tmp$ g++ -O3 main.cpp && perf stat -r 10 ./a.out

Performance counter stats for './a.out' (10 runs):

        35.206824 task-clock                #    0.988 CPUs utilized            ( +-  1.23% )
                4 context-switches          #    0.116 K/sec                    ( +-  4.38% )
                0 cpu-migrations            #    0.006 K/sec                    ( +- 66.67% )
              849 page-faults               #    0.024 M/sec                    ( +-  6.02% )
       95,693,808 cycles                    #    2.718 GHz                      ( +-  1.14% ) [49.72%]
  <not supported> stalled-cycles-frontend 
  <not supported> stalled-cycles-backend  
       95,282,359 instructions              #    1.00  insns per cycle          ( +-  0.65% ) [75.27%]
       30,104,021 branches                  #  855.062 M/sec                    ( +-  0.87% ) [77.46%]
            6,038 branch-misses             #    0.02% of all branches          ( +- 25.73% ) [75.53%]

      0.035648729 seconds time elapsed                                          ( +-  1.22% )

Mặt khác, với C ++ 11 được bật, hiệu suất sẽ giảm đáng kể:

milian:/tmp$ g++ -std=c++11 -O3 main.cpp && perf stat -r 10 ./a.out

Performance counter stats for './a.out' (10 runs):

        86.485313 task-clock                #    0.994 CPUs utilized            ( +-  0.50% )
                9 context-switches          #    0.104 K/sec                    ( +-  1.66% )
                2 cpu-migrations            #    0.017 K/sec                    ( +- 26.76% )
              798 page-faults               #    0.009 M/sec                    ( +-  8.54% )
      237,982,690 cycles                    #    2.752 GHz                      ( +-  0.41% ) [51.32%]
  <not supported> stalled-cycles-frontend 
  <not supported> stalled-cycles-backend  
      135,730,319 instructions              #    0.57  insns per cycle          ( +-  0.32% ) [75.77%]
       30,880,156 branches                  #  357.057 M/sec                    ( +-  0.25% ) [75.76%]
            4,188 branch-misses             #    0.01% of all branches          ( +-  7.59% ) [74.08%]

    0.087016724 seconds time elapsed                                          ( +-  0.50% )

Ai đó có thể giải thích điều này? Cho đến nay kinh nghiệm của tôi là STL trở nên nhanh hơn bằng cách kích hoạt C ++ 11, đặc biệt. nhờ di chuyển ngữ nghĩa.

EDIT: Như đã đề xuất, container.emplace_back();thay vào đó, hiệu suất sẽ ngang bằng với phiên bản C ++ 03. Làm thế nào để phiên bản C ++ 03 có thể đạt được điều tương tự push_back?

milian:/tmp$ g++ -std=c++11 -O3 main.cpp && perf stat -r 10 ./a.out

Performance counter stats for './a.out' (10 runs):

        36.229348 task-clock                #    0.988 CPUs utilized            ( +-  0.81% )
                4 context-switches          #    0.116 K/sec                    ( +-  3.17% )
                1 cpu-migrations            #    0.017 K/sec                    ( +- 36.85% )
              798 page-faults               #    0.022 M/sec                    ( +-  8.54% )
       94,488,818 cycles                    #    2.608 GHz                      ( +-  1.11% ) [50.44%]
  <not supported> stalled-cycles-frontend 
  <not supported> stalled-cycles-backend  
       94,851,411 instructions              #    1.00  insns per cycle          ( +-  0.98% ) [75.22%]
       30,468,562 branches                  #  840.991 M/sec                    ( +-  1.07% ) [76.71%]
            2,723 branch-misses             #    0.01% of all branches          ( +-  9.84% ) [74.81%]

   0.036678068 seconds time elapsed                                          ( +-  0.80% )

1
Nếu bạn biên dịch để lắp ráp, bạn có thể thấy những gì đang diễn ra dưới mui xe. Xem thêm stackoverflow.com/questions/8021874/ Lời
Bánh răng

8
Chuyện gì xảy ra nếu bạn thay đổi push_back(Item())để emplace_back()trong thư mục C ++ 11 phiên bản?
Bánh răng

8
Xem ở trên, "sửa chữa" hồi quy. Tôi vẫn tự hỏi tại sao Push_back hồi quy về hiệu suất giữa C ++ 03 và C ++ 11 mặc dù.
milianw

1
@milianw Hóa ra tôi đang biên dịch chương trình sai. Bỏ qua ý kiến ​​của tôi.

2
Với clang3.4, phiên bản C ++ 11 nhanh hơn, 0,047s so với 0,058 cho phiên bản C ++ 98
Praetorian

Câu trả lời:


247

Tôi có thể sao chép kết quả của bạn trên máy của tôi với các tùy chọn bạn viết trong bài đăng của mình.

Tuy nhiên, nếu tôi cũng kích hoạt tối ưu hóa thời gian liên kết (tôi cũng chuyển -fltocờ cho gcc 4.7.2), kết quả giống hệt nhau:

(Tôi đang biên dịch mã gốc của bạn, với container.push_back(Item());)

$ g++ -std=c++11 -O3 -flto regr.cpp && perf stat -r 10 ./a.out 

 Performance counter stats for './a.out' (10 runs):

         35.426793 task-clock                #    0.986 CPUs utilized            ( +-  1.75% )
                 4 context-switches          #    0.116 K/sec                    ( +-  5.69% )
                 0 CPU-migrations            #    0.006 K/sec                    ( +- 66.67% )
            19,801 page-faults               #    0.559 M/sec                  
        99,028,466 cycles                    #    2.795 GHz                      ( +-  1.89% ) [77.53%]
        50,721,061 stalled-cycles-frontend   #   51.22% frontend cycles idle     ( +-  3.74% ) [79.47%]
        25,585,331 stalled-cycles-backend    #   25.84% backend  cycles idle     ( +-  4.90% ) [73.07%]
       141,947,224 instructions              #    1.43  insns per cycle        
                                             #    0.36  stalled cycles per insn  ( +-  0.52% ) [88.72%]
        37,697,368 branches                  # 1064.092 M/sec                    ( +-  0.52% ) [88.75%]
            26,700 branch-misses             #    0.07% of all branches          ( +-  3.91% ) [83.64%]

       0.035943226 seconds time elapsed                                          ( +-  1.79% )



$ g++ -std=c++98 -O3 -flto regr.cpp && perf stat -r 10 ./a.out 

 Performance counter stats for './a.out' (10 runs):

         35.510495 task-clock                #    0.988 CPUs utilized            ( +-  2.54% )
                 4 context-switches          #    0.101 K/sec                    ( +-  7.41% )
                 0 CPU-migrations            #    0.003 K/sec                    ( +-100.00% )
            19,801 page-faults               #    0.558 M/sec                    ( +-  0.00% )
        98,463,570 cycles                    #    2.773 GHz                      ( +-  1.09% ) [77.71%]
        50,079,978 stalled-cycles-frontend   #   50.86% frontend cycles idle     ( +-  2.20% ) [79.41%]
        26,270,699 stalled-cycles-backend    #   26.68% backend  cycles idle     ( +-  8.91% ) [74.43%]
       141,427,211 instructions              #    1.44  insns per cycle        
                                             #    0.35  stalled cycles per insn  ( +-  0.23% ) [87.66%]
        37,366,375 branches                  # 1052.263 M/sec                    ( +-  0.48% ) [88.61%]
            26,621 branch-misses             #    0.07% of all branches          ( +-  5.28% ) [83.26%]

       0.035953916 seconds time elapsed  

Về các lý do, người ta cần xem mã lắp ráp được tạo ( g++ -std=c++11 -O3 -S regr.cpp). Trong chế độ C ++ 11, mã được tạo lộn xộn hơn đáng kể so với chế độ C ++ 98 và nội tuyến
void std::vector<Item,std::allocator<Item>>::_M_emplace_back_aux<Item>(Item&&)
không hoạt động trong chế độ C ++ 11 với mặc định inline-limit.

Nội tuyến thất bại này có hiệu ứng domino. Không phải vì hàm này đang được gọi (nó thậm chí không được gọi!) Mà bởi vì chúng ta phải chuẩn bị: Nếu nó được gọi, các đối số của hàm ( Item.aItem.b) phải được đặt đúng chỗ. Điều này dẫn đến một mã khá lộn xộn.

Đây là phần có liên quan của mã được tạo cho trường hợp nội tuyến thành công :

.L42:
    testq   %rbx, %rbx  # container$D13376$_M_impl$_M_finish
    je  .L3 #,
    movl    $0, (%rbx)  #, container$D13376$_M_impl$_M_finish_136->a
    movl    $0, 4(%rbx) #, container$D13376$_M_impl$_M_finish_136->b
.L3:
    addq    $8, %rbx    #, container$D13376$_M_impl$_M_finish
    subq    $1, %rbp    #, ivtmp.106
    je  .L41    #,
.L14:
    cmpq    %rbx, %rdx  # container$D13376$_M_impl$_M_finish, container$D13376$_M_impl$_M_end_of_storage
    jne .L42    #,

Đây là một vòng lặp tốt đẹp và nhỏ gọn. Bây giờ, hãy so sánh điều này với trường hợp nội tuyến thất bại :

.L49:
    testq   %rax, %rax  # D.15772
    je  .L26    #,
    movq    16(%rsp), %rdx  # D.13379, D.13379
    movq    %rdx, (%rax)    # D.13379, *D.15772_60
.L26:
    addq    $8, %rax    #, tmp75
    subq    $1, %rbx    #, ivtmp.117
    movq    %rax, 40(%rsp)  # tmp75, container.D.13376._M_impl._M_finish
    je  .L48    #,
.L28:
    movq    40(%rsp), %rax  # container.D.13376._M_impl._M_finish, D.15772
    cmpq    48(%rsp), %rax  # container.D.13376._M_impl._M_end_of_storage, D.15772
    movl    $0, 16(%rsp)    #, D.13379.a
    movl    $0, 20(%rsp)    #, D.13379.b
    jne .L49    #,
    leaq    16(%rsp), %rsi  #,
    leaq    32(%rsp), %rdi  #,
    call    _ZNSt6vectorI4ItemSaIS0_EE19_M_emplace_back_auxIIS0_EEEvDpOT_   #

Mã này bị lộn xộn và có nhiều thứ đang diễn ra trong vòng lặp so với trường hợp trước. Trước hàm call(dòng cuối cùng được hiển thị), các đối số phải được đặt một cách thích hợp:

leaq    16(%rsp), %rsi  #,
leaq    32(%rsp), %rdi  #,
call    _ZNSt6vectorI4ItemSaIS0_EE19_M_emplace_back_auxIIS0_EEEvDpOT_   #

Mặc dù điều này không bao giờ thực sự được thực thi, vòng lặp sắp xếp mọi thứ trước đó:

movl    $0, 16(%rsp)    #, D.13379.a
movl    $0, 20(%rsp)    #, D.13379.b

Điều này dẫn đến mã lộn xộn. Nếu không có chức năng nào callvì nội tuyến thành công, chúng ta chỉ có 2 hướng dẫn di chuyển trong vòng lặp và không có sự lộn xộn nào xảy ra với %rsp(con trỏ ngăn xếp). Tuy nhiên, nếu nội tuyến thất bại, chúng tôi nhận được 6 di chuyển và chúng tôi gây rối rất nhiều với %rsp.

Chỉ để chứng minh lý thuyết của tôi (lưu ý -finline-limit), cả hai trong chế độ C ++ 11:

 $ g++ -std=c++11 -O3 -finline-limit=105 regr.cpp && perf stat -r 10 ./a.out

 Performance counter stats for './a.out' (10 runs):

         84.739057 task-clock                #    0.993 CPUs utilized            ( +-  1.34% )
                 8 context-switches          #    0.096 K/sec                    ( +-  2.22% )
                 1 CPU-migrations            #    0.009 K/sec                    ( +- 64.01% )
            19,801 page-faults               #    0.234 M/sec                  
       266,809,312 cycles                    #    3.149 GHz                      ( +-  0.58% ) [81.20%]
       206,804,948 stalled-cycles-frontend   #   77.51% frontend cycles idle     ( +-  0.91% ) [81.25%]
       129,078,683 stalled-cycles-backend    #   48.38% backend  cycles idle     ( +-  1.37% ) [69.49%]
       183,130,306 instructions              #    0.69  insns per cycle        
                                             #    1.13  stalled cycles per insn  ( +-  0.85% ) [85.35%]
        38,759,720 branches                  #  457.401 M/sec                    ( +-  0.29% ) [85.43%]
            24,527 branch-misses             #    0.06% of all branches          ( +-  2.66% ) [83.52%]

       0.085359326 seconds time elapsed                                          ( +-  1.31% )

 $ g++ -std=c++11 -O3 -finline-limit=106 regr.cpp && perf stat -r 10 ./a.out

 Performance counter stats for './a.out' (10 runs):

         37.790325 task-clock                #    0.990 CPUs utilized            ( +-  2.06% )
                 4 context-switches          #    0.098 K/sec                    ( +-  5.77% )
                 0 CPU-migrations            #    0.011 K/sec                    ( +- 55.28% )
            19,801 page-faults               #    0.524 M/sec                  
       104,699,973 cycles                    #    2.771 GHz                      ( +-  2.04% ) [78.91%]
        58,023,151 stalled-cycles-frontend   #   55.42% frontend cycles idle     ( +-  4.03% ) [78.88%]
        30,572,036 stalled-cycles-backend    #   29.20% backend  cycles idle     ( +-  5.31% ) [71.40%]
       140,669,773 instructions              #    1.34  insns per cycle        
                                             #    0.41  stalled cycles per insn  ( +-  1.40% ) [88.14%]
        38,117,067 branches                  # 1008.646 M/sec                    ( +-  0.65% ) [89.38%]
            27,519 branch-misses             #    0.07% of all branches          ( +-  4.01% ) [86.16%]

       0.038187580 seconds time elapsed                                          ( +-  2.05% )

Thật vậy, nếu chúng ta yêu cầu trình biên dịch thử chỉ một chút khó khăn hơn để nội tuyến hàm đó, sự khác biệt về hiệu suất sẽ biến mất.


Vì vậy, những gì lấy đi từ câu chuyện này? Điều đó không thành công có thể khiến bạn tốn rất nhiều chi phí và bạn nên tận dụng hết khả năng của trình biên dịch: Tôi chỉ có thể đề xuất tối ưu hóa thời gian liên kết. Nó đã tăng hiệu suất đáng kể cho các chương trình của tôi (lên tới 2,5 lần) và tất cả những gì tôi cần làm là vượt qua -fltocờ. Đó là một thỏa thuận khá tốt! ;)

Tuy nhiên, tôi không khuyên bạn nên chuyển mã của bạn bằng từ khóa nội tuyến; hãy để trình biên dịch quyết định làm gì (Trình tối ưu hóa được phép coi từ khóa nội tuyến là khoảng trắng.)


Câu hỏi hay, +1!


3
NB: inlinekhông có gì để làm với chức năng nội tuyến; điều đó có nghĩa là nội tuyến được xác định là nội tuyến và không làm ơn Nếu bạn thực sự muốn yêu cầu nội tuyến, sử dụng __attribute__((always_inline))hoặc tương tự.
Jon Purdy

2
@JonPurdy Không hoàn toàn, ví dụ các hàm thành viên lớp là ngầm định. inlinecũng là một yêu cầu đối với trình biên dịch mà bạn muốn hàm được nội tuyến và ví dụ Trình biên dịch Intel C ++ được sử dụng để đưa ra các cảnh báo về hiệu năng nếu nó không đáp ứng yêu cầu của bạn. (Tôi đã không kiểm tra icc gần đây nếu nó vẫn còn.) Thật không may, tôi đã thấy mọi người bỏ qua mã của họ inlinevà chờ đợi phép màu xảy ra. Tôi sẽ không sử dụng __attribute__((always_inline)); rất có thể các nhà phát triển trình biên dịch biết rõ hơn những gì nội tuyến và những gì không. (Mặc dù có ví dụ ở đây.)
Ali

1
@JonPurdy Mặt khác, nếu bạn xác định một hàm nội tuyến không phải là hàm thành viên của một lớp , thì bạn thực sự không có lựa chọn nào khác ngoài việc đánh dấu nó là nội tuyến nếu không bạn sẽ gặp nhiều lỗi định nghĩa từ trình liên kết. Nếu đó là những gì bạn muốn thì OK.
Ali

1
Vâng, đó là những gì tôi có ý nghĩa. Tiêu chuẩn có nói là Công inlinecụ chỉ định cho biết việc thực hiện thay thế nội tuyến của thân hàm tại điểm gọi sẽ được ưu tiên hơn so với cơ chế gọi hàm thông thường. (§7.1.2.2) Tuy nhiên, việc triển khai không bắt buộc phải thực hiện tối ưu hóa đó, vì phần lớn là sự trùng hợp ngẫu nhiên mà các inlinechức năng thường xảy ra là ứng cử viên tốt cho nội tuyến. Vì vậy, tốt hơn là nên rõ ràng và sử dụng một pragma trình biên dịch.
Jon Purdy

3
@JonPurdy Đối với nửa đầu: Vâng, đó là điều tôi muốn nói khi nói "Trình tối ưu hóa được phép coi từ khóa nội tuyến là khoảng trắng." Đối với pragma của trình biên dịch, tôi sẽ không sử dụng nó, tôi sẽ để nó tối ưu hóa thời gian liên kết cho dù là nội tuyến hay không. Nó làm một công việc khá tốt; nó cũng tự động giải quyết vấn đề này được thảo luận ở đây trong câu trả lời.
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.