Là 'chuyển đổi' nhanh hơn 'nếu'?


242

Là một switchtuyên bố thực sự nhanh hơn một iftuyên bố?

Tôi đã chạy mã dưới đây trên trình biên dịch x64 C ++ của Visual Studio 2010 với /Oxcờ:

#include <stdlib.h>
#include <stdio.h>
#include <time.h>

#define MAX_COUNT (1 << 29)
size_t counter = 0;

size_t testSwitch()
{
    clock_t start = clock();
    size_t i;
    for (i = 0; i < MAX_COUNT; i++)
    {
        switch (counter % 4 + 1)
        {
            case 1: counter += 4; break;
            case 2: counter += 3; break;
            case 3: counter += 2; break;
            case 4: counter += 1; break;
        }
    }
    return 1000 * (clock() - start) / CLOCKS_PER_SEC;
}

size_t testIf()
{
    clock_t start = clock();
    size_t i;
    for (i = 0; i < MAX_COUNT; i++)
    {
        const size_t c = counter % 4 + 1;
        if (c == 1) { counter += 4; }
        else if (c == 2) { counter += 3; }
        else if (c == 3) { counter += 2; }
        else if (c == 4) { counter += 1; }
    }
    return 1000 * (clock() - start) / CLOCKS_PER_SEC;
}

int main()
{
    printf("Starting...\n");
    printf("Switch statement: %u ms\n", testSwitch());
    printf("If     statement: %u ms\n", testIf());
}

và nhận được những kết quả này:

Câu lệnh chuyển đổi: 5261 ms
Nếu câu lệnh: 5196 ms

Từ những gì tôi đã học được, các switchcâu lệnh dường như sử dụng các bảng nhảy để tối ưu hóa việc phân nhánh.

Câu hỏi:

  1. Một bảng nhảy cơ bản trông như thế nào, trong x86 hoặc x64?

  2. Là mã này bằng cách sử dụng một bảng nhảy?

  3. Tại sao không có sự khác biệt hiệu suất trong ví dụ này? Có bất kỳ tình huống trong đó một sự khác biệt đáng kể hiệu suất?


Tháo gỡ mã:

testIf:

13FE81B10 sub  rsp,48h 
13FE81B14 call qword ptr [__imp_clock (13FE81128h)] 
13FE81B1A mov  dword ptr [start],eax 
13FE81B1E mov  qword ptr [i],0 
13FE81B27 jmp  testIf+26h (13FE81B36h) 
13FE81B29 mov  rax,qword ptr [i] 
13FE81B2E inc  rax  
13FE81B31 mov  qword ptr [i],rax 
13FE81B36 cmp  qword ptr [i],20000000h 
13FE81B3F jae  testIf+0C3h (13FE81BD3h) 
13FE81B45 xor  edx,edx 
13FE81B47 mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81B4E mov  ecx,4 
13FE81B53 div  rax,rcx 
13FE81B56 mov  rax,rdx 
13FE81B59 inc  rax  
13FE81B5C mov  qword ptr [c],rax 
13FE81B61 cmp  qword ptr [c],1 
13FE81B67 jne  testIf+6Dh (13FE81B7Dh) 
13FE81B69 mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81B70 add  rax,4 
13FE81B74 mov  qword ptr [counter (13FE835D0h)],rax 
13FE81B7B jmp  testIf+0BEh (13FE81BCEh) 
13FE81B7D cmp  qword ptr [c],2 
13FE81B83 jne  testIf+89h (13FE81B99h) 
13FE81B85 mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81B8C add  rax,3 
13FE81B90 mov  qword ptr [counter (13FE835D0h)],rax 
13FE81B97 jmp  testIf+0BEh (13FE81BCEh) 
13FE81B99 cmp  qword ptr [c],3 
13FE81B9F jne  testIf+0A5h (13FE81BB5h) 
13FE81BA1 mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81BA8 add  rax,2 
13FE81BAC mov  qword ptr [counter (13FE835D0h)],rax 
13FE81BB3 jmp  testIf+0BEh (13FE81BCEh) 
13FE81BB5 cmp  qword ptr [c],4 
13FE81BBB jne  testIf+0BEh (13FE81BCEh) 
13FE81BBD mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81BC4 inc  rax  
13FE81BC7 mov  qword ptr [counter (13FE835D0h)],rax 
13FE81BCE jmp  testIf+19h (13FE81B29h) 
13FE81BD3 call qword ptr [__imp_clock (13FE81128h)] 
13FE81BD9 sub  eax,dword ptr [start] 
13FE81BDD imul eax,eax,3E8h 
13FE81BE3 cdq       
13FE81BE4 mov  ecx,3E8h 
13FE81BE9 idiv eax,ecx 
13FE81BEB cdqe      
13FE81BED add  rsp,48h 
13FE81BF1 ret       

testSwitch:

