Mà, nếu có, trình biên dịch C ++ thực hiện tối ưu hóa đệ quy đuôi?


149

Dường như với tôi rằng nó sẽ hoạt động hoàn hảo để thực hiện tối ưu hóa đệ quy đuôi trong cả C và C ++, tuy nhiên trong khi gỡ lỗi tôi dường như không bao giờ thấy một ngăn xếp khung cho thấy tối ưu hóa này. Đó là loại tốt, bởi vì ngăn xếp cho tôi biết đệ quy sâu đến mức nào. Tuy nhiên, tối ưu hóa cũng sẽ tốt đẹp.

Có trình biên dịch C ++ nào thực hiện tối ưu hóa này không? Tại sao? Tại sao không?

Làm thế nào để tôi đi nói với trình biên dịch để làm điều đó?

  • Đối với MSVC: /O2hoặc/Ox
  • Đối với GCC: -O2hoặc-O3

Làm thế nào về việc kiểm tra nếu trình biên dịch đã làm điều này trong một trường hợp nhất định?

  • Đối với MSVC, cho phép đầu ra PDB có thể theo dõi mã, sau đó kiểm tra mã
  • Đối với GCC ..?

Tôi vẫn sẽ đưa ra gợi ý về cách xác định xem một chức năng nhất định có được tối ưu hóa như thế này bởi trình biên dịch hay không (mặc dù tôi thấy yên tâm rằng Konrad bảo tôi giả sử nó)

Luôn luôn có thể kiểm tra xem trình biên dịch có thực hiện việc này hay không bằng cách thực hiện đệ quy vô hạn và kiểm tra xem nó có dẫn đến một vòng lặp vô hạn hoặc tràn ngăn xếp không (tôi đã làm điều này với GCC và phát hiện ra -O2là đủ), nhưng tôi muốn có thể kiểm tra một chức năng nào đó mà tôi biết sẽ chấm dứt bằng mọi cách. Tôi muốn có một cách dễ dàng để kiểm tra điều này :)


Sau một số thử nghiệm, tôi phát hiện ra rằng các tàu khu trục phá hỏng khả năng thực hiện tối ưu hóa này. Đôi khi có thể đáng để thay đổi phạm vi của các biến và thời gian nhất định để đảm bảo chúng đi ra khỏi phạm vi trước khi câu lệnh return bắt đầu.

Nếu bất kỳ hàm hủy nào cần được chạy sau lệnh gọi đuôi, thì việc tối ưu hóa cuộc gọi đuôi không thể được thực hiện.

Câu trả lời:


128

Tất cả các trình biên dịch chính hiện tại thực hiện tối ưu hóa cuộc gọi đuôi khá tốt (và đã thực hiện trong hơn một thập kỷ), ngay cả đối với các cuộc gọi đệ quy lẫn nhau như:

int bar(int, int);

int foo(int n, int acc) {
    return (n == 0) ? acc : bar(n - 1, acc + 2);
}

int bar(int n, int acc) {
    return (n == 0) ? acc : foo(n - 1, acc + 1);
}

Để trình biên dịch thực hiện tối ưu hóa rất đơn giản: Chỉ cần bật tối ưu hóa cho tốc độ:

  • Đối với MSVC, sử dụng /O2hoặc /Ox.
  • Đối với GCC, Clang và ICC, hãy sử dụng -O3

Một cách dễ dàng để kiểm tra xem trình biên dịch có thực hiện tối ưu hóa hay không là thực hiện một cuộc gọi có thể dẫn đến tràn ngăn xếp - hoặc nhìn vào đầu ra lắp ráp.

Là một ghi chú lịch sử thú vị, tối ưu hóa cuộc gọi đuôi cho C đã được thêm vào GCC trong quá trình làm luận văn bằng tốt nghiệp của Mark Probst. Luận án mô tả một số cảnh báo thú vị trong việc thực hiện. Nó đáng để đọc.


ICC sẽ làm như vậy, tôi tin. Theo hiểu biết tốt nhất của tôi, ICC tạo ra mã nhanh nhất trên thị trường.
Paul Nathan

35
@Paul Câu hỏi đặt ra là tốc độ của mã ICC gây ra bao nhiêu bởi tối ưu hóa thuật toán như tối ưu hóa cuộc gọi đuôi và mức độ gây ra bởi tối ưu hóa bộ đệm và vi cấu trúc mà chỉ Intel, với kiến ​​thức sâu sắc về bộ xử lý của họ, mới có thể làm được.
Tưởng tượng

6
gcccó nhiều tùy chọn hẹp hơn -foptimize-sibling-callsđể "tối ưu hóa các cuộc gọi đệ quy và nối đuôi". Tùy chọn này (theo gcc(1)trang hướng dẫn cho các phiên bản 4.4, 4.7 và 4.8 nhắm mục tiêu nền tảng khác nhau) được kích hoạt ở mức -O2, -O3, -Os.
FooF

