Tại sao mã chủ động cố gắng ngăn chặn tối ưu hóa cuộc gọi đuôi?


81

Tiêu đề của câu hỏi có thể hơi lạ, nhưng vấn đề là, theo như tôi biết, không có gì chống lại tối ưu hóa cuộc gọi đuôi cả. Tuy nhiên, trong khi duyệt các dự án mã nguồn mở, tôi đã gặp một số chức năng tích cực cố gắng ngăn trình biên dịch thực hiện tối ưu hóa lệnh gọi đuôi, ví dụ như việc triển khai CFRunLoopRef chứa đầy các bản hack như vậy . Ví dụ:

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(CFRunLoopObserverCallBack func, CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    if (func) {
        func(observer, activity, info);
    }
    getpid(); // thwart tail-call optimization
}

Tôi rất muốn biết tại sao điều này dường như lại quan trọng đến vậy, và có trường hợp nào tôi là một nhà phát triển bình thường cũng nên ghi nhớ điều này không? Ví dụ. có những cạm bẫy thường gặp với tối ưu hóa cuộc gọi đuôi không?


10
Một lỗi có thể xảy ra có thể là một ứng dụng hoạt động trơn tru trên một số nền tảng và sau đó đột ngột ngừng hoạt động khi được biên dịch bằng trình biên dịch không hỗ trợ tối ưu hóa cuộc gọi đuôi. Hãy nhớ rằng việc tối ưu hóa này thực sự không chỉ có thể tăng hiệu suất mà còn ngăn ngừa lỗi thời gian chạy (tràn ngăn xếp).
Niklas B.

5
@NiklasB. Nhưng đây không phải là lý do để không cố gắng vô hiệu hóa nó?
JustSid

4
Một cuộc gọi hệ thống có thể là một cách chắc chắn để khai thác TCO, nhưng cũng là một cách khá tốn kém.
Fred Foo

39
Đây là một thời điểm tuyệt vời có thể dạy được để nhận xét thích hợp. +1 để giải thích một phần lý do tại sao dòng đó lại ở đó (để ngăn chặn tối ưu hóa cuộc gọi đuôi), -100 vì không giải thích lý do tại sao tối ưu hóa cuộc gọi đuôi cần bị tắt ngay từ đầu ...
Mark Sowul

16
Vì giá trị của getpid()không được sử dụng, nó không thể bị xóa bởi một trình tối ưu hóa được thông báo (vì getpidlà một hàm được biết là không có tác dụng phụ), do đó vẫn cho phép trình biên dịch thực hiện tối ưu hóa lệnh gọi đuôi? Đây có vẻ là một cơ chế thực sự mong manh.
luiscubal

Câu trả lời:


83

Suy đoán của tôi ở đây là nó để đảm bảo rằng nó __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__nằm trong dấu vết ngăn xếp cho mục đích gỡ lỗi. Nó đã __attribute__((no inline))ủng hộ ý tưởng này.

Nếu bạn để ý, dù sao thì chức năng đó cũng chỉ đi và trả lại cho một chức năng khác, vì vậy nó là một dạng tấm bạt lò xo mà tôi chỉ có thể nghĩ là có với một cái tên dài dòng như vậy để hỗ trợ gỡ lỗi. Điều này sẽ đặc biệt hữu ích vì hàm đang gọi một con trỏ hàm đã được đăng ký từ nơi khác và do đó hàm đó có thể không truy cập được các ký hiệu gỡ lỗi.

Cũng chú ý đến các hàm có tên tương tự khác thực hiện những việc tương tự - có vẻ như nó thực sự ở đó để hỗ trợ việc xem những gì đã xảy ra từ một dấu vết. Hãy nhớ rằng đây là mã Mac OS X cốt lõi và sẽ hiển thị trong báo cáo sự cố cũng như xử lý báo cáo mẫu.


Vâng, điều đó phù hợp với __attribute__((noinline)). Tôi nghĩ bạn đang ở đây.
Niklas B.

Vâng, thực sự có ý nghĩa. Nhưng nếu bạn nhìn vào nơi các hàm này được gọi từ đâu, bạn sẽ thấy rằng chúng luôn chỉ được gọi từ một hàm, ví dụ như hàm ví dụ của tôi chỉ được gọi từ __CFRunLoopDoObserversđó chắc chắn hiển thị trong dấu vết ngăn xếp ...
JustSid

1
Chắc chắn rồi, nhưng tôi đoán đó là một điểm đánh dấu khác cho chính xác vị trí mà callback / block / etc của người quan sát đang chạy.
mattjgalloway

2
Tôi nghĩ đây là câu trả lời tốt nhất. +1
R .. GitHub DỪNG TRỢ GIÚP TỪ NGÀY

@R .. Tôi chỉ có thể chấp nhận một câu trả lời và Andrew White cũng đã nêu tên các trường hợp khác mà có thể không muốn tối ưu hóa cuộc gọi đuôi. Hãy nhớ rằng, tôi không hỏi tại sao hàm lại làm điều đó nhưng tại sao nó có thể không được mong muốn nói chung và đã đưa ra hàm như một ví dụ trong thế giới thực.
JustSid

34

Đây chỉ là một phỏng đoán, nhưng có thể để tránh một vòng lặp vô hạn và ném bom ra ngoài với lỗi tràn ngăn xếp.

