Khi nào chức năng gọi chi phí vẫn còn quan trọng trong trình biên dịch hiện đại?


95

Tôi là một người tôn giáo và nỗ lực không phạm tội. Đó là lý do tại sao tôi có xu hướng viết các hàm nhỏ ( nhỏ hơn thế , để sắp xếp lại các hàm Robert C. Martin) để tuân thủ một số điều răn do kinh thánh Clean Code yêu cầu . Nhưng trong khi kiểm tra một số thứ, tôi đã đăng bài này , bên dưới tôi đọc bình luận này:

Hãy nhớ rằng chi phí của một cuộc gọi phương thức có thể là đáng kể, tùy thuộc vào ngôn ngữ. Hầu như luôn luôn có một sự đánh đổi giữa việc viết mã có thể đọc được và viết mã trình diễn.

Trong những điều kiện nào thì tuyên bố được trích dẫn này vẫn còn hiệu lực ngày nay với ngành công nghiệp phong phú của trình biên dịch hiện đại?

Đó là câu hỏi duy nhất của tôi. Và nó không phải là về việc tôi nên viết các hàm dài hay nhỏ. Tôi chỉ nhấn mạnh rằng phản hồi của bạn có thể - hoặc không - góp phần thay đổi thái độ của tôi và khiến tôi không thể cưỡng lại sự cám dỗ của những kẻ báng bổ .


11
Viết mã có thể đọc và duy trì. Chỉ khi bạn gặp vấn đề với lỗi tràn ngăn xếp, bạn mới có thể nghĩ lại về spproach của mình
Fabio

33
Một câu trả lời chung ở đây là không thể. Có quá nhiều trình biên dịch khác nhau, thực hiện quá nhiều đặc tả ngôn ngữ khác nhau. Và sau đó là các ngôn ngữ do JIT biên soạn, các ngôn ngữ được dịch động, v.v. Mặc dù vậy, có thể nói rằng, nếu bạn đang biên dịch mã C hoặc C ++ bản địa bằng một trình biên dịch hiện đại, bạn không phải lo lắng về chi phí của một cuộc gọi hàm. Trình tối ưu hóa sẽ nội tuyến những điều này bất cứ khi nào nó thích hợp. Là một người đam mê tối ưu hóa vi mô, tôi hiếm khi thấy các trình biên dịch đưa ra các quyết định nội tuyến mà tôi hoặc các điểm chuẩn của tôi không đồng ý.
Cody Grey

6
Phát biểu từ kinh nghiệm cá nhân, tôi viết mã bằng một ngôn ngữ độc quyền khá hiện đại về khả năng, nhưng các cuộc gọi chức năng rất tốn kém, đến mức mà ngay cả các vòng lặp điển hình phải được tối ưu hóa cho tốc độ: for(Integer index = 0, size = someList.size(); index < size; index++)thay vì đơn giản for(Integer index = 0; index < someList.size(); index++). Chỉ vì trình biên dịch của bạn đã được thực hiện trong vài năm qua không nhất thiết có nghĩa là bạn có thể từ bỏ hồ sơ.
phyrfox

5
@phyrfox chỉ có ý nghĩa, nhận giá trị của someList.size () bên ngoài vòng lặp thay vì gọi nó mỗi lần qua vòng lặp. Điều đó đặc biệt đúng nếu có bất kỳ vấn đề đồng bộ hóa nào mà độc giả và nhà văn có thể cố gắng xung đột trong quá trình lặp, trong trường hợp đó bạn cũng muốn bảo vệ danh sách trước mọi thay đổi trong quá trình lặp.
Craig

8
Coi chừng việc thực hiện các chức năng nhỏ quá xa, nó có thể làm xáo trộn mã hiệu quả như một chức năng lớn nguyên khối. Nếu bạn không tin tôi, hãy xem một số người chiến thắng của ioccc.org : Một số mã mọi thứ thành một main(), một số khác chia mọi thứ thành 50 chức năng nhỏ và tất cả đều không thể đọc được. Bí quyết là, như mọi khi, để đạt được một sự cân bằng tốt .
cmaster

Câu trả lời:


148

Nó phụ thuộc vào tên miền của bạn.

Nếu bạn đang viết mã cho vi điều khiển công suất thấp, thì chi phí cuộc gọi phương thức có thể là đáng kể. Nhưng nếu bạn đang tạo trang web hoặc ứng dụng bình thường, thì chi phí cuộc gọi phương thức sẽ không đáng kể so với phần còn lại của mã. Trong trường hợp đó, sẽ luôn có giá trị hơn khi tập trung vào các thuật toán và cấu trúc dữ liệu phù hợp thay vì tối ưu hóa vi mô như các cuộc gọi phương thức.

Và cũng có câu hỏi về trình biên dịch nội tuyến các phương thức cho bạn. Hầu hết các trình biên dịch đủ thông minh cho các hàm nội tuyến, nơi có thể.

Và cuối cùng, có một nguyên tắc vàng về hiệu suất: LUÔN LUÔN HỒ SƠ ĐẦU TIÊN. Đừng viết mã "tối ưu hóa" dựa trên các giả định. Nếu bạn không chắc chắn, hãy viết cả hai trường hợp và xem cái nào tốt hơn.


13
Và ví dụ, trình biên dịch HotSpot thực hiện Inlining đầu cơ , trong một số ý nghĩa là nội tuyến ngay cả khi không thể.
Jörg W Mittag

49
Trên thực tế, trong một ứng dụng web, toàn bộ mã có thể không đáng kể liên quan đến truy cập DB và lưu lượng mạng ...
AnoE

72
Tôi thực sự thích công suất nhúng và cực thấp với trình biên dịch rất cũ mà hầu như không biết tối ưu hóa nghĩa là gì, và tin tôi mặc dù chức năng gọi là vấn đề không bao giờ là nơi đầu tiên để tìm kiếm tối ưu hóa. Ngay cả trong miền thích hợp này, chất lượng mã là ưu tiên hàng đầu trong trường hợp này.
Tim

2
@Mehrdad Ngay cả trong trường hợp này, tôi sẽ ngạc nhiên nếu không có gì phù hợp hơn để tối ưu hóa mã. Khi lược tả mã, tôi thấy mọi thứ nặng hơn các hàm gọi và đó là nơi phù hợp để tìm kiếm tối ưu hóa. Một số nhà phát triển phát điên vì một hoặc hai LỘC chưa được tối ưu hóa nhưng khi bạn lập hồ sơ SW, bạn nhận ra rằng thiết kế quan trọng hơn điều này, ít nhất là đối với phần lớn nhất của mã. Khi bạn tìm thấy nút cổ chai, bạn có thể cố gắng tối ưu hóa nó và nó sẽ có tác động lớn hơn nhiều so với tối ưu hóa tùy ý ở mức độ thấp như viết các hàm lớn để tránh các cuộc gọi trên cao.
Tim