Ngoài ra, chạy trong chế độ DEBUG mà không yêu cầu tối ưu hóa rõ ràng sẽ KHÔNG thực hiện bất kỳ tối ưu hóa nào cả. Bạn có thể kích hoạt PDB cho chế độ phát hành thực EXE và thử từng bước, nhưng lưu ý rằng việc gỡ lỗi trong chế độ Phát hành có các biến chứng - biến vô hình / tước, biến được hợp nhất, biến không thuộc phạm vi trong phạm vi không xác định / không mong muốn, các biến không bao giờ đi vào phạm vi và trở thành hằng số thực sự với các địa chỉ cấp độ ngăn xếp, và - cũng được hợp nhất hoặc thiếu các khung ngăn xếp. Thông thường các khung stack được hợp nhất có nghĩa là callee được nội tuyến và các khung bị thiếu / backmerged có thể gọi đuôi.
Петър Петров

21

gcc 4.3.2 hoàn toàn bổ sung chức năng này ( atoi()triển khai crappy / tầm thường ) vào main(). Mức độ tối ưu hóa là -O1. Tôi nhận thấy nếu tôi chơi xung quanh nó (thậm chí thay đổi nó từ staticsang extern, đệ quy đuôi biến mất khá nhanh, vì vậy tôi sẽ không phụ thuộc vào nó cho tính chính xác của chương trình.

#include <stdio.h>
static int atoi(const char *str, int n)
{
    if (str == 0 || *str == 0)
        return n;
    return atoi(str+1, n*10 + *str-'0');
}
int main(int argc, char **argv)
{
    for (int i = 1; i != argc; ++i)
        printf("%s -> %d\n", argv[i], atoi(argv[i], 0));
    return 0;
}

1
Bạn có thể kích hoạt tối ưu hóa thời gian liên kết và tôi đoán rằng ngay cả một externphương thức cũng có thể được nội tuyến.
Konrad Rudolph

5
Lạ thật. Tôi chỉ thử nghiệm gcc 4.2.3 (x86, Slackware 12.1) và gcc 4.6.2 (AMD64, Debian khò khè) và với-O1 đó là không có nội tuyếnkhông tối ưu hóa đuôi-đệ quy . Bạn phải sử dụng -O2cho điều đó (tốt, trong 4.2.x, hiện tại khá cổ xưa, nó vẫn sẽ không được nội tuyến). BTW Cũng đáng để thêm rằng gcc có thể tối ưu hóa đệ quy ngay cả khi nó không hoàn toàn là một cái đuôi (như tích lũy w / o giai thừa).
przemoc

16

Cũng như điều hiển nhiên (trình biên dịch không thực hiện loại tối ưu hóa này trừ khi bạn yêu cầu), có một sự phức tạp về tối ưu hóa cuộc gọi đuôi trong C ++: hàm hủy.

Đưa ra một cái gì đó như:

   int fn(int j, int i)
   {
      if (i <= 0) return j;
      Funky cls(j,i);
      return fn(j, i-1);
   }

Trình biên dịch không thể (nói chung) gọi đuôi tối ưu hóa điều này bởi vì nó cần gọi hàm hủy cls sau khi trả về cuộc gọi đệ quy.

Đôi khi trình biên dịch có thể thấy rằng hàm hủy không có tác dụng phụ có thể nhìn thấy bên ngoài (vì vậy nó có thể được thực hiện sớm), nhưng thường thì không thể.

Một hình thức đặc biệt phổ biến của điều này là nơi Funkythực sự là một std::vectorhoặc tương tự.


Không làm việc cho tôi. Các hệ thống cho tôi biết rằng phiếu bầu của tôi bị khóa cho đến khi câu trả lời được chỉnh sửa.
hmuelner

Chỉ cần chỉnh sửa câu trả lời (loại bỏ parantheses) và bây giờ tôi có thể hoàn tác downvote của mình.
hmuelner

11

Hầu hết các trình biên dịch không thực hiện bất kỳ loại tối ưu hóa nào trong bản dựng gỡ lỗi.

Nếu sử dụng VC, hãy thử xây dựng bản phát hành với thông tin PDB được bật - điều này sẽ cho phép bạn theo dõi thông qua ứng dụng được tối ưu hóa và hy vọng bạn sẽ thấy những gì bạn muốn sau đó. Tuy nhiên, lưu ý rằng việc gỡ lỗi và theo dõi một bản dựng được tối ưu hóa sẽ đưa bạn đi khắp mọi nơi và thường bạn không thể kiểm tra các biến trực tiếp vì chúng chỉ dừng lại ở các thanh ghi hoặc được tối ưu hóa hoàn toàn. Đó là một trải nghiệm "thú vị" ...


2
hãy thử gcc tại sao -g -O3 và để có được các biến đổi trong bản dựng gỡ lỗi. xlC có hành vi tương tự.
g24l

Khi bạn nói "hầu hết các trình biên dịch": bạn xem xét bộ sưu tập trình biên dịch nào? Như đã chỉ ra, có ít nhất hai trình biên dịch thực hiện tối ưu hóa trong quá trình xây dựng gỡ lỗi - và theo như tôi biết thì VC cũng làm điều đó (trừ khi bạn có thể bật và sửa đổi có lẽ).
bầu trời

7

Như Greg đề cập, trình biên dịch sẽ không làm điều đó trong chế độ gỡ lỗi. Việc xây dựng gỡ lỗi sẽ chậm hơn so với bản dựng prod, nhưng chúng không bị sập thường xuyên hơn: và nếu bạn phụ thuộc vào tối ưu hóa cuộc gọi đuôi, chúng có thể thực hiện chính xác điều đó. Bởi vì điều này thường là tốt nhất để viết lại cuộc gọi đuôi như một vòng lặp bình thườ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.