Cái nào hiệu quả hơn: Trả về giá trị so với Chuyển qua tham chiếu?


77

Tôi hiện đang nghiên cứu cách viết mã C ++ hiệu quả và về vấn đề gọi hàm, một câu hỏi nảy ra trong đầu. So sánh chức năng mã giả này:

not-void function-name () {
    do-something
    return value;
}
int main () {
    ...
    arg = function-name();
    ...
}

với chức năng mã giả giống hệt này:

void function-name (not-void& arg) {
    do-something
    arg = value;
}
int main () {
    ...
    function-name(arg);
    ...
}

Phiên bản nào hiệu quả hơn và về khía cạnh nào (thời gian, bộ nhớ, v.v.)? Nếu nó phụ thuộc, thì khi nào cái đầu tiên sẽ hiệu quả hơn và khi nào cái thứ hai sẽ hiệu quả hơn?

Chỉnh sửa : Đối với ngữ cảnh, câu hỏi này được giới hạn trong các khác biệt độc lập với nền tảng phần cứng và đối với hầu hết các phần mềm. Có sự khác biệt nào về hiệu suất độc lập với máy không?

Chỉnh sửa : Tôi không biết đây là bản sao. Câu hỏi khác là so sánh việc chuyển theo tham chiếu (mã trước) với chuyển theo giá trị (bên dưới):

not-void function-name (not-void arg)

Mà không giống với câu hỏi của tôi. Trọng tâm của tôi không phải là cách tốt hơn để chuyển đối số vào một hàm. Trọng tâm của tôi là trên đó là cách tốt hơn để vượt qua ra Kết quả là cho một biến từ phạm vi bên ngoài.


12
Tại sao bạn không thử nó? Có lẽ nó phụ thuộc vào nền tảng và trình biên dịch của bạn. Làm điều đó một triệu lần và lập hồ sơ. Ngoài ra, nói chung, hãy viết mã sao cho rõ ràng nhất và chỉ lo lắng về việc tối ưu hóa nếu bạn cần tăng hiệu suất.
xaxxon

1
Hãy thử cả hai phiên bản vài triệu lần trong khi tính thời gian cuộc gọi. Làm điều đó cả khi không và có bật tối ưu hóa. Xem xét tối ưu hóa giá trị trả về và sao chép, tôi nghi ngờ bạn sẽ tìm thấy bất kỳ sự khác biệt lớn nào.
Một số lập trình viên dude


3
@Pedro: Nhờ sao chép tách và di chuyển ngữ nghĩa, có rất nhiều trường hợp, trong đó chuyển / trả về theo giá trị thực sự hiệu quả hơn.
MikeMB

7
Công việc của bạn liên quan đến việc viết mã và bạn vừa mới học về lập hồ sơ? Đi tìm hiểu cách lập hồ sơ. Điều đó sẽ giúp bạn rất nhiều hơn bất cứ điều gì trong câu hỏi này. Và nếu bạn đang sử dụng phần cứng bị hạn chế, thì không có thông tin cụ thể về thiết bị đó, không có gì ở đây sẽ được biết là đúng.
xaxxon

Câu trả lời:


28

Trước hết, hãy lưu ý rằng việc trả về một đối tượng sẽ luôn dễ đọc hơn (và rất giống về hiệu suất) so với việc chuyển nó qua tham chiếu, do đó, dự án của bạn có thể thú vị hơn khi trả về đối tượng và tăng khả năng đọc mà không có sự khác biệt quan trọng về hiệu suất . Nếu bạn muốn biết làm thế nào để có chi phí thấp nhất, vấn đề là bạn cần trả lại những gì:

  1. Nếu bạn cần trả về một đối tượng đơn giản hoặc cơ bản, hiệu suất sẽ tương tự trong cả hai trường hợp.

  2. Nếu đối tượng quá lớn và phức tạp, việc trả lại nó sẽ cần một bản sao và nó có thể chậm hơn so với việc đặt nó như một tham số được tham chiếu, nhưng tôi nghĩ nó sẽ tốn ít bộ nhớ hơn.

Dù sao thì bạn cũng phải nghĩ rằng các trình biên dịch thực hiện rất nhiều tối ưu hóa khiến cho cả hai hoạt động rất giống nhau. Xem Bản sao Elision .


2
Điều gì về sao chép tách?
juanchopanza

8
Trên thực tế, trên x86 - và bỏ qua tối ưu hóa trình biên dịch - cả hai sẽ tạo ra cùng một mã lắp ráp, bởi vì trả về các giá trị lớn hơn 1 hoặc 2? các thanh ghi được chuyển qua một vùng bộ nhớ, vùng này được cấp phát bởi người gọi và được chuyển đến callee thông qua tham số con trỏ ngầm.
MikeMB