8
Câu trả lời tốt! Điểm cuối cùng của bạn nên là đầu tiên: Luôn luôn hồ sơ trước khi quyết định nơi tối ưu hóa .
CJ Dennis

56

Chi phí cuộc gọi chức năng phụ thuộc hoàn toàn vào ngôn ngữ và ở mức độ bạn đang tối ưu hóa.

Ở mức cực thấp, các lệnh gọi hàm và thậm chí nhiều hơn các cuộc gọi phương thức ảo có thể tốn kém nếu chúng dẫn đến sai lệch nhánh hoặc lỗi bộ đệm CPU. Nếu bạn đã viết lắp ráp , bạn cũng sẽ biết rằng bạn cần một vài hướng dẫn thêm để lưu và khôi phục thanh ghi xung quanh một cuộc gọi. Không phải là trình biên dịch đủ thông minh, có khả năng xử lý các hàm chính xác để tránh chi phí này, bởi vì trình biên dịch bị giới hạn bởi ngữ nghĩa của ngôn ngữ (đặc biệt là xung quanh các tính năng như gửi phương thức giao diện hoặc thư viện được tải động).

Ở cấp độ cao, các ngôn ngữ như Perl, Python, Ruby thực hiện rất nhiều việc ghi sổ cho mỗi lệnh gọi hàm, khiến cho các ngôn ngữ đó tương đối tốn kém. Điều này được làm tồi tệ hơn bởi lập trình meta. Có lần tôi đã tăng tốc phần mềm Python 3x chỉ bằng cách gọi chức năng gọi ra khỏi một vòng lặp rất nóng. Trong mã quan trọng về hiệu năng, các hàm trợ giúp nội tuyến có thể có hiệu ứng rõ rệt.

Nhưng phần lớn phần mềm không quá quan trọng về hiệu năng mà bạn có thể nhận thấy chi phí cuộc gọi chức năng. Trong mọi trường hợp, viết sạch, mã đơn giản sẽ được đền đáp:

  • Nếu mã của bạn không quan trọng về hiệu năng, điều này giúp bảo trì dễ dàng hơn. Ngay cả trong phần mềm quan trọng về hiệu năng, phần lớn mã sẽ không phải là điểm nóng của trực tuyến.

  • Nếu mã của bạn quan trọng về hiệu năng, mã đơn giản giúp bạn dễ hiểu mã hơn và phát hiện ra các cơ hội để tối ưu hóa. Những chiến thắng lớn nhất thường không đến từ tối ưu hóa vi mô như các hàm nội tuyến, mà từ các cải tiến thuật toán. Hoặc phrased khác nhau: không làm điều tương tự nhanh hơn. Tìm cách để làm ít hơn.

Lưu ý rằng, mã đơn giản của người Viking không có nghĩa là được cung cấp thành hàng ngàn chức năng nhỏ. Mọi chức năng cũng giới thiệu một chút chi phí nhận thức - khó khăn hơn để lý giải về mã trừu tượng hơn. Tại một số điểm, các hàm nhỏ này có thể làm rất ít mà không sử dụng chúng sẽ đơn giản hóa mã của bạn.


16
Một DBA thực sự thông minh đã từng nói với tôi "Bình thường hóa cho đến khi đau, sau đó không chuẩn hóa cho đến khi không." Dường như với tôi nó có thể được chia sẻ lại thành "Trích xuất các phương thức cho đến khi nó đau, sau đó nội tuyến cho đến khi nó không."
RubberDuck

1
Ngoài chi phí nhận thức, còn có chi phí tượng trưng trong thông tin trình gỡ lỗi và thông thường chi phí trong các nhị phân cuối cùng là không thể tránh khỏi.
Frank Hileman

Về trình biên dịch thông minh - họ CÓ THỂ làm điều đó, không phải lúc nào cũng vậy. Ví dụ, jvm có thể nội tuyến mọi thứ dựa trên hồ sơ thời gian chạy với bẫy rất rẻ / miễn phí cho đường dẫn không phổ biến hoặc hàm đa hình nội tuyến mà chỉ có một triển khai phương thức / giao diện cụ thể và sau đó mở rộng lệnh gọi đó thành đa hình đúng khi lớp con mới được tải động thời gian chạy. Nhưng vâng, có nhiều ngôn ngữ mà những điều như vậy là không thể và nhiều trường hợp ngay cả trong jvm, khi nó không hiệu quả về chi phí hoặc có thể trong trường hợp chung.
Artur Biesiadowski

19

Hầu như tất cả các câu ngạn ngữ về điều chỉnh mã cho hiệu suất là trường hợp đặc biệt của luật Amdahl . Câu nói ngắn gọn, hài hước về luật của Amdahl là

Nếu một phần trong chương trình của bạn chiếm 5% thời gian chạy và bạn tối ưu hóa phần đó để bây giờ chiếm không phần trăm thời gian chạy, toàn bộ chương trình sẽ chỉ nhanh hơn 5%.

(Tối ưu hóa mọi thứ xuống 0% thời gian chạy là hoàn toàn có thể: khi bạn ngồi xuống để tối ưu hóa một chương trình lớn, phức tạp, bạn hoàn toàn có khả năng thấy rằng nó chi tiêu ít nhất một số thời gian chạy của nó cho những thứ mà nó không cần phải làm .)

Đây là lý do tại sao mọi người thường nói không lo lắng về chi phí cuộc gọi chức năng: cho dù chúng đắt thế nào, thông thường toàn bộ chương trình chỉ dành một phần rất nhỏ thời gian chạy của nó cho chi phí cuộc gọi, vì vậy việc tăng tốc chúng không giúp ích gì nhiều .

Nhưng, nếu có một mẹo bạn có thể thực hiện để làm cho tất cả các chức năng gọi nhanh hơn, thì mẹo đó có thể đáng giá. Các nhà phát triển trình biên dịch dành nhiều thời gian để tối ưu hóa chức năng "prologues" và "epilogues", bởi vì điều đó mang lại lợi ích cho tất cả các chương trình được biên dịch với trình biên dịch đó, ngay cả khi nó chỉ là một chút xíu cho mỗi chương trình.

