Đây là một lỗi Clang
... khi nội tuyến một hàm chứa một vòng lặp vô hạn. Hành vi là khác nhau khi while(1);
xuất hiện trực tiếp trong chính, có mùi rất lỗi với tôi.
Xem câu trả lời của @ Arnavion để biết tóm tắt và liên kết. Phần còn lại của câu trả lời này được viết trước khi tôi xác nhận rằng đó là một lỗi, chứ chưa nói đến một lỗi đã biết.
Để trả lời câu hỏi tiêu đề: Làm cách nào để tạo một vòng lặp vô hạn không được tối ưu hóa? ? -
tạo die()
một macro, không phải là một hàm , để khắc phục lỗi này trong Clang 3.9 trở lên. (Phiên bản Clang Đầu hoặc giữ cho vòng lặp hoặc phát ra mộtcall
đến một phiên bản không-inline của hàm với các vòng lặp vô hạn.) Đó dường như là an toàn ngay cả khi print;while(1);print;
inlines chức năng vào nó gọi ( Godbolt ). -std=gnu11
so với -std=gnu99
không thay đổi bất cứ điều gì.
Nếu bạn chỉ quan tâm đến GNU C, thì P__J ____asm__("");
bên trong vòng lặp cũng hoạt động và không nên làm tối ưu hóa bất kỳ mã xung quanh nào cho bất kỳ trình biên dịch nào hiểu nó. Các câu lệnh asm GNU C Basic được ngầm địnhvolatile
, do đó, điều này được tính là một hiệu ứng phụ có thể nhìn thấy phải "thực thi" nhiều lần như trong máy trừu tượng C. (Và vâng, Clang thực hiện phương ngữ GNU của C, như được hướng dẫn bởi tài liệu hướng dẫn GCC.)
Một số người đã lập luận rằng nó có thể là hợp pháp để tối ưu hóa một vòng lặp vô hạn trống. Tôi không đồng ý 1 , nhưng ngay cả khi chúng tôi chấp nhận điều đó, Clang cũng không thể hợp pháp để giả sử các câu lệnh sau khi vòng lặp không thể truy cập được và để cho việc thực thi rơi vào phần cuối của hàm vào hàm tiếp theo hoặc vào thùng rác mà giải mã như hướng dẫn ngẫu nhiên.
(Điều đó sẽ tuân thủ tiêu chuẩn cho Clang ++ (nhưng vẫn không hữu ích lắm); các vòng lặp vô hạn không có bất kỳ tác dụng phụ nào là UB trong C ++, nhưng không phải C.
Trong khi (1); hành vi không xác định trong C? UB cho phép trình biên dịch phát ra bất cứ thứ gì về cơ bản đối với mã trên đường dẫn thực thi chắc chắn sẽ gặp UB. Một asm
câu lệnh trong vòng lặp sẽ tránh UB này cho C ++. Nhưng trong thực tế, Clang biên dịch như C ++ không loại bỏ các vòng lặp vô hạn biểu thức không đổi trừ khi nội tuyến, giống như khi biên dịch thành C.)
Thủ công nội tuyến while(1);
thay đổi cách Clang biên dịch nó: vòng lặp vô hạn hiện diện trong asm. Đây là những gì chúng ta mong đợi từ một luật sư POV.
#include <stdio.h>
int main() {
printf("begin\n");
while(1);
//infloop_nonconst(1);
//infloop();
printf("unreachable\n");
}
Trên trình thám hiểm trình biên dịch Godbolt , Clang 9.0 -O3 biên dịch thành C ( -xc
) cho x86-64:
main: # @main
push rax # re-align the stack by 16
mov edi, offset .Lstr # non-PIE executable can use 32-bit absolute addresses
call puts
.LBB3_1: # =>This Inner Loop Header: Depth=1
jmp .LBB3_1 # infinite loop
.section .rodata
...
.Lstr:
.asciz "begin"
Trình biên dịch tương tự với cùng các tùy chọn sẽ biên dịch một main
lệnh gọi infloop() { while(1); }
đến cùng trước puts
, nhưng sau đó chỉ dừng phát ra các hướng dẫn cho main
sau thời điểm đó. Vì vậy, như tôi đã nói, việc thực thi chỉ rơi vào cuối hàm, vào bất kỳ chức năng nào tiếp theo (nhưng với ngăn xếp được sắp xếp sai cho mục nhập chức năng để nó thậm chí không phải là một đuôi hợp lệ).
Các tùy chọn hợp lệ sẽ là
- phát ra một
label: jmp label
vòng lặp vô hạn
- hoặc (nếu chúng tôi chấp nhận rằng vòng lặp vô hạn có thể được loại bỏ) phát ra một cuộc gọi khác để in chuỗi thứ 2, sau đó
return 0
từ main
.
Sự cố hoặc tiếp tục mà không in "không thể truy cập" rõ ràng là không ổn đối với việc triển khai C11, trừ khi có UB mà tôi không nhận thấy.
Chú thích 1:
Đối với hồ sơ, tôi đồng ý với câu trả lời của @ Lundin, trích dẫn tiêu chuẩn cho bằng chứng rằng C11 không cho phép giả định chấm dứt đối với các vòng lặp vô hạn biểu thức không đổi, ngay cả khi chúng trống (không có I / O, dễ bay hơi, đồng bộ hóa hoặc khác tác dụng phụ có thể nhìn thấy).
Đây là tập hợp các điều kiện sẽ cho phép một vòng lặp được biên dịch thành một vòng lặp asm trống cho một CPU bình thường. (Ngay cả khi phần thân không trống trong nguồn, các phép gán cho các biến không thể hiển thị cho các luồng hoặc trình xử lý tín hiệu khác mà không có cuộc đua dữ liệu UB trong khi vòng lặp đang chạy. Vì vậy, việc triển khai tuân thủ có thể loại bỏ các thân vòng lặp đó nếu muốn đến. Sau đó, câu hỏi đặt ra là liệu vòng lặp có thể được gỡ bỏ hay không. ISO C11 nói rõ ràng là không.)
Cho rằng C11 chỉ ra trường hợp đó như là một trường hợp trong đó việc triển khai không thể giả sử vòng lặp chấm dứt (và đó không phải là UB), có vẻ như rõ ràng họ dự định vòng lặp sẽ có mặt vào thời gian chạy. Việc triển khai nhắm mục tiêu CPU với mô hình thực thi không thể thực hiện một lượng công việc vô hạn trong thời gian hữu hạn không có lý do gì để loại bỏ một vòng lặp vô hạn liên tục trống. Hoặc thậm chí nói chung, từ ngữ chính xác là về việc họ có thể "giả định chấm dứt" hay không. Nếu một vòng lặp không thể chấm dứt, điều đó có nghĩa là mã sau này không thể truy cập được, bất kể bạn tranh luận gì về toán học và vô số và mất bao lâu để thực hiện một lượng công việc vô hạn trên một số máy giả định.
Ngoài ra, Clang không chỉ đơn thuần là một DeathStation 9000 tuân thủ ISO C, nó dự định sẽ hữu ích cho lập trình hệ thống cấp thấp trong thế giới thực, bao gồm cả nhân và các công cụ nhúng. Vì vậy, cho dù bạn có chấp nhận tranh luận về việc C11 cho phép loại bỏ hay không while(1);
, điều đó không có nghĩa là Clang sẽ thực sự muốn làm điều đó. Nếu bạn viết while(1);
, đó có lẽ không phải là một tai nạn. Việc loại bỏ các vòng lặp kết thúc vô hạn một cách tình cờ (với các biểu thức điều khiển biến thời gian chạy) có thể hữu ích và thật hợp lý khi trình biên dịch thực hiện điều đó.
Thật hiếm khi bạn muốn quay cho đến lần gián đoạn tiếp theo, nhưng nếu bạn viết điều đó trong C thì đó chắc chắn là điều bạn mong đợi sẽ xảy ra. (Và những gì không xảy ra trong GCC và Clang, trừ Clang khi vòng lặp vô hạn là bên trong một hàm wrapper).
Ví dụ, trong nhân hệ điều hành nguyên thủy, khi bộ lập lịch không có tác vụ để chạy, nó có thể chạy tác vụ nhàn rỗi. Việc thực hiện đầu tiên có thể là while(1);
.
Hoặc đối với phần cứng không có bất kỳ tính năng nhàn rỗi tiết kiệm năng lượng nào, đó có thể là triển khai duy nhất. (Cho đến đầu những năm 2000, đó là điều tôi nghĩ không hiếm trên x86. Mặc dù hlt
hướng dẫn đã tồn tại, IDK nếu nó tiết kiệm được một lượng điện năng có ý nghĩa cho đến khi CPU bắt đầu có trạng thái nhàn rỗi năng lượng thấp.)