10

Vâng, người ta phải hiểu rằng việc biên soạn không phải là một công việc dễ dàng. có nhiều cân nhắc khi trình biên dịch biên dịch mã của bạn.

Người ta không thể trả lời câu hỏi này một cách đơn giản bởi vì tiêu chuẩn C ++ không cung cấp ABI tiêu chuẩn (giao diện nhị phân trừu tượng), vì vậy mỗi trình biên dịch được phép biên dịch mã bất cứ thứ gì nó thích và bạn có thể nhận được các kết quả khác nhau trong mỗi lần biên dịch.

Ví dụ, trong một số dự án, C ++ được biên dịch thành phần mở rộng được quản lý của Microsoft CLR (C ++ / CX). vì mọi thứ đã có tham chiếu đến một đối tượng trên heap, tôi đoán không có sự khác biệt.

Câu trả lời là không đơn giản hơn cho các biên dịch không được quản lý. Tôi chợt nghĩ đến một vài câu hỏi khi nghĩ về "Liệu XXX có chạy nhanh hơn YYY không?", ví dụ:

  • Bạn có phải đối tượng deafult-xây dựng không?
  • Trình biên dịch của bạn có hỗ trợ tối ưu hóa giá trị trả về không?
  • Đối tượng của bạn có hỗ trợ ngữ nghĩa Chỉ sao chép hay vừa sao chép vừa di chuyển không?
  • Đối tượng được đóng gói theo cách dễ lây lan (ví dụ std::array) hoặc nó có con trỏ đến một cái gì đó trên heap? (vd std::vector)?

Nếu tôi đưa ra ví dụ cụ thể, tôi đoán là trên MSVC ++ và GCC, trả về std::vectortheo giá trị sẽ giống như khi chuyển nó bằng tham chiếu, vì tối ưu hóa giá trị r và sẽ nhanh hơn một chút (vài nano giây) sau đó trả về vectơ bằng cách di chuyển. ví dụ, điều này có thể hoàn toàn khác trên Clang.

cuối cùng, hồ sơ là câu trả lời đúng duy nhất ở đây.


8

Việc trả lại đối tượng nên được sử dụng trong hầu hết các trường hợp vì một sự lựa chọn được gọi là sao chép giải phóng .

Tuy nhiên, tùy thuộc vào cách hàm của bạn được dự định sử dụng, có thể tốt hơn nếu chuyển đối tượng bằng tham chiếu.

Nhìn vào std::getlineví dụ, có một std::stringtham chiếu. Hàm này được sử dụng như một điều kiện vòng lặp và tiếp tục điền vào một std::stringcho đến khi đạt đến EOF. Việc sử dụng cùng một std::stringcho phép không gian lưu trữ của std::stringđược sử dụng lại trong mỗi lần lặp vòng lặp, giảm đáng kể số lần cấp phát bộ nhớ cần được thực hiện.


5

Một số câu trả lời đã đề cập đến vấn đề này, nhưng tôi muốn nhấn mạnh vào phần chỉnh sửa

Đối với ngữ cảnh, câu hỏi này được giới hạn trong các sự khác biệt độc lập với nền tảng phần cứng và đối với hầu hết các phần mềm. Có sự khác biệt nào về hiệu suất độc lập với máy không?

Nếu đây là giới hạn của câu hỏi, câu trả lời là không có câu trả lời. Đặc tả c ++ không quy định cách thức trả về của một đối tượng hoặc một tham chiếu chuyển qua được thực thi hiệu suất khôn ngoan như thế nào, chỉ có ngữ nghĩa của những gì cả hai đều làm về mặt mã.

Do đó, một trình biên dịch có thể tự do tối ưu hóa một mã thành mã giống hệt với mã khác giả sử rằng điều này không tạo ra sự khác biệt có thể nhận thấy đối với lập trình viên.

Về vấn đề này, tôi nghĩ tốt nhất nên sử dụng cái nào trực quan nhất cho tình huống. Nếu hàm thực sự "trả về" một đối tượng là kết quả của một số tác vụ hoặc truy vấn, hãy trả về nó, trong khi nếu hàm đang thực hiện một thao tác trên một số đối tượng thuộc sở hữu của mã bên ngoài, hãy chuyển bằng tham chiếu.

Bạn không thể khái quát hiệu suất về điều này. Khi bắt đầu, hãy làm bất cứ điều gì trực quan và xem hệ thống mục tiêu và trình biên dịch của bạn tối ưu hóa nó tốt như thế nào. Nếu sau khi lập hồ sơ, bạn sẽ phát hiện ra vấn đề, hãy thay đổi nó nếu bạn cần.


4