Và, nếu bạn có lý do để tin rằng một chương trình đang tiêu tốn rất nhiều thời gian chạy của nó chỉ để thực hiện các cuộc gọi chức năng, thì bạn nên bắt đầu suy nghĩ về việc liệu một số cuộc gọi chức năng đó có cần thiết hay không. Dưới đây là một số quy tắc để biết khi nào bạn nên làm điều này:

  • Nếu thời gian chạy cho mỗi lần gọi của hàm nhỏ hơn một phần nghìn giây, nhưng hàm đó được gọi là hàng trăm nghìn lần, thì có lẽ nên được nội tuyến.

  • Nếu một cấu hình của chương trình hiển thị hàng ngàn hàm và không có hàm nào chiếm hơn 0,1% thời gian chạy, thì tổng phí gọi hàm có thể có ý nghĩa tổng hợp.

  • Nếu bạn có " mã lasagna " , trong đó có nhiều lớp trừu tượng mà hầu như không có công việc nào ngoài việc gửi đến lớp tiếp theo và tất cả các lớp này được thực hiện bằng các lệnh gọi phương thức ảo, thì rất có thể CPU đang lãng phí rất nhiều thời gian trên các quầy hàng đường ống chi nhánh gián tiếp. Thật không may, cách chữa trị duy nhất cho việc này là loại bỏ một số lớp, thường rất khó.


7
Chỉ cần cẩn thận với những thứ đắt tiền được thực hiện sâu trong các vòng lặp lồng nhau. Tôi đã tối ưu hóa một chức năng và nhận được mã chạy nhanh gấp 10 lần. Đó là sau khi hồ sơ chỉ ra thủ phạm. (Nó được gọi nhiều lần, trong các vòng lặp từ O (n ^ 3) đến một n nhỏ (n ^ 6).)
Loren Pechtel