13FE81C00 sub  rsp,48h 
13FE81C04 call qword ptr [__imp_clock (13FE81128h)] 
13FE81C0A mov  dword ptr [start],eax 
13FE81C0E mov  qword ptr [i],0 
13FE81C17 jmp  testSwitch+26h (13FE81C26h) 
13FE81C19 mov  rax,qword ptr [i] 
13FE81C1E inc  rax  
13FE81C21 mov  qword ptr [i],rax 
13FE81C26 cmp  qword ptr [i],20000000h 
13FE81C2F jae  testSwitch+0C5h (13FE81CC5h) 
13FE81C35 xor  edx,edx 
13FE81C37 mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81C3E mov  ecx,4 
13FE81C43 div  rax,rcx 
13FE81C46 mov  rax,rdx 
13FE81C49 inc  rax  
13FE81C4C mov  qword ptr [rsp+30h],rax 
13FE81C51 cmp  qword ptr [rsp+30h],1 
13FE81C57 je   testSwitch+73h (13FE81C73h) 
13FE81C59 cmp  qword ptr [rsp+30h],2 
13FE81C5F je   testSwitch+87h (13FE81C87h) 
13FE81C61 cmp  qword ptr [rsp+30h],3 
13FE81C67 je   testSwitch+9Bh (13FE81C9Bh) 
13FE81C69 cmp  qword ptr [rsp+30h],4 
13FE81C6F je   testSwitch+0AFh (13FE81CAFh) 
13FE81C71 jmp  testSwitch+0C0h (13FE81CC0h) 
13FE81C73 mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81C7A add  rax,4 
13FE81C7E mov  qword ptr [counter (13FE835D0h)],rax 
13FE81C85 jmp  testSwitch+0C0h (13FE81CC0h) 
13FE81C87 mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81C8E add  rax,3 
13FE81C92 mov  qword ptr [counter (13FE835D0h)],rax 
13FE81C99 jmp  testSwitch+0C0h (13FE81CC0h) 
13FE81C9B mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81CA2 add  rax,2 
13FE81CA6 mov  qword ptr [counter (13FE835D0h)],rax 
13FE81CAD jmp  testSwitch+0C0h (13FE81CC0h) 
13FE81CAF mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81CB6 inc  rax  
13FE81CB9 mov  qword ptr [counter (13FE835D0h)],rax 
13FE81CC0 jmp  testSwitch+19h (13FE81C19h) 
13FE81CC5 call qword ptr [__imp_clock (13FE81128h)] 
13FE81CCB sub  eax,dword ptr [start] 
13FE81CCF imul eax,eax,3E8h 
13FE81CD5 cdq       
13FE81CD6 mov  ecx,3E8h 
13FE81CDB idiv eax,ecx 
13FE81CDD cdqe      
13FE81CDF add  rsp,48h 
13FE81CE3 ret       

Cập nhật:

Kết quả thú vị ở đây . Không chắc chắn tại sao một người nhanh hơn và một người chậm hơn, mặc dù.


47
Những gì trên trái đất là những người bỏ phiếu để đóng suy nghĩ này? Có phải họ tin vào khái niệm trình biên dịch tối ưu hóa hoàn hảo đến mức mọi người nghĩ rằng nó tạo ra ít hơn mã lý tưởng là dị giáo? Có phải chính ý tưởng về bất kỳ tối ưu hóa bất cứ nơi nào xúc phạm họ?
Crashworks

6
Chính xác thì điều gì là sai với câu hỏi này?
Tugrul Ates

25
Đối với bất cứ ai tự hỏi điều gì là sai với câu hỏi này : Đối với người mới bắt đầu, nó không phải là một câu hỏi, đó là 3 câu hỏi, có nghĩa là nhiều câu trả lời hiện nay giải quyết các vấn đề khác nhau. Điều này có nghĩa là sẽ khó chấp nhận bất kỳ câu trả lời nào trả lời tất cả mọi thứ . Ngoài ra, phản ứng giật đầu gối điển hình cho câu hỏi trên là đóng nó lại vì "không thực sự thú vị" chủ yếu là do ở mức tối ưu hóa này, bạn hầu như luôn tối ưu hóa sớm . Cuối cùng, 5196 so với 5261 không đủ để thực sự quan tâm. Viết mã logic có ý nghĩa.
Lasse V. Karlsen

40
@Lasse: Thay vào đó, bạn có thực sự muốn tôi đăng ba câu hỏi về SO không? Ngoài ra: 5196 vs. 5261 shouldn't be enough to actually care-> Tôi không chắc chắn nếu bạn hiểu lầm câu hỏi hoặc nếu tôi hiểu sai ý kiến của bạn, nhưng không phải là toàn bộ các điểm câu hỏi của tôi để hỏi tại sao có không phải là một sự khác biệt? (Tôi có bao giờ cho rằng đó là một sự khác biệt đáng kể để quan tâm không?)
user541686

5
@Robert: Vâng, nó chỉ có hơn 20 bình luận về nó bởi vì chúng là những bình luận meta. Chỉ có 7 bình luận thực sự liên quan đến câu hỏi ở đây. Ý kiến: Tôi không thấy có "ý kiến" ở đây như thế nào. Có một lý do mà tôi không thấy sự khác biệt về hiệu suất, phải không? Có phải chỉ là hương vị? Tranh luận: Có thể, nhưng có vẻ như là một cuộc tranh luận lành mạnh với tôi, như tôi đã thấy ở những nơi khác trên SO (hãy cho tôi biết nếu có bất cứ điều gì chống lại điều đó). Luận điểm: Tôi không thấy bất cứ điều gì tranh luận ở đây (trừ khi bạn lấy nó làm từ đồng nghĩa cho 'tranh luận'?). Thảo luận mở rộng: Nếu bạn bao gồm các bình luận meta này.
dùng541686

Câu trả lời:


122

Có một số tối ưu hóa trình biên dịch có thể thực hiện trên một chuyển đổi. Tôi không nghĩ rằng "bảng nhảy" được đề cập đến là một cái rất hữu ích, vì nó chỉ hoạt động khi đầu vào có thể bị chặn theo một cách nào đó.

C Mã giả cho "bảng nhảy" sẽ giống như thế này - lưu ý rằng trình biên dịch trong thực tế sẽ cần chèn một số dạng nếu kiểm tra xung quanh bảng để đảm bảo rằng đầu vào là hợp lệ trong bảng. Cũng lưu ý rằng nó chỉ hoạt động trong trường hợp cụ thể rằng đầu vào là một chuỗi các số liên tiếp.

