Làm thế nào nhiều chức năng gọi hiệu suất tác động?


13

Trích xuất chức năng vào các phương thức hoặc chức năng là điều bắt buộc đối với mô đun mã, khả năng đọc và khả năng tương tác, đặc biệt là trong OOP.

Nhưng điều này có nghĩa là nhiều cuộc gọi chức năng sẽ được thực hiện.

Làm thế nào để chia mã của chúng tôi thành các phương thức hoặc hàm thực sự ảnh hưởng đến hiệu suất trong các ngôn ngữ * hiện đại ?

* Những cái phổ biến nhất: C, Java, C ++, C #, Python, JavaScript, Ruby ...



1
Tất cả các ngôn ngữ thực hiện giá trị muối của nó đã được thực hiện trong vài thập kỷ nay, tôi nghĩ. IOW, chi phí chính xác là 0.
Jörg W Mittag

1
"nhiều cuộc gọi chức năng sẽ được thực hiện" thường không đúng vì nhiều cuộc gọi trong số đó sẽ được tối ưu hóa chi phí của chúng bởi các trình biên dịch / trình thông dịch khác nhau xử lý mã và nội dung của bạn. Nếu ngôn ngữ của bạn không có những loại tối ưu hóa này, tôi có thể không coi đó là hiện đại.
Ixrec

2
Nó sẽ ảnh hưởng đến hiệu suất như thế nào? Nó sẽ làm cho nó nhanh hơn, hoặc chậm hơn hoặc không thay đổi nó, tùy thuộc vào ngôn ngữ cụ thể bạn sử dụng và cấu trúc của mã thực tế và có thể trên phiên bản trình biên dịch bạn đang sử dụng và thậm chí là nền tảng nào bạn ' Đang chạy trên. Mỗi câu trả lời bạn nhận được sẽ là một số biến thể của sự không chắc chắn này, với nhiều từ hơn và nhiều bằng chứng hỗ trợ hơn.
GrandOpener

1
Tác động, nếu có, nhỏ đến mức bạn, một người, sẽ không bao giờ nhận thấy nó. Có những điều quan trọng hơn nhiều để lo lắng về. Giống như các tab nên là 5 hoặc 7 khoảng trắng.
MetaFight

Câu trả lời:


21

Có lẽ. Trình biên dịch có thể quyết định "này, chức năng này chỉ được gọi một vài lần và tôi phải tối ưu hóa tốc độ, vì vậy tôi sẽ chỉ nội tuyến chức năng này". Về cơ bản, trình biên dịch sẽ thay thế lời gọi hàm bằng phần thân của hàm. Ví dụ, mã nguồn sẽ trông như thế này.

void DoSomething()
{
   a = a + 1;
   DoSomethingElse(a);
}

void DoSomethingElse(int a)
{
   b = a + 3;
}

Trình biên dịch quyết định nội tuyến DoSomethingElsevà mã trở thành

void DoSomething()
{
   a = a + 1;
   b = a + 3;
}

Khi các chức năng không được nội tuyến, có, có một lần nhấn hiệu năng để thực hiện cuộc gọi chức năng. Tuy nhiên, đó là một cú đánh cực nhỏ mà chỉ có mã hiệu suất cực cao mới lo lắng về các lệnh gọi hàm. Và trên các loại dự án đó, mã thường được viết bằng cách lắp ráp.

Các cuộc gọi chức năng (tùy thuộc vào nền tảng) thường bao gồm một vài 10 hướng dẫn và bao gồm lưu / khôi phục ngăn xếp. Một số lệnh gọi hàm bao gồm một lệnh nhảy và trả về.

Nhưng có những thứ khác có thể ảnh hưởng đến hiệu suất gọi chức năng. Chức năng được gọi có thể không được tải vào bộ đệm của bộ xử lý, gây ra lỗi bộ nhớ cache và buộc bộ điều khiển bộ nhớ lấy chức năng từ RAM chính. Điều này có thể gây ra một cú hích lớn cho hiệu suất.

