Tại sao một công tắc không được tối ưu hóa giống như cách xâu chuỗi nếu khác trong c / c ++?


39

Việc triển khai hình vuông sau đây tạo ra một loạt các câu lệnh cmp / je như tôi mong đợi về một câu lệnh if bị xiềng xích:

int square(int num) {
    if (num == 0){
        return 0;
    } else if (num == 1){
        return 1;
    } else if (num == 2){
        return 4;
    } else if (num == 3){
        return 9;
    } else if (num == 4){
        return 16;
    } else if (num == 5){
        return 25;
    } else if (num == 6){
        return 36;
    } else if (num == 7){
        return 49;
    } else {
        return num * num;
    }
}

Và sau đây tạo ra một bảng dữ liệu để trả về:

int square_2(int num) {
    switch (num){
        case 0: return 0;
        case 1: return 1;
        case 2: return 4;
        case 3: return 9;
        case 4: return 16;
        case 5: return 25;
        case 6: return 36;
        case 7: return 49;
        default: return num * num;
    }
}

Tại sao gcc không thể tối ưu hóa cái đầu vào cái dưới cùng?

Phân tích để tham khảo: https://godbolt.org/z/UP_igi

EDIT: thật thú vị, MSVC tạo bảng nhảy thay vì bảng dữ liệu cho trường hợp chuyển đổi. Và đáng ngạc nhiên, clang tối ưu hóa chúng cho cùng một kết quả.


3
"Hành vi không xác định" nghĩa là gì? Miễn là hành vi có thể quan sát là như nhau, trình biên dịch có thể tạo ra bất kỳ mã lắp ráp / mã máy nào mà nó muốn
bolov

2
@ user207421 bỏ qua returns; các trường hợp không có breaks, do đó, chuyển đổi cũng có một thứ tự thực hiện cụ thể. Chuỗi if / other có trả về trong mỗi nhánh, ngữ nghĩa trong trường hợp này là tương đương. Việc tối ưu hóa là không thể . Là một ví dụ mẫu, icc không tối ưu hóa bất kỳ chức năng nào.
user1810087

9
Có lẽ câu trả lời đơn giản nhất ... gcc chỉ là không thể nhìn thấy cấu trúc này và tối ưu hóa nó (chưa).
user1810087

3
Tôi đồng ý với @ user1810087. Bạn chỉ cần tìm ranh giới hiện tại của quá trình sàng lọc trình biên dịch. Một trường hợp con phụ hiện không được công nhận là tối ưu hóa (bởi một số trình biên dịch). Trong thực tế, không phải mọi chuỗi-if khác đều có thể được tối ưu hóa theo cách đó, mà chỉ có tập hợp con trong đó biến SAME được kiểm tra theo các giá trị không đổi.
Roberto Caboni

1
If-other có thứ tự thực hiện khác nhau, từ trên xuống dưới. Tuy nhiên, thay thế mã bằng chỉ khi các câu lệnh không cải thiện mã máy. Mặt khác, công tắc không có thứ tự thực hiện được xác định trước và về cơ bản chỉ là một bảng nhảy goto được tôn vinh. Điều đó đang được nói, một trình biên dịch được phép lý do về hành vi có thể quan sát được ở đây, vì vậy việc tối ưu hóa kém của phiên bản if-other là khá đáng thất vọng.
Lundin

Câu trả lời:


29

Mã được tạo cho switch-case quy ước sử dụng bảng nhảy. Trong trường hợp này, việc trả lại trực tiếp thông qua bảng tra cứu dường như là một sự tối ưu hóa sử dụng thực tế là mọi trường hợp ở đây đều liên quan đến việc trả lại. Mặc dù tiêu chuẩn không đảm bảo cho hiệu ứng đó, tôi sẽ ngạc nhiên nếu trình biên dịch tạo ra một loạt so sánh thay vì bảng nhảy cho trường hợp chuyển đổi thông thường.

Bây giờ đến if-else, nó là hoàn toàn ngược lại. Trong khi switch-casethực thi trong thời gian không đổi, bất kể số lượng nhánh, if-elseđược tối ưu hóa cho số lượng nhánh nhỏ hơn. Ở đây, bạn sẽ mong đợi trình biên dịch về cơ bản tạo ra một loạt các so sánh theo thứ tự mà bạn đã viết chúng.

Vì vậy, nếu tôi đã sử dụng if-elsevì tôi hy vọng hầu hết các cuộc gọi square()sẽ dành cho 0hoặc 1hiếm khi cho các giá trị khác, thì 'tối ưu hóa' điều này để tra cứu bảng thực sự có thể khiến mã của tôi chạy chậm hơn tôi mong đợi, đánh bại mục đích của tôi vì sử dụng ifthay thế của a switch. Vì vậy, mặc dù còn nhiều tranh cãi, tôi cảm thấy GCC đang làm điều đúng đắn và tiếng kêu đang được tích cực quá mức trong việc tối ưu hóa.