Nếu số lượng nhánh trong một công tắc là rất lớn, trình biên dịch có thể thực hiện những việc như sử dụng tìm kiếm nhị phân trên các giá trị của công tắc, điều này (theo tôi) sẽ là một tối ưu hóa hữu ích hơn nhiều, vì nó làm tăng đáng kể hiệu suất trong một số các kịch bản, nói chung như một chuyển đổi, và không dẫn đến kích thước mã được tạo lớn hơn. Nhưng để thấy điều đó, mã kiểm tra của bạn sẽ cần nhiều chi nhánh hơn để thấy bất kỳ sự khác biệt nào.

Để trả lời các câu hỏi cụ thể của bạn:

  1. Clang tạo ra một cái trông như thế này :

    test_switch(char):                       # @test_switch(char)
            movl    %edi, %eax
            cmpl    $19, %edi
            jbe     .LBB0_1
            retq
    .LBB0_1:
            jmpq    *.LJTI0_0(,%rax,8)
            jmp     void call<0u>()         # TAILCALL
            jmp     void call<1u>()         # TAILCALL
            jmp     void call<2u>()         # TAILCALL
            jmp     void call<3u>()         # TAILCALL
            jmp     void call<4u>()         # TAILCALL
            jmp     void call<5u>()         # TAILCALL
            jmp     void call<6u>()         # TAILCALL
            jmp     void call<7u>()         # TAILCALL
            jmp     void call<8u>()         # TAILCALL
            jmp     void call<9u>()         # TAILCALL
            jmp     void call<10u>()        # TAILCALL
            jmp     void call<11u>()        # TAILCALL
            jmp     void call<12u>()        # TAILCALL
            jmp     void call<13u>()        # TAILCALL
            jmp     void call<14u>()        # TAILCALL
            jmp     void call<15u>()        # TAILCALL
            jmp     void call<16u>()        # TAILCALL
            jmp     void call<17u>()        # TAILCALL
            jmp     void call<18u>()        # TAILCALL
            jmp     void call<19u>()        # TAILCALL
    .LJTI0_0:
            .quad   .LBB0_2
            .quad   .LBB0_3
            .quad   .LBB0_4
            .quad   .LBB0_5
            .quad   .LBB0_6
            .quad   .LBB0_7
            .quad   .LBB0_8
            .quad   .LBB0_9
            .quad   .LBB0_10
            .quad   .LBB0_11
            .quad   .LBB0_12
            .quad   .LBB0_13
            .quad   .LBB0_14
            .quad   .LBB0_15
            .quad   .LBB0_16
            .quad   .LBB0_17
            .quad   .LBB0_18
            .quad   .LBB0_19
            .quad   .LBB0_20
            .quad   .LBB0_21
  2. Tôi có thể nói rằng nó không sử dụng bảng nhảy - 4 hướng dẫn so sánh được hiển thị rõ ràng:

    13FE81C51 cmp  qword ptr [rsp+30h],1 
    13FE81C57 je   testSwitch+73h (13FE81C73h) 
    13FE81C59 cmp  qword ptr [rsp+30h],2 
    13FE81C5F je   testSwitch+87h (13FE81C87h) 
    13FE81C61 cmp  qword ptr [rsp+30h],3 
    13FE81C67 je   testSwitch+9Bh (13FE81C9Bh) 
    13FE81C69 cmp  qword ptr [rsp+30h],4 
    13FE81C6F je   testSwitch+0AFh (13FE81CAFh) 

    Một giải pháp dựa trên bảng nhảy hoàn toàn không sử dụng so sánh.

  3. Không đủ các nhánh để khiến trình biên dịch tạo bảng nhảy hoặc trình biên dịch của bạn đơn giản là không tạo ra chúng. Tôi không chắc chắn điều đó.

EDIT 2014 : Đã có một số cuộc thảo luận ở những nơi khác từ những người quen thuộc với trình tối ưu hóa LLVM nói rằng tối ưu hóa bảng nhảy có thể quan trọng trong nhiều tình huống; ví dụ trong trường hợp có một bảng liệt kê có nhiều giá trị và nhiều trường hợp chống lại các giá trị trong bảng liệt kê nói trên. Điều đó nói rằng, tôi đứng trước những gì tôi đã nói ở trên vào năm 2011 - tôi thường thấy mọi người nghĩ rằng "nếu tôi biến nó thành một công tắc, thì đó sẽ là cùng một lúc cho dù tôi có bao nhiêu trường hợp" - và điều đó hoàn toàn sai. Ngay cả với một bảng nhảy, bạn có được chi phí nhảy gián tiếp và bạn trả tiền cho các mục trong bảng cho từng trường hợp; và băng thông bộ nhớ là một thỏa thuận lớn trên phần cứng hiện đại.

Viết mã cho dễ đọc. Bất kỳ trình biên dịch nào có giá trị muối của nó sẽ thấy một if / khác nếu thang và chuyển đổi nó thành chuyển đổi tương đương hoặc ngược lại nếu nó sẽ nhanh hơn để làm như vậy.


3
+1 để thực sự trả lời câu hỏi và cho thông tin hữu ích. :-) Tuy nhiên, một câu hỏi: Theo những gì tôi hiểu, một bảng nhảy sử dụng các bước nhảy gián tiếp ; đúng không? Nếu vậy, điều đó thường chậm hơn do việc tìm nạp trước / đường ống khó khăn hơn?
dùng541686