Tóm lại: các cuộc gọi chức năng có thể hoặc không thể ảnh hưởng đến hiệu suất. Cách duy nhất để nói là hồ sơ mã của bạn. Đừng cố đoán vị trí các mã chậm, bởi vì trình biên dịch và phần cứng có một số thủ thuật đáng kinh ngạc. Hồ sơ mã để có được vị trí của các điểm chậm.


1
Tôi đã thấy với các trình biên dịch hiện đại (gcc, clang) trong các tình huống mà tôi thực sự quan tâm rằng chúng tạo ra mã khá xấu cho các vòng lặp bên trong một hàm lớn . Trích xuất vòng lặp thành một hàm tĩnh không giúp ích gì vì nội tuyến. Trích xuất vòng lặp thành một chức năng bên ngoài được tạo ra trong một số trường hợp cải thiện tốc độ đáng kể (có thể đo được trong điểm chuẩn).
gnasher729

1
Tôi sẽ ủng hộ điều này và nói rằng OP nên cẩn thận về Tối ưu hóa sớm
Patrick

1
@Patrick Bingo. Nếu bạn sẽ tối ưu hóa, hãy sử dụng một hồ sơ để xem các phần chậm ở đâu. Đừng đoán. Bạn thường có thể cảm nhận được nơi các phần chậm có thể, nhưng xác nhận nó với một hồ sơ.
CHendrix

@ gnasher729 Để giải quyết vấn đề cụ thể đó, người ta sẽ cần nhiều hơn một trình lược tả - người ta cũng sẽ phải học cách đọc mã máy đã tháo rời. Mặc dù có tối ưu hóa sớm, nhưng không có thứ gọi là học sớm (ít nhất là trong phát triển phần mềm).
rwong

Bạn có thể gặp vấn đề này nếu bạn đang gọi một chức năng một triệu lần, nhưng bạn có nhiều khả năng gặp các vấn đề khác đang có tác động lớn hơn đáng kể.
Michael Shaw

5

Đây là vấn đề thực hiện trình biên dịch hoặc thời gian chạy (và các tùy chọn của nó) và không thể nói chắc chắn được.

Trong C và C ++, một số trình biên dịch sẽ gọi nội tuyến dựa trên cài đặt tối ưu hóa - điều này có thể được nhìn thấy một cách tầm thường bằng cách kiểm tra lắp ráp được tạo khi xem các công cụ như https://gcc.godbolt.org/

Các ngôn ngữ khác, như Java có điều này như là một phần của thời gian chạy. Đây là một phần của JIT và được xây dựng trong câu hỏi SO này . Nhìn sâu vào các tùy chọn JVM cho HotSpot

-XX:InlineSmallCode=n Nội tuyến một phương thức được biên dịch trước đó chỉ khi kích thước mã gốc được tạo của nó nhỏ hơn kích thước này. Giá trị mặc định thay đổi theo nền tảng mà JVM đang chạy.
-XX:MaxInlineSize=35 Kích thước mã byte tối đa của một phương thức được nội tuyến.
-XX:FreqInlineSize=n Kích thước mã byte tối đa của một phương thức được thực hiện thường xuyên sẽ được nội tuyến. Giá trị mặc định thay đổi theo nền tảng mà JVM đang chạy.

Vì vậy, có, trình biên dịch JIT HotSpot sẽ phương thức nội tuyến đáp ứng các tiêu chí nhất định.

Tác động của điều này, rất khó để xác định vì mỗi JVM (hoặc trình biên dịch) có thể làm những việc khác nhau và cố gắng trả lời bằng một nét rộng của ngôn ngữ gần như chắc chắn là sai. Tác động chỉ có thể được xác định chính xác bằng cách định hình mã trong môi trường chạy phù hợp và kiểm tra đầu ra được biên dịch.

Đây có thể được xem là một cách tiếp cận sai lầm với CPython không nội tuyến, nhưng Jython (Python chạy trong JVM) có một số cuộc gọi được nội tuyến. Tương tự như vậy, MRI Ruby không nội tuyến trong khi JRuby sẽ và ruby2c là bộ chuyển đổi cho ruby ​​vào C ... mà sau đó có thể được đặt nội tuyến hoặc không phụ thuộc vào các tùy chọn trình biên dịch C được biên dịch.