"Thật không may, cách chữa trị duy nhất cho việc này là loại bỏ một số lớp, thường rất khó." - điều này phụ thuộc rất nhiều vào trình biên dịch ngôn ngữ và / hoặc công nghệ máy ảo của bạn. Nếu bạn có thể sửa đổi mã để trình biên dịch trở nên dễ dàng hơn (ví dụ: bằng cách sử dụng finalcác lớp và phương thức áp dụng trong Java hoặc không phải là virtualphương thức trong C # hoặc C ++) thì trình biên dịch có thể được loại bỏ bởi trình biên dịch / thời gian chạy và bạn ' sẽ thấy một lợi ích mà không cần tái cấu trúc lớn. Như @JorgWMittag chỉ ra ở trên, JVM thậm chí có thể nội tuyến trong trường hợp không thể chứng minh rằng tối ưu hóa là ...
Jules

... hợp lệ, do đó, có thể là nó đang thực hiện nó trong mã của bạn mặc dù cách phân lớp nào.
Jules

@Jules Mặc dù đúng là trình biên dịch JIT có thể thực hiện tối ưu hóa đầu cơ, nhưng điều đó không có nghĩa là tối ưu hóa như vậy được áp dụng thống nhất. Cụ thể liên quan đến Java, kinh nghiệm của tôi là văn hóa nhà phát triển ưu tiên các lớp xếp chồng lên nhau trên các lớp dẫn đến các ngăn xếp cuộc gọi cực kỳ sâu sắc. Thông thường, điều đó góp phần vào cảm giác chậm chạp, cồng kềnh của nhiều ứng dụng Java. Kiến trúc phân lớp cao như vậy hoạt động chống lại thời gian chạy JIT, bất kể các lớp có thể bị lỗi kỹ thuật hay không. JIT không phải là một viên đạn ma thuật có thể tự động khắc phục các vấn đề về cấu trúc.
amon

@amon Trải nghiệm của tôi với "mã lasagna" đến từ các ứng dụng C ++ rất lớn với rất nhiều mã có từ những năm 1990, khi hệ thống phân cấp đối tượng được lồng sâu và COM là mốt. Trình biên dịch C ++ đi đến những nỗ lực khá anh hùng để xóa bỏ các hình phạt trừu tượng trong các chương trình như thế này, và bạn vẫn có thể thấy họ dành một phần đáng kể thời gian chạy đồng hồ treo tường cho các quầy hàng đường ống nhánh gián tiếp (và một đoạn đáng kể khác về lỗi I-cache) .
zwol

17

Tôi sẽ thách thức trích dẫn này:

Hầu như luôn luôn có một sự đánh đổi giữa việc viết mã có thể đọc được và viết mã trình diễn.

Đây là một tuyên bố thực sự sai lệch, và một thái độ nguy hiểm tiềm tàng. Có một số trường hợp cụ thể mà bạn phải thực hiện một sự đánh đổi, nhưng nói chung hai yếu tố là độc lập.

Một ví dụ về sự đánh đổi cần thiết là khi bạn có một thuật toán đơn giản so với một thuật toán phức tạp hơn nhưng hiệu quả hơn. Việc triển khai hashtable rõ ràng phức tạp hơn so với triển khai danh sách được liên kết, nhưng việc tra cứu sẽ chậm hơn, do đó bạn có thể phải đánh đổi sự đơn giản (là yếu tố dễ đọc) để thực hiện.

Về chi phí gọi hàm, biến thuật toán đệ quy thành phép lặp có thể có lợi ích đáng kể tùy thuộc vào thuật toán và ngôn ngữ. Nhưng đây lại là một kịch bản rất cụ thể và nói chung, tổng chi phí của các cuộc gọi chức năng sẽ không đáng kể hoặc được tối ưu hóa.

(Một số ngôn ngữ động như Python thực sự có chi phí gọi phương thức đáng kể. Nhưng nếu hiệu suất trở thành vấn đề thì có lẽ bạn không nên sử dụng Python ngay từ đầu.)

Hầu hết các nguyên tắc cho mã có thể đọc được - định dạng nhất quán, tên định danh có ý nghĩa, nhận xét phù hợp và hữu ích, v.v không có ảnh hưởng đến hiệu suất. Và một số - như sử dụng enums chứ không phải chuỗi - cũng có lợi ích hiệu suất.


5

Chi phí cuộc gọi chức năng là không quan trọng trong hầu hết các trường hợp.

Tuy nhiên, mức tăng lớn hơn từ mã nội tuyến là tối ưu hóa mã mới sau khi nội tuyến .

Ví dụ: nếu bạn gọi một hàm với một đối số không đổi, trình tối ưu hóa có thể liên tục gấp lại đối số mà nó không thể trước khi thực hiện cuộc gọi. Nếu đối số là một con trỏ hàm (hoặc lambda), trình tối ưu hóa bây giờ cũng có thể thực hiện các cuộc gọi đến lambda đó.

Đây là một lý do lớn tại sao các hàm ảo và các con trỏ hàm không hấp dẫn vì bạn hoàn toàn không thể đặt nội tuyến cho chúng trừ khi con trỏ hàm thực sự không đổi được liên tục đến trang web của cuộc gọi.


5

Giả sử hiệu suất không thành vấn đề đối với chương trình của bạn và thực sự có rất nhiều cuộc gọi, chi phí vẫn có thể hoặc không quan trọng tùy thuộc vào loại cuộc gọi.

Nếu hàm được gọi là nhỏ và trình biên dịch có thể định tuyến nó, thì chi phí về cơ bản sẽ bằng không. Trình biên dịch / triển khai ngôn ngữ hiện đại có JIT, tối ưu hóa thời gian liên kết và / hoặc hệ thống mô-đun được thiết kế để tối đa hóa khả năng cho các chức năng nội tuyến khi có lợi.

OTOH, có một chi phí không rõ ràng đối với các cuộc gọi chức năng: sự tồn tại đơn thuần của chúng có thể ức chế tối ưu hóa trình biên dịch trước và sau cuộc gọi.

Nếu trình biên dịch không thể giải thích được chức năng được gọi là gì (ví dụ: công cụ ảo / động hoặc một chức năng trong thư viện động) thì có thể phải giả định rằng hàm đó có thể có bất kỳ hiệu ứng phụ nào mà ném ra một ngoại lệ, sửa đổi trạng thái toàn cầu, hoặc thay đổi bất kỳ bộ nhớ nhìn thấy thông qua con trỏ. Trình biên dịch có thể phải lưu các giá trị tạm thời vào bộ nhớ trở lại và đọc lại chúng sau cuộc gọi. Nó sẽ không thể sắp xếp lại các hướng dẫn xung quanh cuộc gọi, vì vậy nó có thể không thể vector hóa các vòng lặp hoặc tính toán dự phòng ra khỏi các vòng lặp.

Ví dụ: nếu bạn không cần gọi một hàm trong mỗi vòng lặp:

for(int i=0; i < /* gasp! */ strlen(s); i++) x ^= s[i];

Trình biên dịch có thể biết đó là một hàm thuần túy và di chuyển nó ra khỏi vòng lặp (trong trường hợp khủng khiếp như ví dụ này thậm chí sửa thuật toán O (n ^ 2) tình cờ thành O (n)):

for(int i=0, end=strlen(s); i < end; i++) x ^= s[i];

Và sau đó thậm chí có thể viết lại vòng lặp để xử lý 4/8/16 phần tử tại một thời điểm bằng cách sử dụng các hướng dẫn rộng / SIMD.

Nhưng nếu bạn thêm một cuộc gọi vào một số mã mờ trong vòng lặp, ngay cả khi cuộc gọi không làm gì và bản thân nó rất rẻ, trình biên dịch phải giả định điều tồi tệ nhất - rằng cuộc gọi sẽ truy cập vào một biến toàn cục chỉ vào cùng một bộ nhớ là sthay đổi nội dung của nó (ngay cả khi nó constnằm trong chức năng của bạn, nó có thể không ở constbất kỳ nơi nào khác), khiến cho việc tối ưu hóa là không thể:

for(int i=0; i < strlen(s); i++) {
    x ^= s[i];
    do_nothing();
}

3

Bài báo cũ này có thể trả lời câu hỏi của bạn:

Guy Lewis Steele, Jr .. Phòng thí nghiệm AI của MIT. Ghi nhớ phòng thí nghiệm AI AIM-443. Tháng 10/2017.

Trừu tượng:

Văn hóa dân gian nói rằng các tuyên bố GOTO là "rẻ", trong khi các cuộc gọi thủ tục là "đắt tiền". Huyền thoại này phần lớn là kết quả của việc thực hiện ngôn ngữ được thiết kế kém. Sự phát triển lịch sử của huyền thoại này được xem xét. Cả hai ý tưởng lý thuyết và một triển khai hiện có đều được thảo luận để gỡ bỏ huyền thoại này. Nó cho thấy rằng việc sử dụng các cuộc gọi thủ tục không hạn chế cho phép tự do phong cách tuyệt vời. Đặc biệt, bất kỳ sơ đồ nào cũng có thể được viết dưới dạng chương trình "có cấu trúc" mà không cần đưa ra các biến phụ. Khó khăn với câu lệnh GOTO và lệnh gọi thủ tục được đặc trưng là xung đột giữa các khái niệm lập trình trừu tượng và các cấu trúc ngôn ngữ cụ thể.


12
Tôi rất nghi ngờ một bài báo cũ sẽ trả lời câu hỏi liệu "chi phí cuộc gọi chức năng có còn quan trọng trong trình biên dịch hiện đại " hay không.
Cody Grey

6
@CodyGray Tôi nghĩ rằng công nghệ trình biên dịch nên đã phát triển từ năm 1977. Vì vậy, nếu các lệnh gọi hàm có thể được thực hiện với giá rẻ vào năm 1977, chúng ta sẽ có thể thực hiện ngay bây giờ. Vì vậy, câu trả lời là không. Tất nhiên, điều này giả định rằng bạn đang sử dụng một triển khai ngôn ngữ hợp lý có thể thực hiện các công việc như nội tuyến.
Alex Vong

4
@AlexVong Dựa vào tối ưu hóa trình biên dịch năm 1977 giống như dựa vào xu hướng giá cả hàng hóa trong thời kỳ đồ đá. Mọi thứ đã thay đổi quá nhiều. Ví dụ, phép nhân được sử dụng để được thay thế bằng truy cập bộ nhớ như một thao tác rẻ hơn. Hiện tại, nó đắt hơn bởi một yếu tố rất lớn. Các cuộc gọi phương thức ảo tương đối đắt hơn nhiều so với trước đây (truy cập bộ nhớ và dự đoán sai chi nhánh), nhưng đôi khi chúng có thể được tối ưu hóa và cuộc gọi phương thức ảo có thể được thực hiện ngay cả (Java luôn luôn như vậy), vì vậy chi phí là chính xác bằng không. Không có gì như thế này vào năm 1977.
maaartinus

3
Như những người khác đã chỉ ra, nó không chỉ là những thay đổi trong công nghệ trình biên dịch đã làm mất hiệu lực nghiên cứu cũ. Nếu các trình biên dịch tiếp tục được cải thiện trong khi các kiến ​​trúc vi mô vẫn không thay đổi nhiều, thì kết luận của bài báo vẫn có hiệu lực. Nhưng điều đó đã không xảy ra. Nếu bất cứ điều gì, kiến ​​trúc vi mô đã thay đổi nhiều hơn trình biên dịch. Những điều trước đây là nhanh thì chậm, nói một cách tương đối.
Cody Grey

2
@AlexVong Để chính xác hơn về các thay đổi CPU làm cho tờ giấy đó trở nên lỗi thời: Quay trở lại năm 1977, truy cập bộ nhớ chính là một chu kỳ CPU. Ngày nay, ngay cả một truy cập đơn giản của bộ đệm L1 (!) Có độ trễ từ 3 đến 4 chu kỳ. Giờ đây, các lệnh gọi hàm khá nặng trong việc truy cập bộ nhớ (tạo khung ngăn xếp, lưu địa chỉ trả về, lưu các thanh ghi cho các biến cục bộ), điều này dễ dàng khiến chi phí của một lệnh gọi hàm đến 20 chu kỳ trở lên. Nếu chức năng của bạn chỉ sắp xếp lại các đối số của nó và có thể thêm một đối số không đổi khác để chuyển qua một cuộc gọi, thì đó gần như là 100% chi phí.
cmaster

3
  • Trong C ++, hãy cẩn thận khi thiết kế các hàm gọi các đối số sao chép, mặc định là "truyền theo giá trị". Hàm gọi trên cao do lưu các thanh ghi và các thứ khác liên quan đến khung ngăn xếp có thể bị quá tải bởi một bản sao ngoài ý muốn (và có khả năng rất đắt tiền) của một đối tượng.

  • Có những tối ưu hóa liên quan đến stack-frame mà bạn nên điều tra trước khi từ bỏ mã có nhiều yếu tố.

  • Hầu hết thời gian tôi phải đối phó với một chương trình chậm, tôi thấy việc thực hiện các thay đổi thuật toán mang lại tốc độ tăng nhanh hơn nhiều so với các lệnh gọi hàm. Ví dụ: một kỹ sư khác làm lại một trình phân tích cú pháp chứa đầy cấu trúc bản đồ. Là một phần trong đó, ông đã xóa một chỉ mục được lưu trong bộ nhớ cache từ một bản đồ sang một liên kết logic. Đó là một động thái mạnh mẽ của mã, tuy nhiên nó làm cho chương trình không thể sử dụng được do hệ số chậm 100 do thực hiện tra cứu băm cho tất cả các truy cập trong tương lai so với sử dụng chỉ mục được lưu trữ. Hồ sơ cho thấy rằng hầu hết thời gian được dành cho chức năng băm.


4
Lời khuyên đầu tiên là một chút cũ. Kể từ C ++ 11, việc di chuyển đã có thể. Đặc biệt, đối với các hàm cần sửa đổi các đối số của chúng trong nội bộ, lấy một đối số theo giá trị và sửa đổi nó tại chỗ có thể là lựa chọn hiệu quả nhất.
MSalters

@MSalters: Tôi nghĩ bạn đã nhầm "đặc biệt" với "hơn nữa" hoặc một cái gì đó. Quyết định vượt qua các bản sao hoặc tài liệu tham khảo đã có trước C ++ 11 (tho tôi biết bạn biết điều đó).
phresnel

@phresnel: Tôi nghĩ rằng tôi đã hiểu đúng. Trường hợp cụ thể mà tôi đang đề cập đến là trường hợp bạn tạo tạm thời trong trình gọi, chuyển nó sang một đối số và sau đó sửa đổi nó trong callee. Điều này là không thể trước C ++ 11, vì C ++ 03 không thể / sẽ không ràng buộc một tham chiếu không liên quan đến tạm thời ..
MSalters

@MSalters: Sau đó tôi đã hiểu nhầm nhận xét của bạn khi lần đầu tiên đọc nó. Dường như với tôi, bạn đã ám chỉ rằng trước C ++ 11, việc truyền theo giá trị không phải là điều người ta sẽ làm nếu muốn sửa đổi giá trị đã qua.
phresnel

Sự ra đời của 'di chuyển' giúp đáng kể nhất trong việc trả lại các đối tượng được xây dựng thuận tiện hơn trong chức năng so với bên ngoài và được chuyển qua tham chiếu. Trước đó, việc trả lại một đối tượng từ một hàm đã gọi một bản sao, thường là một động thái đắt tiền. Điều đó không giải quyết các đối số chức năng. Tôi cẩn thận đặt từ "thiết kế" vào bình luận vì người ta phải cấp phép rõ ràng cho trình biên dịch để 'di chuyển' vào các đối số hàm (&& cú pháp). Tôi đã có thói quen 'xóa' các nhà xây dựng sao chép để xác định những nơi làm như vậy có giá trị.
dùng2543191

3

Như những người khác nói, bạn nên đo hiệu suất của chương trình trước và có thể sẽ không tìm thấy sự khác biệt nào trong thực tế.

Tuy nhiên, từ cấp độ khái niệm, tôi nghĩ rằng tôi sẽ làm sáng tỏ một vài điều bị lẫn lộn trong câu hỏi của bạn. Đầu tiên, bạn hỏi:

Do chi phí cuộc gọi chức năng vẫn còn quan trọng trong trình biên dịch hiện đại?

Lưu ý các từ khóa "chức năng" và "trình biên dịch". Trích dẫn của bạn là khác nhau:

Hãy nhớ rằng chi phí của một cuộc gọi phương thức có thể là đáng kể, tùy thuộc vào ngôn ngữ.

Đây là nói về các phương thức , theo nghĩa hướng đối tượng.

Trong khi "hàm" và "phương thức" thường được sử dụng thay thế cho nhau, có sự khác biệt khi nói về chi phí của chúng (mà bạn đang hỏi về) và khi nói đến việc biên dịch (đó là bối cảnh bạn đưa ra).

Cụ thể, chúng ta cần biết về công văn tĩnh so với công văn động . Tôi sẽ bỏ qua sự tối ưu cho thời điểm này.

Trong một ngôn ngữ như C, chúng ta thường gọi các hàm với công văn tĩnh . Ví dụ:

int foo(int x) {
  return x + 1;
}

int bar(int y) {
  return foo(y);
}

int main() {
  return bar(42);
}

Khi trình biên dịch nhìn thấy cuộc gọi foo(y), nó biết footên mà hàm đó đang đề cập đến, vì vậy chương trình đầu ra có thể chuyển thẳng đến foohàm, khá rẻ. Đó là những gì công văn tĩnh có nghĩa.

Thay thế là công văn động , trong đó trình biên dịch không biết hàm nào đang được gọi. Ví dụ: đây là một số mã Haskell (vì tương đương C sẽ lộn xộn!):

foo x = x + 1

bar f x = f x

main = print (bar foo 42)

Ở đây barhàm đang gọi đối số của nó f, có thể là bất cứ thứ gì. Do đó, trình biên dịch không thể biên dịch barthành một lệnh nhảy nhanh, bởi vì nó không biết phải nhảy tới đâu. Thay vào đó, mã mà chúng ta tạo ra barsẽ bổ sung fđể tìm ra chức năng mà nó trỏ đến, sau đó chuyển sang nó. Đó là những gì công văn năng động có nghĩa.

Cả hai ví dụ này đều dành cho các chức năng . Bạn đã đề cập đến các phương thức , có thể được coi là một kiểu đặc biệt của hàm được gửi động. Ví dụ: đây là một số Python:

class A:
  def __init__(self, x):
    self.x = x

  def foo(self):
    return self.x + 1

def bar(y):
  return y.foo()

z = A(42)
bar(z)

Cuộc y.foo()gọi sử dụng công văn động, vì nó tìm kiếm giá trị của footài sản trong yđối tượng và gọi bất cứ thứ gì nó tìm thấy; nó không biết rằng ysẽ có lớp Ahoặc Alớp có một foophương thức, vì vậy chúng ta không thể nhảy thẳng vào nó.

OK, đó là ý tưởng cơ bản. Lưu ý rằng công văn tĩnh nhanh hơn công văn động bất kể chúng tôi biên dịch hay giải thích; tất cả những thứ khác đều bình đẳng Các cuộc hội thảo phát sinh thêm một chi phí.

Vậy làm thế nào điều này ảnh hưởng đến trình biên dịch hiện đại, tối ưu hóa?

Điều đầu tiên cần lưu ý là công văn tĩnh có thể được tối ưu hóa mạnh mẽ hơn: khi chúng ta biết chức năng nào chúng ta đang nhảy tới, có thể thực hiện những việc như nội tuyến. Với công văn động, chúng tôi không biết chúng tôi sẽ nhảy cho đến khi chạy, vì vậy chúng tôi không thể tối ưu hóa nhiều.

Thứ hai, trong một số ngôn ngữ, có thể suy ra nơi một số công văn động sẽ kết thúc, và do đó tối ưu hóa chúng thành công văn tĩnh. Điều này cho phép chúng tôi thực hiện các tối ưu hóa khác như nội tuyến, v.v.

Trong ví dụ Python ở trên, suy luận như vậy là khá vô vọng, vì Python cho phép các mã khác ghi đè lên các lớp và thuộc tính, do đó, rất khó để suy ra nhiều thứ sẽ giữ trong mọi trường hợp.

Nếu ngôn ngữ của chúng tôi cho phép chúng tôi áp đặt nhiều hạn chế hơn, ví dụ như bằng cách giới hạn ylớp Abằng cách sử dụng chú thích, thì chúng tôi có thể sử dụng thông tin đó để suy ra chức năng đích. Trong các ngôn ngữ có phân lớp (gần như tất cả các ngôn ngữ có các lớp!) Thực sự không đủ, vì ythực sự có thể có một lớp (phụ) khác, vì vậy chúng tôi cần thêm thông tin như finalchú thích của Java để biết chính xác hàm nào sẽ được gọi.

Haskell không phải là ngôn ngữ OO, nhưng chúng ta có thể suy ra giá trị của fbằng cách đặt nội tuyến bar( được gửi tĩnh ) vào main, thay thế foocho y. Vì mục tiêu của fooin mainđược biết đến tĩnh, nên cuộc gọi sẽ được gửi đi tĩnh và có thể sẽ được nội tuyến và tối ưu hóa hoàn toàn (vì các hàm này nhỏ, trình biên dịch có nhiều khả năng nội tuyến chúng hơn, mặc dù chúng ta không thể tin vào điều đó nói chung ).

Do đó, chi phí giảm xuống:

  • Liệu ngôn ngữ gửi cuộc gọi của bạn tĩnh hay động?
  • Nếu đó là ngôn ngữ thứ hai, liệu ngôn ngữ có cho phép thực hiện suy ra mục tiêu bằng cách sử dụng thông tin khác (ví dụ: các loại, lớp, chú thích, nội tuyến, v.v.) không?
  • Làm thế nào tích cực có thể gửi công văn tĩnh (suy ra hoặc cách khác)?

Nếu bạn đang sử dụng ngôn ngữ "rất năng động", với rất nhiều công văn động và một vài đảm bảo có sẵn cho trình biên dịch, thì mọi cuộc gọi sẽ phải chịu một chi phí. Nếu bạn đang sử dụng ngôn ngữ "rất tĩnh", thì trình biên dịch trưởng thành sẽ tạo ra mã rất nhanh. Nếu bạn ở giữa, thì nó có thể phụ thuộc vào phong cách mã hóa của bạn và cách triển khai thông minh.


Tôi không đồng ý rằng việc gọi một bao đóng (hoặc một số con trỏ hàm ) - như ví dụ Haskell của bạn - là công văn động. công văn động liên quan đến một số tính toán (ví dụ: sử dụng một số vtable ) để có được sự đóng cửa đó, do đó tốn kém hơn so với các cuộc gọi gián tiếp. Nếu không, câu trả lời tốt đẹp.
Basile Starynkevitch

2

Vâng, một dự đoán chi nhánh bị bỏ lỡ tốn kém hơn trên phần cứng hiện đại so với nhiều thập kỷ trước, nhưng trình biên dịch đã thông minh hơn rất nhiều trong việc tối ưu hóa điều này.

Ví dụ, xem xét Java. Thoạt nhìn, chức năng gọi qua chức năng nên đặc biệt chiếm ưu thế trong ngôn ngữ này:

  • các hàm nhỏ được phổ biến rộng rãi do quy ước JavaBean
  • các chức năng mặc định là ảo và thường là
  • đơn vị biên soạn là lớp; thời gian chạy hỗ trợ tải các lớp mới bất cứ lúc nào, bao gồm các lớp con ghi đè các phương thức đơn hình trước đó

Kinh hoàng trước những thực tiễn này, lập trình viên C trung bình sẽ dự đoán rằng Java phải chậm hơn một bậc so với C. Và 20 năm trước, anh ta đã đúng. Tuy nhiên, các điểm chuẩn hiện đại đặt mã Java thành ngữ trong một vài phần trăm của mã C tương đương. Làm thế nào là có thể?

Một lý do là hàm JVM hiện đại gọi là vấn đề tất nhiên. Nó làm như vậy bằng cách sử dụng nội tuyến đầu cơ:

  1. Mã mới được tải thực thi mà không cần tối ưu hóa. Trong giai đoạn này, đối với mọi trang web cuộc gọi, JVM theo dõi các phương thức nào thực sự được gọi.
  2. Khi mã đã được xác định là điểm nóng hiệu năng, bộ thực thi sử dụng các số liệu thống kê này để xác định đường dẫn thực thi có thể xảy ra nhất và nội tuyến đó, tiền tố đó với một nhánh có điều kiện trong trường hợp tối ưu hóa đầu cơ không áp dụng.

Đó là, mã:

int x = point.getX();

được viết lại thành

if (point.class != Point) GOTO interpreter;
x = point.x;

Và tất nhiên thời gian chạy đủ thông minh để di chuyển lên kiểm tra loại này miễn là điểm không được chỉ định hoặc bỏ qua nó nếu loại được biết đến với mã gọi.

Tóm lại, nếu ngay cả Java quản lý nội tuyến phương thức tự động, không có lý do cố hữu tại sao trình biên dịch không thể hỗ trợ tự động nội tuyến và mọi lý do để làm như vậy, vì nội tuyến rất có lợi cho các bộ xử lý hiện đại. Do đó, tôi khó có thể tưởng tượng bất kỳ trình biên dịch chính hiện đại nào không biết gì về chiến lược tối ưu hóa cơ bản nhất này và sẽ cho rằng một trình biên dịch có khả năng này trừ khi được chứng minh khác đi.


4
Không có lý do cố hữu tại sao một trình biên dịch không thể hỗ trợ tự động nội tuyến - có. Bạn đã nói về quá trình biên dịch JIT, tương đương với mã tự sửa đổi (điều mà HĐH có thể ngăn chặn vì bảo mật) và khả năng tối ưu hóa toàn bộ chương trình theo hướng dẫn hồ sơ tự động. Trình biên dịch AOT cho một ngôn ngữ cho phép liên kết động không biết đủ để làm ảo hóa và thực hiện bất kỳ cuộc gọi nào. OTOH: trình biên dịch AOT có thời gian để tối ưu hóa mọi thứ có thể, trình biên dịch JIT chỉ có thời gian để tập trung vào tối ưu hóa giá rẻ ở các điểm nóng. Trong hầu hết các trường hợp, điều đó khiến JIT gặp bất lợi nhỏ.
amon

2
Hãy cho tôi biết một hệ điều hành ngăn Google Chrome chạy "vì bảo mật" (V8 biên dịch JavaScript thành mã gốc khi chạy). Ngoài ra, muốn AOT nội tuyến không hoàn toàn là một lý do cố hữu (nó không được xác định bởi ngôn ngữ, nhưng kiến ​​trúc bạn chọn cho trình biên dịch của bạn) và trong khi liên kết động không ngăn cản AOT nội tuyến trên các đơn vị biên dịch, nó không ức chế nội tuyến trong quá trình biên dịch các đơn vị, nơi hầu hết các cuộc gọi diễn ra. Trong thực tế, nội tuyến hữu ích dễ dàng hơn trong ngôn ngữ sử dụng liên kết động ít quá mức so với Java.
meriton

4
Đáng chú ý, iOS ngăn chặn JIT cho các ứng dụng không có đặc quyền. Chrome hoặc Firefox phải sử dụng chế độ xem web do Apple cung cấp thay vì các công cụ của riêng họ. Điểm hay mặc dù AOT so với JIT là cấp độ triển khai, không phải là lựa chọn cấp độ ngôn ngữ.
amon

@meriton Windows 10 S và các hệ điều hành bảng điều khiển trò chơi video cũng có xu hướng chặn các công cụ JIT của bên thứ ba.
Damian Yerrick

2

Hãy nhớ rằng chi phí của một cuộc gọi phương thức có thể là đáng kể, tùy thuộc vào ngôn ngữ. Hầu như luôn luôn có một sự đánh đổi giữa việc viết mã có thể đọc được và viết mã trình diễn.

Thật không may, điều này phụ thuộc rất nhiều vào:

  • chuỗi công cụ biên dịch, bao gồm JIT nếu có,
  • lĩnh vực.

Trước hết, luật đầu tiên của tối ưu hóa hiệu suất là hồ sơ đầu tiên . Có nhiều miền trong đó hiệu suất của phần mềm không liên quan đến hiệu suất của toàn bộ ngăn xếp: các cuộc gọi cơ sở dữ liệu, hoạt động mạng, hoạt động của hệ điều hành, ...

Điều này không có nghĩa là hiệu suất của phần mềm hoàn toàn không liên quan, ngay cả khi nó không cải thiện độ trễ, tối ưu hóa phần mềm có thể giúp tiết kiệm năng lượng và tiết kiệm phần cứng (hoặc tiết kiệm pin cho ứng dụng di động), điều này có thể quan trọng.

Tuy nhiên, những thứ đó thường KHÔNG thể bắt mắt và thường cải tiến thuật toán để tối ưu hóa vi mô bằng một lề lớn.

Vì vậy, trước khi tối ưu hóa, bạn cần hiểu những gì bạn đang tối ưu hóa cho ... và liệu nó có xứng đáng hay không.


Bây giờ, liên quan đến hiệu suất phần mềm thuần túy, nó thay đổi rất nhiều giữa các bộ công cụ.

Có hai chi phí cho một cuộc gọi chức năng:

  • chi phí thời gian chạy,
  • chi phí thời gian biên dịch.

Chi phí thời gian chạy là khá rõ ràng; để thực hiện một chức năng, hãy gọi một số lượng công việc nhất định. Ví dụ, sử dụng C trên x86, một lệnh gọi hàm sẽ yêu cầu (1) đổ các thanh ghi vào ngăn xếp, (2) đẩy các đối số vào các thanh ghi, thực hiện cuộc gọi và sau đó (3) khôi phục các thanh ghi từ ngăn xếp. Xem tóm tắt của các quy ước gọi để xem các công việc liên quan .

Việc đổ / khôi phục thanh ghi này mất một lượng thời gian không đáng kể (hàng chục chu kỳ CPU).

Nhìn chung, dự kiến ​​chi phí này sẽ không đáng kể so với chi phí thực tế của chức năng, tuy nhiên một số mẫu phản tác dụng ở đây: getters, chức năng được bảo vệ bởi một điều kiện đơn giản, v.v ...

Do đó, ngoài các thông dịch viên , một lập trình viên sẽ hy vọng rằng trình biên dịch hoặc JIT của họ sẽ tối ưu hóa các cuộc gọi hàm không cần thiết; mặc dù hy vọng này đôi khi có thể không sinh hoa trái. Bởi vì tối ưu hóa không phải là phép thuật.

Một ưu hoa có thể phát hiện rằng một cuộc gọi chức năng là tầm thường, và nội tuyến cuộc gọi: về cơ bản, sao chép / dán cơ quan chức năng tại địa điểm cuộc gọi. Điều này không phải lúc nào cũng là tối ưu hóa tốt (có thể gây ra sự phình to) nhưng nói chung là đáng giá vì nội tuyến phơi bày bối cảnh và bối cảnh cho phép tối ưu hóa nhiều hơn.

Một ví dụ điển hình là:

void func(condition: boolean) {
    if (condition) {
        doLotsOfWork();
    }
}

void call() { func(false); }

Nếu funcđược nội tuyến, thì trình tối ưu hóa sẽ nhận ra rằng nhánh không bao giờ được thực hiện và tối ưu hóa callthành void call() {}.

Theo nghĩa đó, các lệnh gọi hàm, bằng cách ẩn thông tin khỏi trình tối ưu hóa (nếu chưa được nội tuyến), có thể ức chế một số tối ưu hóa nhất định. Các cuộc gọi chức năng ảo đặc biệt có lỗi về điều này, bởi vì ảo hóa (chứng minh chức năng nào cuối cùng được gọi trong thời gian chạy) không phải lúc nào cũng dễ dàng.


Tóm lại, lời khuyên của tôi là viết rõ ràng trước, tránh bi quan hóa thuật toán sớm (độ phức tạp hình khối hoặc vết cắn nhanh hơn), và sau đó chỉ tối ưu hóa những gì cần tối ưu hóa.


1

"Hãy nhớ rằng chi phí của một cuộc gọi phương thức có thể là đáng kể, tùy thuộc vào ngôn ngữ. Hầu như luôn luôn có sự đánh đổi giữa việc viết mã có thể đọc được và viết mã trình diễn."

Trong những điều kiện nào thì tuyên bố được trích dẫn này vẫn còn hiệu lực ngày nay với ngành công nghiệp phong phú của trình biên dịch hiện đại?

Tôi sẽ nói thẳng rằng không bao giờ. Tôi tin rằng trích dẫn là liều lĩnh để chỉ ném ra đó.

Tất nhiên tôi không nói sự thật hoàn toàn, nhưng tôi không quan tâm đến việc trung thực đến thế. Giống như trong bộ phim Ma trận đó, tôi đã quên nếu là 1 hoặc 2 hoặc 3 - Tôi nghĩ đó là người có nữ diễn viên gợi cảm người Ý với những quả dưa lớn (tôi không thực sự thích bất kỳ ai ngoài cái đầu tiên), khi oracle lady nói với Keanu Reeves, "Tôi chỉ nói với bạn những gì bạn cần nghe" , hoặc một cái gì đó cho hiệu ứng này, đó là những gì tôi muốn làm bây giờ.

Các lập trình viên không cần phải nghe điều này. Nếu họ có kinh nghiệm với trình biên dịch trong tay và báo giá phần nào có thể áp dụng cho trình biên dịch của họ, họ sẽ biết điều này và sẽ học theo cách thích hợp với điều kiện họ hiểu đầu ra hồ sơ của họ và tại sao các cuộc gọi lá nhất định là điểm nóng, thông qua đo lường. Nếu họ chưa có kinh nghiệm và chưa bao giờ lập hồ sơ mã của họ, thì đây là điều cuối cùng họ cần nghe, rằng họ nên bắt đầu thỏa hiệp một cách mê tín cách họ viết mã xuống đến mức nội tuyến mọi thứ trước khi thậm chí xác định các điểm nóng với hy vọng rằng nó sẽ trở nên hiệu quả hơn.

Dù sao, để đáp ứng chính xác hơn, nó phụ thuộc. Một số thuyền điều kiện đã được liệt kê trong số các câu trả lời tốt. Các điều kiện có thể chỉ cần chọn một ngôn ngữ là rất lớn, như C ++, sẽ phải chuyển thành động trong các cuộc gọi ảo và khi nó có thể được tối ưu hóa và theo đó trình biên dịch và thậm chí là trình liên kết, và điều đó đã đảm bảo phản hồi chi tiết để giải quyết các điều kiện trong mọi ngôn ngữ và trình biên dịch có thể có. Nhưng tôi sẽ thêm vào đầu, "ai quan tâm?" bởi vì ngay cả khi làm việc trong các lĩnh vực quan trọng về hiệu suất như raytracing, điều cuối cùng tôi sẽ bắt đầu thực hiện trước là các phương pháp nội tuyến trước khi tôi có bất kỳ phép đo nào.

Tôi tin rằng một số người trở nên quá nhiệt tình về việc đề nghị bạn không bao giờ nên thực hiện bất kỳ tối ưu hóa vi mô nào trước khi đo. Nếu tối ưu hóa cho địa phương của số tham chiếu là tối ưu hóa vi mô, thì tôi thường bắt đầu áp dụng các tối ưu hóa đó ngay từ đầu với tư duy thiết kế hướng dữ liệu trong các lĩnh vực mà tôi biết chắc chắn sẽ rất quan trọng về hiệu năng (ví dụ mã raytracing), bởi vì nếu không tôi biết tôi sẽ phải viết lại các phần lớn ngay sau khi đã làm việc trong các lĩnh vực này trong nhiều năm. Tối ưu hóa biểu diễn dữ liệu cho các lần truy cập bộ đệm thường có thể có cùng loại cải tiến hiệu suất như cải tiến thuật toán trừ khi chúng ta đang nói như thời gian bậc hai với tuyến tính.

Nhưng tôi chưa bao giờ thấy lý do chính đáng để bắt đầu nội tuyến trước các phép đo, đặc biệt là vì các trình biên dịch rất rõ ràng trong việc tiết lộ những gì có thể có lợi từ nội tuyến, nhưng không tiết lộ những gì có thể có lợi từ việc không được nội tuyến (và không thực sự có thể tạo mã nhanh hơn nếu cuộc gọi hàm không giới hạn là một trường hợp hiếm gặp, cải thiện địa phương tham chiếu cho icache cho mã nóng và đôi khi thậm chí cho phép trình tối ưu hóa thực hiện công việc tốt hơn cho đường dẫn thực hiện trường hợp phổ biế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.