1
@Mehrdad: Vâng, nó sử dụng các bước nhảy gián tiếp. Tuy nhiên, một bước nhảy gián tiếp (với gian hàng đường ống đi kèm) có thể ít hơn hàng trăm lần nhảy trực tiếp. :)
Billy ONeal

1
@Mehrdad: Không, thật không may. :( Tôi rất vui vì tôi ở trong trại của những người luôn nghĩ rằng IF dễ đọc hơn! :)
Billy ONeal

1
Vài câu đố - "[công tắc] chỉ hoạt động khi đầu vào có thể bị ràng buộc theo cách nào đó" "cần chèn một số dạng nếu kiểm tra xung quanh bảng để đảm bảo rằng đầu vào hợp lệ trong bảng. Lưu ý rằng nó chỉ hoạt động trong cụ thể trong trường hợp đầu vào là một chuỗi các số liên tiếp. ": hoàn toàn có thể có một bảng dân cư thưa thớt, nơi con trỏ tiềm năng được đọc và chỉ khi non-NULL là một bước nhảy được thực hiện, nếu không thì trường hợp mặc định nếu có được nhảy tới, sau đó là switchlối thoát hiểm Soren đã nói một số điều khác mà tôi muốn nói sau khi đọc câu trả lời này.
Tony Delroy

2
"Bất kỳ trình biên dịch nào xứng đáng với muối của nó sẽ thấy một if / khác nếu thang và biến nó thành công tắc tương đương hoặc ngược lại" - có hỗ trợ nào cho khẳng định này không? một trình biên dịch có thể cho rằng thứ tự các ifmệnh đề của bạn đã được điều chỉnh bằng tay để phù hợp với tần suất và nhu cầu hiệu suất tương đối, trong đó theo switchtruyền thống được xem như một lời mời mở để tối ưu hóa tuy nhiên trình biên dịch chọn. Điểm tốt đang nhảy qua switch:-). Kích thước mã phụ thuộc vào trường hợp / phạm vi - có thể tốt hơn. Cuối cùng, một số enum, trường bit và charkịch bản vốn đã hợp lệ / giới hạn và miễn phí.
Tony Delroy

47

Cho câu hỏi của bạn:

1. Bảng nhảy cơ bản trông như thế nào, trong x86 hoặc x64?

Bảng nhảy là địa chỉ bộ nhớ giữ con trỏ tới các nhãn trong một cái gì đó giống như cấu trúc mảng. ví dụ sau đây sẽ giúp bạn hiểu cách các bảng nhảy được đặt ra

00B14538  D8 09 AB 00 D8 09 AB 00 D8 09 AB 00 D8 09 AB 00  Ø.«.Ø.«.Ø.«.Ø.«.
00B14548  D8 09 AB 00 D8 09 AB 00 D8 09 AB 00 00 00 00 00  Ø.«.Ø.«.Ø.«.....
00B14558  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00B14568  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

nhập mô tả hình ảnh ở đây

Trong đó 00B14538 là con trỏ tới bảng Jump và giá trị như D8 09 AB 00 đại diện cho con trỏ nhãn.

2.Là mã này bằng cách sử dụng một bảng nhảy? Không trong trường hợp này.

3.Tại sao không có sự khác biệt hiệu suất trong ví dụ này?

Không có sự khác biệt về hiệu suất vì hướng dẫn cho cả hai trường hợp trông giống nhau, không có bảng nhảy.

4. Có bất kỳ tình huống trong đó có một sự khác biệt hiệu suất đáng kể?

Nếu bạn có chuỗi kiểm tra if rất dài , trong trường hợp đó, sử dụng bảng nhảy sẽ cải thiện hiệu suất (hướng dẫn phân nhánh / jmp rất tốn kém nếu chúng không dự đoán gần như hoàn hảo) nhưng đi kèm với chi phí bộ nhớ.

Mã cho tất cả các hướng dẫn so sánh cũng có một số kích thước, vì vậy, đặc biệt với các con trỏ hoặc bit 32 bit, một tra cứu bảng nhảy đơn có thể không tốn nhiều kích thước hơn trong một tệp thực thi.

Kết luận: Trình biên dịch đủ thông minh xử lý trường hợp đó và tạo hướng dẫn phù hợp :)


(chỉnh sửa: nvm, câu trả lời của Billy đã có những gì tôi đã gợi ý tôi đoán đây là một suplement tốt đẹp..) Nó sẽ là tốt để bao gồm gcc -Sđầu ra: một chuỗi các .long L1/ .long L2bảng mục có nhiều ý nghĩa hơn là một hexdump, và hữu ích hơn cho ai đó mà muốn tìm hiểu làm thế nào để xem trình biên dịch. (Mặc dù tôi đoán bạn chỉ cần nhìn vào mã chuyển đổi để xem đó là jmp gián tiếp hay một bó jcc).
Peter Cordes

31

Trình biên dịch có thể tự do biên dịch câu lệnh switch dưới dạng mã tương đương với câu lệnh if hoặc để tạo bảng nhảy. Nó có thể sẽ chọn cái khác dựa trên cái gì sẽ thực thi nhanh nhất hoặc tạo mã nhỏ nhất một chút tùy thuộc vào những gì bạn đã chỉ định trong tùy chọn trình biên dịch - vì vậy, trường hợp xấu nhất sẽ có cùng tốc độ như câu lệnh if

Tôi tin tưởng trình biên dịch sẽ đưa ra lựa chọn tốt nhất và tập trung vào những gì làm cho mã dễ đọc nhất.