Ngôn ngữ không nội tuyến. Triển khai có thể .


5

Bạn đang tìm kiếm hiệu suất ở sai vị trí. Vấn đề với các cuộc gọi chức năng không phải là chúng có giá cao. Có một vấn đề khác. Các cuộc gọi chức năng có thể hoàn toàn miễn phí, và bạn vẫn sẽ gặp vấn đề khác này.

Đó là một chức năng giống như một thẻ tín dụng. Vì bạn có thể dễ dàng sử dụng nó, bạn có xu hướng sử dụng nó nhiều hơn có thể bạn nên. Giả sử bạn gọi nó nhiều hơn 20% so với bạn cần. Sau đó, phần mềm lớn điển hình chứa một số lớp, mỗi hàm gọi trong lớp bên dưới, vì vậy hệ số 1,2 có thể được gộp bởi số lớp. (Ví dụ: nếu có năm lớp và mỗi lớp có hệ số làm chậm là 1,2, thì hệ số làm chậm gộp là 1,2 ^ 5 hoặc 2,5.) Đây chỉ là một cách để nghĩ về nó.

Điều này không có nghĩa là bạn nên tránh các cuộc gọi chức năng. Điều đó có nghĩa là, khi mã đang hoạt động, bạn nên biết cách tìm và loại bỏ chất thải. Có rất nhiều lời khuyên tuyệt vời về cách làm điều này trên các trang web stackexchange. Điều này cho một trong những đóng góp của tôi.

THÊM: Ví dụ nhỏ. Khi tôi làm việc trong một nhóm trên phần mềm nhà máy theo dõi một loạt các đơn đặt hàng công việc hoặc "công việc". Có một chức năng JobDone(idJob)có thể cho biết nếu một công việc đã được thực hiện. Một công việc được thực hiện khi tất cả các nhiệm vụ phụ của nó được thực hiện và mỗi nhiệm vụ được thực hiện khi tất cả các hoạt động phụ của nó được thực hiện. Tất cả những điều này đã được theo dõi trong một cơ sở dữ liệu quan hệ. Một cuộc gọi đến một chức năng khác có thể trích xuất tất cả thông tin đó, JobDoneđược gọi là chức năng khác đó, đã thấy nếu công việc đã hoàn thành và ném phần còn lại đi. Sau đó mọi người có thể dễ dàng viết mã như thế này:

while(!JobDone(idJob)){
    ...
}

hoặc là

foreach(idJob in jobs){
    if (JobDone(idJob)){
        ...
    }
}

Thấy điểm nào? Hàm này rất "mạnh mẽ" và dễ gọi là nó được gọi quá nhiều. Vì vậy, vấn đề hiệu năng không phải là hướng dẫn đi vào và ra khỏi chức năng. Đó là cần phải có một cách trực tiếp hơn để biết nếu công việc đã được thực hiện. Một lần nữa, mã này có thể đã được nhúng vào hàng ngàn dòng mã vô tội. Cố gắng sửa nó trước là điều mà mọi người đều cố gắng làm, nhưng điều đó giống như cố gắng ném phi tiêu trong một căn phòng tối. Thay vào đó, những gì bạn cần là để nó chạy, và sau đó để "mã chậm" cho bạn biết nó là gì, chỉ đơn giản bằng cách dành thời gian. Cho rằng tôi sử dụng tạm dừng ngẫu nhiên .


1

Tôi nghĩ rằng nó thực sự phụ thuộc vào ngôn ngữ và chức năng. Mặc dù trình biên dịch c và c ++ có thể nội tuyến rất nhiều hàm, nhưng đây không phải là trường hợp của Python hay Java.

Mặc dù tôi không biết chi tiết cụ thể cho java (ngoại trừ mọi phương thức đều là ảo nhưng tôi khuyên bạn nên kiểm tra tài liệu tốt hơn), trong Python tôi chắc chắn rằng không có nội tuyến, không có tối ưu hóa đệ quy đuôi và các lệnh gọi hàm khá tốn kém.