Vì phương thức được đề cập không đặt bất kỳ thứ gì vào ngăn xếp, nên có vẻ như tối ưu hóa đệ quy cuộc gọi đuôi tạo ra mã sẽ nhập vào một vòng lặp vô hạn trái ngược với mã không được tối ưu hóa sẽ đặt địa chỉ trả về trên ngăn xếp mà cuối cùng sẽ tràn trong trường hợp sử dụng sai.

Suy nghĩ khác duy nhất mà tôi có liên quan đến việc duy trì các lệnh gọi trên ngăn xếp để gỡ lỗi và in stacktrace.


8
Tôi nghĩ rằng giải thích về stacktrace / gỡ lỗi có nhiều khả năng hơn (và tôi đã chuẩn bị đăng nó). Vòng lặp vô hạn không thực sự tồi tệ hơn sự cố, vì người dùng có thể buộc ứng dụng thoát. Điều đó cũng sẽ giải thích noinline.
ughoavgfhw

3
@ughoavgfhw: có thể, nhưng khi bạn tham gia vào luồng và như vậy, các vòng lặp vô hạn thực sự khó theo dõi. Tôi luôn có suy nghĩ rằng lạm dụng sẽ gây ra một ngoại lệ. Vì tôi chưa bao giờ phải làm điều này nên nó vẫn chỉ là phỏng đoán.
Andrew White

1
tính đồng bộ, đại loại là ... Tôi vừa gặp phải một lỗi xấu khiến ứng dụng vẫn mở cửa sổ mới. Điều này khiến tôi nghĩ, nếu ứng dụng đã bị lỗi trước khi cố gắng bão hòa "đống" (bộ nhớ của tôi) và làm nghẹt thở X, tôi sẽ không cần phải chuyển sang thiết bị đầu cuối để đột ngột giết ứng dụng điên rồ (vì X đã sớm trở thành không phản hồi). Vì vậy, có lẽ nó sẽ là một lý do để thích cách tiếp cận "fail fast" có thể đi kèm với tràn ngăn xếp và không có tối ưu hóa ...? hoặc có thể đó chỉ là một vấn đề khác, mặc dù ...!
ShinTakezou

2
@AndrewWhite. Nhưng nếu bạn muốn nhận stacktraces từ người dùng, tôi đồng ý rằng vòng lặp vô hạn là có vấn đề, vì vậy điều đó có vẻ hợp lý - một lỗi sẽ xuất hiện trong nhật ký của bạn, vòng lặp vô hạn thì không.
Voo

1
Điều này giả định rằng ngay từ đầu, hàm là đệ quy - nhưng không phải vậy; không trực tiếp cũng không gián tiếp (bằng cách nhìn vào ngữ cảnh nơi hàm xuất phát). Tôi đã đưa ra giả định sai lầm ban đầu.
Konrad Rudolph

21

Một lý do tiềm năng là làm cho việc gỡ lỗi và lập hồ sơ dễ dàng hơn (với TCO, khung ngăn xếp mẹ biến mất, điều này làm cho dấu vết ngăn xếp khó hiểu hơn).


2
Tuy nhiên, việc tạo hồ sơ dễ dàng hơn với chi phí làm chậm chương trình là một điều khá kỳ lạ. Nó cũng có ý nghĩa như việc pha loãng dầu của bạn trước khi đo xem xe của bạn có thể đi được bao xa: x
Matthieu M.

1
@MatthieuM: Một điều như vậy sẽ không có ý nghĩa nếu lệnh gọi thêm vào được thực hiện hàng triệu lần trong một vòng lặp, nhưng nếu nó được thực hiện vài trăm lần một giây hoặc ít hơn, có thể tốt hơn là để nó trong hệ thống thực và có thể kiểm tra cách hệ thống thực hoạt động, thay vì loại bỏ nó và có nguy cơ bị loại bỏ như vậy tạo ra một thay đổi tinh tế nhưng quan trọng trong hành vi của hệ thống.
supercat

@MatthieuM. Nếu pha loãng dầu của bạn là điều kiện tiên quyết cho bất kỳ phép đo nào, thì nó thực sự có ý nghĩa hoàn hảo.
Dmitry Grigoryev

@DmitryGrigoryev: Không, không. Không có biện pháp nào là khó chịu, nhưng một biện pháp sai sẽ từ vô dụng đến nguy hiểm (tùy thuộc vào mức độ tin tưởng mà bạn đặt vào nó). Tiếp tục với phép tương tự về dầu: nếu nó làm bạn chậm lại, thì bạn có thể nhận được các số đo cho thấy rằng trọng lượng quan trọng hơn khí động học và do đó loại bỏ trọng lượng và làm xấu tính khí động học để tối ưu hóa cho những gì bạn đã đo ... tuy nhiên với dầu thật, khi bạn đi nhanh hơn, hóa ra khí động học quan trọng hơn và việc "cải thiện" của bạn còn tệ hơn là không làm gì!
Matthieu M.

@MatthieuM. Bạn có quen thuộc với nguyên lý bất định không? Bất kỳ phép đo nào cũng sai ở một mức độ nào đó, bởi vì không có cách nào để đo bất cứ thứ gì mà không tương tác với đối tượng được đo. Vì vậy, ngay cả khi bạn không thay đổi dầu trong ví dụ của mình, dù sao thì việc sửa chữa xe sẽ thay đổi khí động học.
Dmitry Grigoryev
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.