Nếu số lượng các trường hợp trở nên rất lớn, một bảng nhảy sẽ nhanh hơn nhiều so với một loạt nếu. Tuy nhiên, nếu các bước giữa các giá trị là rất lớn, thì bảng nhảy có thể trở nên lớn và trình biên dịch có thể chọn không tạo một giá trị.


13
Tôi không nghĩ điều này trả lời câu hỏi của OP. Ở tất cả.
Billy ONeal

5
@Soren: Nếu đó là "câu hỏi cơ bản" thì tôi sẽ không bận tâm với 179 dòng khác trong câu hỏi, nó chỉ là 1 dòng. :-)
dùng541686

8
@Soren: Tôi thấy ít nhất 3 câu hỏi phụ được đánh số là một phần của câu hỏi của OP. Bạn chỉ đơn thuần đưa ra câu trả lời chính xác tương tự cho tất cả các câu hỏi "hiệu suất" - cụ thể là, bạn phải đo trước. Hãy xem xét rằng có lẽ Mehrdad đã đo lường và đã tách đoạn mã này thành một điểm nóng. Trong những trường hợp như vậy, câu trả lời của bạn tệ hơn vô giá trị, đó là tiếng ồn.
Billy ONeal

2
Có một đường mờ giữa bảng nhảy và cái gì không phụ thuộc vào định nghĩa của bạn. Tôi đã cung cấp thông tin về câu hỏi phụ phần 3.
Soren

2
@wnoise: Nếu đó là câu trả lời đúng duy nhất thì sẽ không bao giờ có lý do để hỏi bất kỳ câu hỏi về hiệu suất. Tuy nhiên, có một số người trong chúng ta trong thế giới thực đã đo phần mềm của chúng tôi và đôi khi chúng tôi không biết làm thế nào để tạo ra một đoạn mã nhanh hơn một khi đã được đo. Rõ ràng là Mehrdad đã đặt một số nỗ lực vào câu hỏi này trước khi hỏi nó; và tôi nghĩ rằng những câu hỏi cụ thể của anh ấy nhiều hơn câu trả lời.
Billy ONeal

13

Làm thế nào để bạn biết máy tính của bạn không thực hiện một số tác vụ không liên quan đến kiểm tra trong vòng kiểm tra chuyển đổi và thực hiện ít tác vụ hơn trong vòng kiểm tra if? Kết quả kiểm tra của bạn không hiển thị bất cứ điều gì như:

  1. sự khác biệt là rất nhỏ
  2. chỉ có một kết quả, không phải là một loạt kết quả
  3. có quá ít trường hợp

Kết quả của tôi:

Tôi đã thêm:

printf("counter: %u\n", counter);

đến cuối cùng để nó không tối ưu hóa vòng lặp vì bộ đếm không bao giờ được sử dụng trong ví dụ của bạn, vậy tại sao trình biên dịch sẽ thực hiện vòng lặp? Ngay lập tức, công tắc luôn giành chiến thắng ngay cả với điểm chuẩn vi mô như vậy.

Vấn đề khác với mã của bạn là:

switch (counter % 4 + 1)

trong vòng lặp chuyển đổi của bạn, so với

const size_t c = counter % 4 + 1; 

trong vòng lặp if của bạn. Sự khác biệt rất lớn nếu bạn khắc phục điều đó. Tôi tin rằng việc đặt câu lệnh bên trong câu lệnh switch sẽ kích thích trình biên dịch gửi giá trị trực tiếp vào các thanh ghi CPU thay vì đặt nó lên ngăn xếp trước. Do đó, điều này có lợi cho tuyên bố chuyển đổi và không phải là một thử nghiệm cân bằng.

Oh và tôi nghĩ bạn cũng nên thiết lập lại bộ đếm giữa các bài kiểm tra. Trên thực tế, có lẽ bạn nên sử dụng một số loại số ngẫu nhiên thay vì +1, +2, +3, v.v., vì nó có thể sẽ tối ưu hóa một cái gì đó ở đó. Theo số ngẫu nhiên, tôi có nghĩa là một số dựa trên thời gian hiện tại, ví dụ. Mặt khác, trình biên dịch có thể biến cả hai hàm của bạn thành một phép toán dài và thậm chí không bận tâm với bất kỳ vòng lặp nào.

Tôi đã sửa đổi mã của Ryan vừa đủ để đảm bảo trình biên dịch không thể tìm ra mọi thứ trước khi mã chạy:

#include <stdlib.h>
#include <stdio.h>
#include <time.h>

#define MAX_COUNT (1 << 26)
size_t counter = 0;

long long testSwitch()
{
    clock_t start = clock();
    size_t i;
    for (i = 0; i < MAX_COUNT; i++)
    {
        const size_t c = rand() % 20 + 1;

        switch (c)
        {
                case 1: counter += 20; break;
                case 2: counter += 33; break;
                case 3: counter += 62; break;
                case 4: counter += 15; break;
                case 5: counter += 416; break;
                case 6: counter += 3545; break;
                case 7: counter += 23; break;
                case 8: counter += 81; break;
                case 9: counter += 256; break;
                case 10: counter += 15865; break;
                case 11: counter += 3234; break;
                case 12: counter += 22345; break;
                case 13: counter += 1242; break;
                case 14: counter += 12341; break;
                case 15: counter += 41; break;
                case 16: counter += 34321; break;
                case 17: counter += 232; break;
                case 18: counter += 144231; break;
                case 19: counter += 32; break;
                case 20: counter += 1231; break;
        }
    }
    return 1000 * (long long)(clock() - start) / CLOCKS_PER_SEC;
}