Các hàm Python về cơ bản là các đối tượng thực thi (và nguyên vẹn, bạn cũng có thể định nghĩa phương thức call () để tạo một đối tượng đối tượng thành một hàm). Điều này có nghĩa là có khá nhiều chi phí để gọi họ ...

NHƯNG

khi bạn xác định các biến bên trong các hàm, trình thông dịch sử dụng LOADFAST thay vì lệnh LOAD thông thường trong mã byte, làm cho mã của bạn nhanh hơn ...

Một điều nữa là khi bạn xác định một đối tượng có thể gọi được, các mẫu như ghi nhớ là có thể và chúng có thể tăng tốc hiệu quả tính toán của bạn rất nhiều (với chi phí sử dụng nhiều bộ nhớ hơn). Về cơ bản nó luôn luôn là một sự đánh đổi. Chi phí cuộc gọi chức năng cũng phụ thuộc vào các tham số, bởi vì chúng xác định số lượng thực sự bạn phải sao chép trên ngăn xếp (do đó, trong c / c ++, thông thường là truyền các tham số lớn như cấu trúc bằng con trỏ / tham chiếu thay vì theo giá trị).

Tôi nghĩ rằng câu hỏi của bạn trong thực tế quá rộng để được trả lời hoàn toàn trên stackexchange.

Những gì tôi khuyên bạn nên làm là bắt đầu với một ngôn ngữ và nghiên cứu tài liệu nâng cao để hiểu cách gọi hàm được thực hiện bởi ngôn ngữ cụ thể đó.

Bạn sẽ ngạc nhiên bởi có bao nhiêu điều bạn sẽ học được trong quá trình này.

Nếu bạn có một vấn đề cụ thể, hãy thực hiện các phép đo / định hình và quyết định thời tiết, tốt hơn là tạo một hàm hoặc sao chép / dán mã tương đương.

Nếu bạn hỏi một câu hỏi cụ thể hơn, tôi nghĩ sẽ dễ dàng hơn để có được câu trả lời cụ thể hơn.


Trích dẫn bạn: "Tôi nghĩ rằng câu hỏi của bạn trong thực tế quá rộng để được trả lời hoàn toàn trên stackexchange." Làm thế nào tôi có thể thu hẹp nó xuống sau đó? Tôi rất thích xem một số dữ liệu thực tế thể hiện tác động của cuộc gọi chức năng trong hiệu suất. Tôi không quan tâm ngôn ngữ nào, tôi chỉ tò mò muốn xem một lời giải thích chi tiết hơn, sao lưu dữ liệu nếu có thể, như tôi đã nói.
dabadaba

Vấn đề là nó phụ thuộc vào ngôn ngữ. Trong C và C ++, nếu chức năng được nội tuyến, tác động là 0. Nếu không được nội tuyến, nó phụ thuộc vào các tham số của nó, nếu nó có trong bộ đệm hay không, v.v ...
ingframin

1

Tôi đã đo chi phí của các cuộc gọi chức năng C ++ trực tiếp và ảo trên Xenon PowerPC một thời gian trước đây .

Các hàm trong câu hỏi có một tham số duy nhất và một trả về duy nhất, do đó việc truyền tham số xảy ra trên các thanh ghi.

Tóm lại, chi phí của một cuộc gọi chức năng trực tiếp (không ảo) là khoảng 5,5 nano giây, hoặc 18 chu kỳ đồng hồ, so với một cuộc gọi chức năng nội tuyến. Tổng chi phí của một cuộc gọi chức năng ảo là 13,2 nano giây, hoặc 42 chu kỳ xung nhịp, so với nội tuyến.

Những thời gian này có thể khác nhau trên các họ bộ xử lý khác nhau. Mã kiểm tra của tôi ở đây ; bạn có thể chạy thử nghiệm tương tự trên phần cứng của bạn. Sử dụng một bộ đếm thời gian chính xác cao như RDTSC thi CFastTimer của bạn; thời gian hệ thống () không đủ chính xác.

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.