Một hàm đệ quy có thể là nội tuyến?


134
inline int factorial(int n)
{
    if(!n) return 1;
    else return n*factorial(n-1);
}

Khi tôi đọc , thấy rằng đoạn mã trên sẽ dẫn đến "biên dịch vô hạn" nếu không được trình biên dịch xử lý chính xác.

Làm thế nào để trình biên dịch quyết định có nên nội tuyến một chức năng hay không?

Câu trả lời:


137

Đầu tiên, inlineđặc tả về một chức năng chỉ là một gợi ý. Trình biên dịch có thể (và thường không) hoàn toàn bỏ qua sự hiện diện hay vắng mặt của inlinevòng loại. Như đã nói, một trình biên dịch có thể nội tuyến một hàm đệ quy, vì nó có thể hủy bỏ một vòng lặp vô hạn. Nó chỉ đơn giản là phải đặt một giới hạn ở mức độ mà nó sẽ "hủy đăng ký" chức năng.

Trình biên dịch tối ưu hóa có thể biến mã này:

inline int factorial(int n)
{
    if (n <= 1)
    {
        return 1;
    }
    else
    {
        return n * factorial(n - 1);
    }
}

int f(int x)
{
    return factorial(x);
}

vào mã này:

int factorial(int n)
{
    if (n <= 1)
    {
        return 1;
    }
    else
    {
        return n * factorial(n - 1);
    }
}

int f(int x)
{
    if (x <= 1)
    {
        return 1;
    }
    else
    {
        int x2 = x - 1;
        if (x2 <= 1)
        {
            return x * 1;
        }
        else
        {
            int x3 = x2 - 1;
            if (x3 <= 1)
            {
                return x * x2 * 1;
            }
            else
            {
                return x * x2 * x3 * factorial(x3 - 1);
            }
        }
    }
}

Trong trường hợp này, về cơ bản chúng tôi đã nội dung hàm 3 lần. Một số trình biên dịch làm thực hiện tối ưu hóa này. Tôi nhớ rằng MSVC ++ có cài đặt để điều chỉnh mức độ nội tuyến sẽ được thực hiện trên các hàm đệ quy (tối đa 20, tôi tin).


20
đó là #pragma inline_recursion (bật). Tài liệu về độ sâu tối đa không nhất quán hoặc không có kết luận. Các giá trị 8, 16 hoặc giá trị của #pragma inline_depth là có thể.
peterchen

@peterchen Nếu hàm được nội tuyến đang thay đổi giá trị của một trong các đối số của nó thì điều gì xảy ra, tôi nghĩ tốt hơn là nội tuyến hàm bên trong thực tế thay vì chính. Xin lỗi vì tiếng Anh của tôi
ob_dev

1
@obounaim: Bạn có thể nghĩ rằng. MSVC không.
SecurityMatt

23

Thật vậy, nếu trình biên dịch của bạn không hoạt động thông minh, nó có thể thử chèn các bản sao của inlinehàm d của bạn một cách đệ quy, tạo mã lớn vô hạn. Hầu hết các trình biên dịch hiện đại sẽ nhận ra điều này, tuy nhiên. Họ có thể:

  1. Không nội tuyến chức năng nào cả
  2. Đặt nội tuyến đến một độ sâu nhất định và nếu sau đó nó chưa kết thúc, hãy gọi phiên bản riêng của hàm bằng cách sử dụng quy ước gọi hàm tiêu chuẩn. Điều này có thể xử lý nhiều trường hợp phổ biến theo cách hiệu suất cao, trong khi để lại dự phòng cho trường hợp hiếm gặp với độ sâu cuộc gọi lớn. Điều này cũng có nghĩa là bạn giữ cả hai phiên bản nội tuyến và riêng biệt của mã chức năng đó.

Đối với trường hợp 2, nhiều trình biên dịch có #pragmas bạn có thể đặt để chỉ định độ sâu tối đa mà việc này sẽ được thực hiện. Trong gcc , bạn cũng có thể chuyển cái này từ dòng lệnh với --max-inline-insns-recursive(xem thêm thông tin ở đây ).