Ai đó, trong các bình luận, đã chia sẻ một liên kết trong đó clang thực hiện tối ưu hóa này và cũng tạo mã dựa trên bảng tra cứu if-else. Một cái gì đó đáng chú ý xảy ra khi chúng ta giảm số lượng các trường hợp xuống chỉ còn hai (và một mặc định) với tiếng kêu. Nó một lần nữa tạo mã giống hệt nhau cho cả if và switch, nhưng lần này, chuyển sang so sánh và di chuyển thay vì cách tiếp cận bảng tra cứu, cho cả hai. Điều này có nghĩa là ngay cả những người thích chuyển đổi cũng biết rằng mẫu 'nếu' là tối ưu hơn khi số lượng các trường hợp là nhỏ!

Tóm lại, một chuỗi so sánh if-elsevà bảng nhảy switch-caselà mẫu chuẩn mà trình biên dịch có xu hướng tuân theo và các nhà phát triển có xu hướng mong đợi khi họ viết mã. Tuy nhiên, đối với một số trường hợp đặc biệt, một số trình biên dịch có thể chọn phá vỡ mẫu này khi chúng cảm thấy nó cung cấp tối ưu hóa tốt hơn. Các trình biên dịch khác có thể chỉ chọn bám vào mẫu dù thế nào, ngay cả khi rõ ràng là tối ưu phụ, tin tưởng vào nhà phát triển để biết anh ta muốn gì. Cả hai đều là phương pháp hợp lệ với những ưu điểm và nhược điểm riêng.


2
Vâng, tối ưu hóa là một con dao nhiều lưỡi: Những gì họ viết, những gì họ muốn, những gì họ nhận được, và chúng ta nguyền rủa ai vì điều đó.
Ded repeatator

1
"... Sau đó, 'tối ưu hóa' điều này để tra cứu bảng thực sự sẽ khiến mã của tôi chạy chậm hơn tôi mong đợi ..." Bạn có thể đưa ra lời biện minh cho việc này không? Tại sao một bảng nhảy bao giờ chậm hơn hai nhánh có điều kiện có thể (để kiểm tra đầu vào so với 01)?
Cody Grey

@CodyGray Tôi phải thú nhận rằng tôi đã không đạt đến mức độ của các chu kỳ đếm - Tôi chỉ cảm nhận được rằng tải từ bộ nhớ qua một con trỏ có thể mất nhiều chu kỳ hơn so với so sánh và nhảy, nhưng tôi có thể sai. Tuy nhiên, tôi hy vọng bạn đồng ý với tôi rằng ngay cả trong trường hợp này, ít nhất là cho '0', ifrõ ràng là nhanh hơn? Bây giờ, đây là một ví dụ về nền tảng mà cả 0 và 1 sẽ nhanh hơn khi sử dụng ifso với khi sử dụng switch: godbolt.org/z/wcJhvS (Lưu ý rằng cũng có nhiều tối ưu hóa khác đang chơi ở đây)
th33lf

1
Chà, đếm chu kỳ không hoạt động trên các kiến ​​trúc superscalar hiện đại. :-) Tải từ bộ nhớ sẽ không chậm hơn các nhánh bị dự đoán sai, vì vậy câu hỏi đặt ra là khả năng dự đoán của nhánh là bao nhiêu? Câu hỏi đó áp dụng cho tất cả các cách thức của các nhánh có điều kiện, cho dù được tạo bởi các ifcâu lệnh rõ ràng hoặc tự động bởi trình biên dịch. Tôi không phải là chuyên gia ARM, vì vậy tôi không thực sự chắc chắn nếu tuyên bố mà bạn đưa ra liên quan đến switchviệc nhanh hơn iflà đúng. Nó sẽ phụ thuộc vào hình phạt cho các chi nhánh mispredicted, và đó thực sự sẽ phụ thuộc vào đó ARM.
Cody Grey

0

Một lý do có thể là nếu các giá trị thấp numcó nhiều khả năng, ví dụ luôn là 0, mã được tạo cho mã đầu tiên có thể nhanh hơn. Mã được tạo cho chuyển đổi mất thời gian bằng nhau cho tất cả các giá trị.

So sánh các trường hợp tốt nhất, theo bảng này . Xem câu trả lời này cho lời giải thích của bảng.

Nếu num == 0, với "nếu" bạn có xor, test, je (có nhảy), ret. Độ trễ: nhảy 1 + 1 +. Tuy nhiên, xor và test là độc lập nên tốc độ thực hiện thực tế sẽ nhanh hơn chu kỳ 1 + 1.

Nếu num < 7, đối với "chuyển đổi", bạn có Mov, cmp, ja (không nhảy), Mov, ret. Độ trễ: 2 + 1 + không nhảy + 2.

Một lệnh nhảy không dẫn đến nhảy sẽ nhanh hơn một lệnh dẫn đến nhảy. Tuy nhiên, bảng không xác định độ trễ cho một bước nhảy, vì vậy tôi không rõ cái nào tốt hơn. Có thể là cái cuối cùng luôn tốt hơn và GCC đơn giản là không thể tối ưu hóa nó.


1
Hmm, lý thuyết thú vị, nhưng đối với ifs vs switch bạn có: xor, test, jmp vs Mov, cmp jmp. Ba hướng dẫn mỗi lần cuối cùng là một bước nhảy. Có vẻ như bằng nhau trong trường hợp tốt nhất, không?
chacham15

3
"Một lệnh nhảy không dẫn đến nhảy nhanh hơn một lệnh dẫn đến nhảy.". Đó là dự đoán chi nhánh quan trọng.
geza
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.