long long testIf()
{
    clock_t start = clock();
    size_t i;
    for (i = 0; i < MAX_COUNT; i++)
    {
        const size_t c = rand() % 20 + 1;
        if (c == 1) { counter += 20; }
        else if (c == 2) { counter += 33; }
        else if (c == 3) { counter += 62; }
        else if (c == 4) { counter += 15; }
        else if (c == 5) { counter += 416; }
        else if (c == 6) { counter += 3545; }
        else if (c == 7) { counter += 23; }
        else if (c == 8) { counter += 81; }
        else if (c == 9) { counter += 256; }
        else if (c == 10) { counter += 15865; }
        else if (c == 11) { counter += 3234; }
        else if (c == 12) { counter += 22345; }
        else if (c == 13) { counter += 1242; }
        else if (c == 14) { counter += 12341; }
        else if (c == 15) { counter += 41; }
        else if (c == 16) { counter += 34321; }
        else if (c == 17) { counter += 232; }
        else if (c == 18) { counter += 144231; }
        else if (c == 19) { counter += 32; }
        else if (c == 20) { counter += 1231; }
    }
    return 1000 * (long long)(clock() - start) / CLOCKS_PER_SEC;
}

int main()
{
    srand(time(NULL));
    printf("Starting...\n");
    printf("Switch statement: %lld ms\n", testSwitch()); fflush(stdout);
    printf("counter: %d\n", counter);
    counter = 0;
    srand(time(NULL));
    printf("If     statement: %lld ms\n", testIf()); fflush(stdout);
    printf("counter: %d\n", counter);
} 

chuyển đổi: 3740
nếu: 3980

(kết quả tương tự qua nhiều lần thử)

Tôi cũng giảm số trường hợp / if xuống còn 5 và chức năng chuyển đổi vẫn thắng.


Idk, tôi không thể chứng minh điều đó; Bạn có nhận được kết quả khác nhau?
dùng541686

+1: Điểm chuẩn rất khó và bạn thực sự không thể rút ra bất kỳ kết luận nào từ chênh lệch thời gian nhỏ trên một lần chạy trên máy tính bình thường. Bạn có thể thử chạy một số lượng lớn các bài kiểm tra và thực hiện một số thống kê về kết quả. Hoặc đếm chu kỳ bộ xử lý khi thực hiện có kiểm soát trong trình giả lập.
Thomas Padron-McCarthy

Er, nơi chính xác bạn đã thêm printtuyên bố? Tôi đã thêm nó vào cuối của toàn bộ chương trình và thấy không có sự khác biệt. Tôi cũng không hiểu "vấn đề" với người kia là gì ... tâm trí giải thích "sự khác biệt rất lớn" là gì?
dùng541686

1
@BobTurbo: 45983493 là hơn 12 giờ. Đó có phải là một lỗi đánh máy?
Gus

1
thật tuyệt, giờ tôi phải đi làm lại :)
BobTurbo

7

Một trình biên dịch tối ưu hóa tốt như MSVC có thể tạo ra:

  1. một bàn nhảy đơn giản nếu các trường hợp được sắp xếp trong một phạm vi dài đẹp
  2. một bàn nhảy thưa thớt (hai cấp) nếu có nhiều khoảng trống
  3. một loạt các if nếu số lượng các trường hợp nhỏ hoặc các giá trị không gần nhau
  4. một sự kết hợp ở trên nếu các trường hợp đại diện cho một số nhóm phạm vi khoảng cách gần nhau.

Nói tóm lại, nếu công tắc có vẻ chậm hơn một loạt if, trình biên dịch có thể chỉ chuyển đổi nó thành một. Và nó có thể không chỉ là một chuỗi so sánh cho từng trường hợp, mà là một cây tìm kiếm nhị phân. Xem ở đây cho một ví dụ.


Trên thực tế, một trình biên dịch cũng có thể thay thế nó bằng hàm băm và nhảy, hoạt động tốt hơn giải pháp hai cấp thưa thớt mà bạn đề xuất.
Alice

5

Tôi sẽ trả lời 2) và đưa ra một số nhận xét chung. 2) Không, không có bảng nhảy trong mã lắp ráp bạn đã đăng. Bảng nhảy là một bảng các điểm đến nhảy và một hoặc hai hướng dẫn để nhảy trực tiếp đến một vị trí được lập chỉ mục từ bảng. Một bảng nhảy sẽ có ý nghĩa hơn khi có nhiều điểm chuyển đổi có thể. Có thể trình tối ưu hóa biết rằng đơn giản nếu logic khác nhanh hơn trừ khi số lượng đích lớn hơn một số ngưỡng. Hãy thử lại ví dụ của bạn với 20 khả năng thay vì 4.


+1 cảm ơn vì câu trả lời cho # 2! :) (Btw, đây là kết quả có nhiều khả năng hơn.)
user541686

4

Tôi đã bị thu hút và xem xét những gì tôi có thể thay đổi về ví dụ của bạn để khiến nó chạy câu lệnh chuyển đổi nhanh hơn.

Nếu bạn nhận được 40 câu lệnh if và thêm trường hợp 0, thì khối if sẽ chạy chậm hơn câu lệnh chuyển đổi tương đương. Tôi có kết quả ở đây: https://www.ideone.com/KZeCz .

Hiệu quả của việc loại bỏ trường hợp 0 ​​có thể được nhìn thấy ở đây: https://www.ideone.com/LFnrX .


1
Liên kết của bạn đã bị hỏng.
TS

4

Dưới đây là một số kết quả từ điểm chuẩn (hiện tại khó tìm) của băng ghế dự bị ++:

Test Name:   F000003                         Class Name:  Style
CPU Time:       0.781  nanoseconds           plus or minus     0.0715
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 2-way if/else if statement
 compare this test with F000004

Test Name:   F000004                         Class Name:  Style
CPU Time:        1.53  nanoseconds           plus or minus     0.0767
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 2-way switch statement
 compare this test with F000003

Test Name:   F000005                         Class Name:  Style
CPU Time:        7.70  nanoseconds           plus or minus      0.385
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way if/else if statement
 compare this test with F000006

Test Name:   F000006                         Class Name:  Style
CPU Time:        2.00  nanoseconds           plus or minus     0.0999
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way switch statement
 compare this test with F000005

Test Name:   F000007                         Class Name:  Style
CPU Time:        3.41  nanoseconds           plus or minus      0.171
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way sparse switch statement
 compare this test with F000005 and F000006

Những gì chúng ta có thể thấy từ đây là (trên máy này, với trình biên dịch này - VC ++ 9.0 x64), mỗi ifthử nghiệm mất khoảng 0,7 nano giây. Khi số lượng bài kiểm tra tăng lên, quy mô thời gian gần như hoàn hảo tuyến tính.

Với câu lệnh chuyển đổi, hầu như không có sự khác biệt về tốc độ giữa thử nghiệm 2 chiều và thử nghiệm 10 chiều, miễn là các giá trị này dày đặc. Thử nghiệm 10 chiều với các giá trị thưa thớt mất khoảng 1,6 lần thời gian so với thử nghiệm 10 chiều với các giá trị dày đặc - nhưng ngay cả với các giá trị thưa thớt, vẫn tốt hơn gấp đôi tốc độ của 10 chiều if/ else if.

Điểm mấu chốt: chỉ sử dụng thử nghiệm 4 chiều sẽ không thực sự cho bạn thấy nhiều về hiệu suất của switchvs if/ else. Nếu bạn nhìn vào các số từ mã này, thật dễ dàng để nội suy thực tế rằng đối với thử nghiệm 4 chiều, chúng tôi hy vọng cả hai sẽ tạo ra kết quả khá giống nhau (~ 2,8 nano giây cho if/ else, ~ 2.0 cho switch).


1
Thật khó để biết phải làm gì nếu chúng ta không biết liệu thử nghiệm có cố tình tìm kiếm một giá trị không khớp hoặc chỉ khớp ở cuối chuỗi if/ elseso với phân tán chúng, v.v. Không thể tìm thấy các bench++nguồn sau 10 phút googling.
Tony Delroy

3

Lưu ý rằng khi công tắc KHÔNG được biên dịch thành bảng nhảy, bạn có thể thường xuyên viết nếu hiệu quả hơn công tắc ...

(1) nếu các trường hợp có thứ tự, thay vì thử nghiệm trường hợp xấu nhất cho tất cả N, bạn có thể viết if của mình để kiểm tra nếu ở nửa trên hoặc dưới, sau đó trong mỗi nửa, kiểu tìm kiếm nhị phân ... dẫn đến trường hợp xấu nhất là logN chứ không phải N

(2) nếu một số trường hợp / nhóm nhất định thường xuyên hơn nhiều so với các trường hợp khác, thì việc thiết kế if của bạn để cách ly những trường hợp đó trước tiên có thể tăng tốc thời gian trung bình thông qua


Điều này là không đúng sự thật; trình biên dịch có nhiều khả năng tạo ra CẢ HAI tối ưu hóa này.
Alice

1
Alice, làm thế nào một trình biên dịch được cho là biết trường hợp nào sẽ xảy ra phổ biến hơn các trường hợp khác trong khối lượng công việc dự kiến ​​của bạn? (A: Không thể biết được, vì vậy nó không thể thực hiện tối ưu hóa như vậy.)
Brian Kennedy

(1) có thể được thực hiện dễ dàng và được thực hiện trong một số trình biên dịch, chỉ bằng cách thực hiện tìm kiếm nhị phân. (2) có thể được dự đoán theo nhiều cách khác nhau, hoặc được chỉ định cho trình biên dịch. Bạn đã bao giờ sử dụng "có khả năng" hoặc "không thể" của GCC chưa?
Alice

Và một số trình biên dịch cho phép chạy chương trình trong chế độ thu thập số liệu thống kê và sau đó tối ưu hóa từ thông tin đó.
Phil1970

2

Không có những thứ này nếu sau đó nhảy khác nếu nhảy khác ... Một bảng nhảy sẽ có một bảng địa chỉ hoặc sử dụng hàm băm hoặc đại loại như thế.

Nhanh hơn hay chậm hơn là chủ quan. Ví dụ, bạn có thể có trường hợp 1 là điều cuối cùng thay vì lần đầu tiên và nếu chương trình thử nghiệm hoặc chương trình thế giới thực của bạn sử dụng trường hợp 1 thì hầu hết thời gian mã sẽ chậm hơn khi thực hiện. Vì vậy, chỉ cần sắp xếp lại danh sách trường hợp, tùy thuộc vào việc thực hiện, có thể tạo ra một sự khác biệt lớn.

Nếu bạn đã sử dụng các trường hợp 0-3 thay vì 1-4, trình biên dịch có thể đã sử dụng bảng nhảy, trình biên dịch sẽ tìm ra cách loại bỏ +1 của bạn. Có lẽ đó là số lượng nhỏ của các mặt hàng. Nếu bạn đã làm cho nó 0 - 15 hoặc 0 - 31, ví dụ như nó có thể đã thực hiện nó với một bảng hoặc sử dụng một số phím tắt khác. Trình biên dịch có thể tự do lựa chọn cách nó thực hiện mọi thứ miễn là nó đáp ứng chức năng của mã nguồn. Và điều này nhận được sự khác biệt về trình biên dịch và sự khác biệt về phiên bản và sự khác biệt tối ưu hóa. Nếu bạn muốn có một bảng nhảy, hãy tạo một bảng nhảy, nếu bạn muốn một cây if-then-other tạo một cây if-then-other. Nếu bạn muốn trình biên dịch quyết định, hãy sử dụng câu lệnh switch / case.