7

AFAIK GCC sẽ thực hiện loại bỏ cuộc gọi đuôi trên các chức năng đệ quy, nếu có thể. Chức năng của bạn tuy nhiên không phải là đệ quy đuôi.


6

Trình biên dịch tạo một biểu đồ cuộc gọi; khi một chu trình được phát hiện gọi chính nó, hàm không còn được đặt vào sau một độ sâu nhất định (n = 1, 10, 100, bất kể trình biên dịch được điều chỉnh theo).


3

Một số hàm đệ quy có thể được chuyển thành các vòng lặp, điều này thực sự vô hiệu hóa chúng. Tôi tin rằng gcc có thể làm điều này, nhưng tôi không biết về các trình biên dịch khác.


2

Xem các câu trả lời đã được đưa ra để biết tại sao điều này thường không hoạt động.

Là một "chú thích", bạn có thể đạt được hiệu quả mà bạn đang tìm kiếm (ít nhất là cho giai thừa mà bạn đang sử dụng làm ví dụ) bằng cách sử dụng siêu lập trình mẫu . Dán từ Wikipedia:

template <int N>
struct Factorial 
{
    enum { value = N * Factorial<N - 1>::value };
};

template <>
struct Factorial<0> 
{
    enum { value = 1 };
};

1
Điều đó rất dễ thương, nhưng xin lưu ý rằng bài đăng gốc có một đối số biến "int n".
Lập trình viên Windows

1
Đúng, nhưng cũng có rất ít điểm muốn "nội tuyến đệ quy" khi n không được biết đến vào thời gian biên dịch ... làm thế nào trình biên dịch có thể đạt được điều đó? Vì vậy, trong bối cảnh của câu hỏi tôi nghĩ rằng đây là một thay thế có liên quan.
yungchin

1
Xem ví dụ về cách thực hiện của Derek Park: Bằng cách nội tuyến hai lần, bạn lặp lại n >> 2 lần và bạn có lợi nhuận 2 + 2 từ mã kết quả.
MSalters

1

Trình biên dịch sẽ tạo một biểu đồ cuộc gọi để phát hiện các loại điều này và ngăn chặn chúng. Vì vậy, nó sẽ thấy rằng hàm gọi chính nó và không nội tuyến.

Nhưng chủ yếu nó được điều khiển bởi các khóa chuyển đổi từ khóa và trình biên dịch nội tuyến (Ví dụ: bạn có thể tự động thực hiện các chức năng nhỏ nội tuyến ngay cả khi không có từ khóa.) Điều quan trọng cần lưu ý là các phần biên dịch Debug không bao giờ được đặt nội tuyến vì lệnh gọi sẽ không được bảo toàn để phản chiếu các cuộc gọi bạn đã tạo trong mã.


1

"Làm thế nào để trình biên dịch quyết định có nên nội tuyến một hàm hay không?"

Điều đó phụ thuộc vào trình biên dịch, các tùy chọn đã được chỉ định, số phiên bản của trình biên dịch, có thể là bao nhiêu bộ nhớ khả dụng, v.v.

Mã nguồn của chương trình vẫn phải tuân theo các quy tắc cho các hàm được nội tuyến. Cho dù chức năng có được nội tuyến hay không, bạn phải chuẩn bị cho khả năng nó sẽ được nội tuyến (một số lần không xác định).

Tuyên bố Wikipedia rằng các macro đệ quy thường có vẻ bất hợp pháp khá kém thông tin. C và C ++ ngăn chặn các yêu cầu đệ quy nhưng một đơn vị dịch thuật không trở thành bất hợp pháp bằng cách chứa mã macro trông giống như nó sẽ được đệ quy. Trong trình biên dịch, các macro đệ quy thường hợp pháp.


0

Một số trình biên dịch (Ie Borland C ++) không có mã nội tuyến có chứa các câu lệnh điều kiện (if, case, while, v.v.) nên hàm đệ quy trong ví dụ của bạn sẽ không được nội tuyến.

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.