TL: DR:
- Trình biên dịch nội bộ có thể không được thiết lập để tìm kiếm tối ưu hóa này một cách dễ dàng và có lẽ nó chỉ hữu ích xung quanh các chức năng nhỏ, không phải bên trong các chức năng lớn giữa các cuộc gọi.
- Nội tuyến để tạo các hàm lớn là một giải pháp tốt hơn hầu hết thời gian
- Có thể có độ trễ so với đánh đổi thông lượng nếu
foo
xảy ra không lưu / khôi phục RBX.
Trình biên dịch là phần phức tạp của máy móc. Chúng không "thông minh" như con người và các thuật toán đắt tiền để tìm mọi tối ưu hóa có thể thường không đáng giá trong thời gian biên dịch thêm.
Tôi đã báo cáo đây là lỗi GCC 69986 - mã nhỏ hơn có thể với -Os bằng cách sử dụng đẩy / pop để đổ / tải lại vào năm 2016 ; không có hoạt động hoặc trả lời từ các nhà phát triển GCC. : /
Liên quan một chút: Lỗi GCC 70408 - sử dụng lại cùng một thanh ghi được bảo toàn cuộc gọi sẽ cung cấp mã nhỏ hơn trong một số trường hợp - nhà phát triển trình biên dịch nói với tôi rằng GCC sẽ phải mất một lượng lớn công việc để có thể thực hiện tối ưu hóa đó vì nó yêu cầu chọn thứ tự đánh giá của hai foo(int)
cuộc gọi dựa trên những gì sẽ làm cho mục tiêu trở nên đơn giản hơn.
Nếu foo
không tự lưu / khôi phục rbx
, sẽ có sự đánh đổi giữa thông lượng (số lệnh) so với lưu trữ bổ sung / độ trễ tải lại trên chuỗi x
-> chuỗi phụ thuộc retval.
Trình biên dịch thường ưu tiên độ trễ hơn thông lượng, ví dụ sử dụng 2x LEA thay vì imul reg, reg, 10
(độ trễ 3 chu kỳ, thông lượng 1 / xung nhịp), vì hầu hết mã trung bình thấp hơn đáng kể 4 uops / đồng hồ trên các đường ống 4 chiều thông thường như Skylake. (Tuy nhiên, nhiều hướng dẫn / uops chiếm nhiều không gian hơn trong ROB, làm giảm khoảng cách phía trước cùng một cửa sổ không theo thứ tự có thể nhìn thấy, và việc thực thi thực sự bùng nổ với các quầy hàng có thể chiếm một số ít hơn 4 uops / đồng hồ trung bình.)
Nếu foo
RBX đẩy / bật, thì sẽ không có nhiều để đạt được độ trễ. Việc khôi phục xảy ra ngay trước khi ret
thay vì chỉ sau đó có lẽ không liên quan, trừ khi có lỗi ret
dự đoán sai hoặc I-cache làm chậm quá trình tìm nạp mã tại địa chỉ trả về.
Hầu hết các hàm không tầm thường sẽ lưu / khôi phục RBX, do đó, thường không phải là một giả định tốt khi để lại một biến trong RBX sẽ thực sự có nghĩa là nó thực sự nằm trong một thanh ghi trong suốt cuộc gọi. (Mặc dù việc chọn ngẫu nhiên các chức năng của các thanh ghi được bảo toàn cuộc gọi chọn có thể là một ý tưởng tốt để giảm thiểu điều này đôi khi.)
Vì vậy, có push rdi
/ pop rax
sẽ hiệu quả hơn trong trường hợp này và đây có lẽ là một tối ưu hóa bị bỏ lỡ cho các hàm không có lá nhỏ, tùy thuộc vào điều gì foo
và sự cân bằng giữa độ trễ lưu trữ / tải lại thêm cho x
so với nhiều hướng dẫn hơn để lưu / khôi phục trình gọi rbx
.
Có thể cho siêu dữ liệu stack-bung đại diện cho các thay đổi đối với RSP ở đây, giống như nếu nó được sử dụng sub rsp, 8
để đổ / tải lại x
vào một vị trí ngăn xếp. (Tuy nhiên, trình biên dịch không biết tối ưu hóa này một trong hai, của việc sử dụng push
không gian dự trữ và khởi tạo một biến. Có gì C / C ++ biên dịch có thể sử dụng hướng dẫn đẩy pop để tạo các biến địa phương, thay vì chỉ tăng đặc biệt một lần? . Và làm điều đó hơn một var cục bộ sẽ dẫn đến .eh_frame
siêu dữ liệu thư giãn ngăn xếp lớn hơn bởi vì bạn đang di chuyển con trỏ ngăn xếp riêng biệt với mỗi lần đẩy. Tuy nhiên, điều đó không ngăn các trình biên dịch sử dụng Push / pop để lưu / khôi phục regs được bảo toàn cuộc gọi.)
IDK nếu nó đáng để các trình biên dịch giảng dạy tìm kiếm tối ưu hóa này
Đây có thể là một ý tưởng tốt xung quanh toàn bộ một chức năng, không phải qua một cuộc gọi bên trong một chức năng. Và như tôi đã nói, nó dựa trên giả định bi quan foo
sẽ cứu / khôi phục RBX. (Hoặc tối ưu hóa thông lượng nếu bạn biết rằng độ trễ từ giá trị x để trả về giá trị không quan trọng. Nhưng trình biên dịch không biết điều đó và thường tối ưu hóa cho độ trễ).
Nếu bạn bắt đầu thực hiện giả định bi quan đó bằng nhiều mã (như xung quanh các hàm gọi đơn bên trong các hàm), bạn sẽ bắt đầu nhận được nhiều trường hợp hơn khi RBX không được lưu / khôi phục và bạn có thể đã tận dụng lợi thế.
Bạn cũng không muốn có thêm lưu / khôi phục đẩy / bật trong một vòng lặp, chỉ cần lưu / khôi phục RBX bên ngoài vòng lặp và sử dụng các thanh ghi được bảo toàn cuộc gọi trong các vòng lặp thực hiện các cuộc gọi hàm. Ngay cả khi không có vòng lặp, trong trường hợp chung, hầu hết các hàm thực hiện nhiều lệnh gọi hàm. Ý tưởng tối ưu hóa này có thể áp dụng nếu bạn thực sự không sử dụng x
giữa bất kỳ cuộc gọi nào, ngay trước cuộc gọi đầu tiên và sau cuộc gọi cuối cùng, nếu không , bạn gặp vấn đề trong việc duy trì căn chỉnh ngăn xếp 16 byte cho mỗi cuộc gọi call
nếu bạn thực hiện một lần bật sau gọi, trước một cuộc gọi khác.
Trình biên dịch không tuyệt vời ở các chức năng nhỏ nói chung. Nhưng nó cũng không tuyệt vời cho CPU. Các lệnh gọi hàm không nội tuyến có tác động đến tối ưu hóa vào thời điểm tốt nhất, trừ khi các trình biên dịch có thể nhìn thấy phần bên trong của callee và đưa ra nhiều giả định hơn bình thường. Cuộc gọi hàm không nội tuyến là một rào cản bộ nhớ ngầm: người gọi phải giả định rằng một chức năng có thể đọc hoặc ghi bất kỳ dữ liệu nào có thể truy cập toàn cầu, vì vậy tất cả các bình như vậy phải được đồng bộ hóa với máy trừu tượng C. (Phân tích thoát cho phép giữ cho người dân địa phương trong các thanh ghi qua các cuộc gọi nếu địa chỉ của họ không thoát khỏi chức năng.) Ngoài ra, trình biên dịch phải giả định rằng các thanh ghi bị chặn bị chặn đều bị ghi đè. Điều này hút cho điểm nổi trong x86-64 System V, không có các thanh ghi XMM được bảo toàn cuộc gọi.
Các chức năng nhỏ như bar()
tốt hơn là nội tuyến vào người gọi của họ. Biên dịch với -flto
để điều này có thể xảy ra ngay cả trên các ranh giới tệp trong hầu hết các trường hợp. (Con trỏ hàm và ranh giới thư viện dùng chung có thể đánh bại điều này.)
Tôi nghĩ một lý do khiến các trình biên dịch không bận tâm để thực hiện các tối ưu hóa này là nó sẽ yêu cầu một loạt các mã khác nhau trong các phần tử trình biên dịch , khác với ngăn xếp thông thường so với mã phân bổ đăng ký biết cách lưu giữ cuộc gọi đăng ký và sử dụng chúng.
tức là sẽ có rất nhiều việc phải thực hiện, và rất nhiều mã để duy trì, và nếu nó quá nhiệt tình về việc này thì nó có thể làm cho mã tệ hơn .
Và cũng là nó (hy vọng) không đáng kể; nếu vấn đề, bạn nên nội tuyến bar
vào gọi của nó, hoặc nội tuyến foo
vào bar
. Điều này là tốt trừ khi có rất nhiều bar
chức năng giống như khác nhau và foo
lớn, và vì một số lý do, họ không thể nội tuyến vào người gọi của họ.