2

Không chắc chắn tại sao một người nhanh hơn và một người chậm hơn, mặc dù.

Điều đó thực sự không quá khó để giải thích ... Nếu bạn nhớ rằng các nhánh bị dự đoán sai sẽ đắt hơn hàng chục đến hàng trăm lần so với các nhánh được dự đoán chính xác.

Trong % 20phiên bản, trường hợp đầu tiên / nếu luôn luôn là trường hợp xảy ra. Các CPU hiện đại "học" những nhánh nào thường được lấy và không phải, vì vậy chúng có thể dễ dàng dự đoán cách nhánh này sẽ hoạt động trên hầu hết các lần lặp của vòng lặp. Điều đó giải thích tại sao phiên bản "nếu" bay; nó không bao giờ phải thực hiện bất cứ điều gì qua bài kiểm tra đầu tiên và nó (chính xác) dự đoán kết quả của bài kiểm tra đó trong hầu hết các lần lặp. Rõ ràng "công tắc" được triển khai hơi khác một chút - thậm chí có thể là một bảng nhảy, có thể chậm nhờ vào nhánh được tính toán.

Trong % 21phiên bản, các nhánh về cơ bản là ngẫu nhiên. Vì vậy, không chỉ nhiều người trong số họ thực hiện mỗi lần lặp, CPU không thể đoán được họ sẽ đi theo con đường nào. Đây là trường hợp một bảng nhảy (hoặc tối ưu hóa "chuyển đổi" khác) có thể giúp đỡ.

Rất khó để dự đoán một đoạn mã sẽ hoạt động như thế nào với trình biên dịch và CPU hiện đại, và nó trở nên khó hơn với mọi thế hệ. Lời khuyên tốt nhất là "thậm chí không thèm thử, luôn luôn hồ sơ". Lời khuyên đó trở nên tốt hơn - và tập hợp những người có thể bỏ qua nó thành công trở nên nhỏ hơn - mỗi năm.

Tất cả những điều đó là để nói rằng lời giải thích của tôi ở trên phần lớn là một phỏng đoán. :-)


2
Tôi không thấy nơi nào có thể chậm hơn hàng trăm lần. Trường hợp xấu nhất của một nhánh bị dự đoán sai là một gian hàng đường ống, sẽ chậm hơn ~ 20 lần trên hầu hết các CPU hiện đại. Không phải hàng trăm lần. (Được rồi, nếu bạn đang sử dụng chip NetBurst cũ, nó có thể chậm hơn 35 lần ...)
Billy ONeal

@Billy: OK, vì vậy tôi đang nhìn về phía trước một chút. Trên bộ xử lý Sandy Bridge , "Mỗi nhánh bị dự đoán sai sẽ làm hỏng toàn bộ đường ống, làm mất công việc lên tới hàng trăm hướng dẫn trong chuyến bay". Các đường ống thực sự trở nên sâu hơn với mọi thế hệ, nói chung ...
Nemo

1
Không đúng. P4 (NetBurst) có 31 giai đoạn đường ống; Sandy Bridge có ít giai đoạn hơn đáng kể. Tôi nghĩ rằng "mất công việc của 100 hướng dẫn hoặc hơn" theo giả định rằng bộ đệm hướng dẫn bị vô hiệu. Đối với một bước nhảy gián tiếp chung thực tế xảy ra, nhưng đối với một thứ như bảng nhảy, có khả năng mục tiêu của bước nhảy gián tiếp nằm ở đâu đó trong bộ đệm lệnh.
Billy ONeal

@Billy: Tôi không nghĩ chúng tôi không đồng ý. Tuyên bố của tôi là: "Các chi nhánh dự đoán đắt hơn hàng chục đến hàng trăm lần so với các chi nhánh được dự đoán chính xác". Một sự phóng đại nhẹ, có lẽ ... Nhưng có nhiều điều đang diễn ra hơn là chỉ các lần truy cập trong I-cache và độ sâu đường ống thực thi; từ những gì tôi đã đọc, hàng đợi để giải mã một mình là ~ 20 hướng dẫn.
Nemo

Nếu phần cứng dự đoán nhánh đánh giá sai đường dẫn thực thi, các uops từ đường dẫn không chính xác trong đường dẫn lệnh sẽ bị xóa đơn giản tại nơi chúng không có, mà không bị đình trệ thực thi. Tôi không biết làm thế nào điều này là có thể (hoặc liệu tôi có hiểu sai về nó không), nhưng rõ ràng không có quầy hàng đường ống với các chi nhánh bị dự đoán sai ở Nehalem? (Sau đó, một lần nữa, tôi không có i7; tôi có i5, vì vậy điều này không áp dụng cho trường hợp của tôi.)
user541686

1

Không ai. Trong hầu hết các trường hợp cụ thể khi bạn đi vào trình biên dịch chương trình và thực hiện các phép đo thực tế về hiệu suất, câu hỏi của bạn chỉ đơn giản là sai. Đối với ví dụ đã cho, suy nghĩ của bạn quá ngắn

counter += (4 - counter % 4);

có vẻ như tôi là biểu thức gia tăng chính xác mà bạn nên sử dụng.

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.