Chúng ta không thể nói chung chung 100% vì các nền tảng khác nhau có ABI khác nhau nhưng tôi nghĩ chúng ta có thể đưa ra một số tuyên bố khá chung chung sẽ áp dụng trên hầu hết các triển khai với lưu ý rằng những điều này chủ yếu áp dụng cho các chức năng không được nội tuyến.

Trước hết chúng ta hãy xem xét các kiểu nguyên thủy. Ở mức thấp, một tham số truyền bằng tham chiếu được thực hiện bằng cách sử dụng một con trỏ trong khi các giá trị trả về nguyên thủy thường được truyền theo nghĩa đen trong các thanh ghi. Vì vậy, các giá trị trả về có khả năng hoạt động tốt hơn. Trên một số kiến ​​trúc, điều tương tự cũng áp dụng cho các cấu trúc nhỏ. Sao chép một giá trị đủ nhỏ để vừa với một hoặc hai thanh ghi là rất rẻ.

Bây giờ chúng ta hãy xem xét các giá trị trả về lớn hơn nhưng vẫn đơn giản (không có hàm tạo mặc định, hàm tạo sao chép, v.v.). Thông thường, các giá trị trả về lớn hơn được xử lý bằng cách chuyển một con trỏ hàm đến vị trí mà giá trị trả về sẽ được đặt. Copy elision cho phép biến được trả về từ hàm, biến tạm thời được sử dụng để trả về và biến trong trình gọi mà kết quả được đặt vào được hợp nhất thành một. Vì vậy, những điều cơ bản của việc chuyển sẽ giống nhau đối với truyền theo tham chiếu và giá trị trả về.

Nhìn chung đối với các kiểu nguyên thủy, tôi mong đợi các giá trị trả về sẽ tốt hơn một chút và đối với các kiểu lớn hơn nhưng vẫn đơn giản, tôi mong đợi chúng giống nhau hoặc tốt hơn trừ khi trình biên dịch của bạn rất kém trong việc sao chép.

Đối với các kiểu sử dụng các hàm tạo mặc định, các hàm tạo sao chép, v.v. mọi thứ trở nên phức tạp hơn. Nếu hàm được gọi nhiều lần thì các giá trị trả về sẽ buộc đối tượng phải được xây dựng lại mỗi lần trong khi các tham số tham chiếu có thể cho phép cấu trúc dữ liệu được sử dụng lại mà không cần tái tạo lại. Mặt khác, các tham số tham chiếu sẽ buộc một cấu trúc (có thể không cần thiết) trước khi hàm được gọi.


2

Chức năng mã giả này:

not-void function-name () {
    do-something
    return value;
}

sẽ được sử dụng tốt hơn khi giá trị trả về không yêu cầu bất kỳ sửa đổi nào đối với nó. Tham số được truyền vào chỉ được sửa đổi trong function-name. Không có thêm tài liệu tham khảo cần thiết cho nó.


chức năng mã giả giống hệt nhau:

void function-name (not-void& arg) {
    do-something
    arg = value;
}

sẽ hữu ích nếu chúng ta có một phương thức khác kiểm duyệt giá trị của cùng một biến như thế và chúng ta cần giữ các thay đổi được thực hiện cho biến bằng một trong hai lệnh gọi.

void another-function-name (not-void& arg) {
    do-something
    arg = value;
}

1

Về mặt hiệu suất, các bản sao thường đắt hơn, mặc dù sự khác biệt có thể không đáng kể đối với các đối tượng nhỏ. Ngoài ra, trình biên dịch của bạn có thể tối ưu hóa một bản sao trả lại thành một bước di chuyển, tương đương với việc chuyển một tham chiếu.

Tôi khuyên bạn không nên chuyển mục không consttham chiếu trừ khi bạn có lý do chính đáng. Sử dụng giá trị trả về (ví dụ: các hàm của tryGet()loại).

Nếu bạn muốn, bạn có thể tự đo lường sự khác biệt, như những người khác đã nói. Chạy mã thử nghiệm vài triệu lần cho cả hai phiên bản và thấy sự khác biệt.


Tôi muốn chỉ ra rằng việc không const tham chiếu sẽ có thể dẫn đến nhiều vấn đề hơn do trạng thái tiềm ẩn của tham chiếu. Bất cứ khi nào bạn thay đổi tham chiếu, chúng tôi phải đảm bảo rằng mọi trạng thái của đối số khác cũng không bị thay đổi bởi tham chiếu.
CinchBlue,

Ngoài những gì Vermillion đã nói, trình biên dịch sẽ không tối ưu hóa một bản sao trả lại thành một bước di chuyển: Sự trở lại được định nghĩa theo nghĩa di chuyển (bản sao là dự phòng). Tuy nhiên, nó có thể hoàn toàn làm sáng tỏ việc di chuyển.
MikeMB
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.