Là truyền đối số như tham chiếu const tối ưu hóa sớm?


20

"Tối ưu hóa sớm là gốc rễ của mọi tội lỗi"

Tôi nghĩ rằng tất cả chúng ta có thể đồng ý. Và tôi rất cố gắng để tránh làm điều đó.

Nhưng gần đây tôi đã tự hỏi về việc thực hành truyền tham số bằng const Reference thay vì theo Value . Tôi đã được dạy / học rằng các đối số hàm không tầm thường (nghĩa là hầu hết các loại không nguyên thủy) tốt nhất nên được chuyển qua tham chiếu const - khá nhiều cuốn sách tôi đã đọc khuyến nghị đây là "cách thực hành tốt nhất".

Tôi vẫn không thể không tự hỏi: Trình biên dịch hiện đại và các tính năng ngôn ngữ mới có thể làm việc kỳ diệu, vì vậy kiến ​​thức tôi đã học rất có thể bị lỗi thời và tôi thực sự không bao giờ bận tâm đến hồ sơ nếu có bất kỳ sự khác biệt về hiệu suất giữa

void fooByValue(SomeDataStruct data);   

void fooByReference(const SomeDataStruct& data);

Là thực hành mà tôi đã học được - chuyển các tham chiếu const (theo mặc định cho các loại không tầm thường) - tối ưu hóa sớm?


1
Xem thêm: F.call trong Nguyên tắc cốt lõi C ++ để thảo luận về các chiến lược truyền tham số khác nhau.
amon


1
@DocBrown Câu trả lời được chấp nhận cho câu hỏi đó đề cập đến nguyên tắc ít ngạc nhiên nhất , cũng có thể áp dụng ở đây (tức là sử dụng tài liệu tham khảo const là tiêu chuẩn ngành, v.v.). Điều đó nói rằng, tôi không đồng ý rằng câu hỏi là một bản sao: Câu hỏi mà bạn đề cập đến hỏi liệu đó có phải là thông lệ xấu (nói chung) dựa vào tối ưu hóa trình biên dịch. Câu hỏi này hỏi ngược lại: Việc truyền tham chiếu const có phải là tối ưu hóa (sớm) không?
CharonX

@CharonX: nếu ai đó có thể dựa vào tối ưu hóa trình biên dịch ở đây, câu trả lời cho câu hỏi của bạn rõ ràng là "có, tối ưu hóa thủ công là không cần thiết, nó còn quá sớm". Nếu người ta không thể dựa vào nó (có thể vì bạn không biết trước trình biên dịch nào sẽ được sử dụng cho mã), câu trả lời là "đối với các đối tượng lớn hơn thì có lẽ nó không còn sớm". Vì vậy, ngay cả khi hai câu hỏi đó không bằng nhau theo nghĩa đen, thì IMHO dường như đủ giống nhau để liên kết chúng lại với nhau như là bản sao.
Doc Brown

1
@DocBrown: Vì vậy, trước khi bạn có thể khai báo nó là bản sao, hãy chỉ ra nơi trong câu hỏi nó nói rằng trình biên dịch sẽ được cho phép và có thể "tối ưu hóa" điều đó.
Ded repeatator

Câu trả lời:


49

"Tối ưu hóa sớm" không phải là về việc sử dụng tối ưu hóa sớm . Đó là về việc tối ưu hóa trước khi vấn đề được hiểu, trước khi thời gian chạy được hiểu và thường làm cho mã ít đọc hơn và ít bảo trì hơn cho các kết quả đáng ngờ.

Sử dụng "const &" thay vì truyền một đối tượng theo giá trị là một tối ưu hóa được hiểu rõ, với các hiệu ứng được hiểu rõ về thời gian chạy, thực tế không có nỗ lực và không có bất kỳ tác động xấu nào đến khả năng đọc và bảo trì. Nó thực sự cải thiện cả hai, bởi vì nó cho tôi biết rằng một cuộc gọi sẽ không sửa đổi đối tượng được truyền vào. Vì vậy, việc thêm "const &" ngay khi bạn viết mã là KHÔNG TRƯỚC.


2
Tôi đồng ý về phần "thực tế không có nỗ lực" trong câu trả lời của bạn. Nhưng tối ưu hóa sớm là trước hết về tối ưu hóa trước khi chúng là một tác động hiệu suất được đo lường đáng chú ý. Và tôi không nghĩ rằng hầu hết các lập trình viên C ++ (bao gồm cả bản thân tôi) thực hiện bất kỳ phép đo nào trước khi sử dụng const&, vì vậy tôi nghĩ rằng câu hỏi này khá hợp lý.
Doc Brown

1
Bạn đo lường trước khi tối ưu hóa để biết liệu có sự đánh đổi nào xứng đáng hay không. Với const & toàn bộ nỗ lực gõ bảy ký tự, và nó có lợi thế khác. Khi bạn không có ý định sửa đổi biến được truyền vào, đó là lợi thế ngay cả khi không có cải thiện tốc độ.
gnasher729

3
Tôi không phải là chuyên gia C, vì vậy một câu hỏi :. const& foocho biết chức năng sẽ không sửa đổi foo, vì vậy người gọi an toàn. Nhưng một giá trị được sao chép nói rằng không có chủ đề nào khác có thể thay đổi foo, vì vậy callee là an toàn. Đúng? Vì vậy, trong một ứng dụng đa luồng, câu trả lời phụ thuộc vào tính chính xác, không phải tối ưu hóa.
dùng949300

1
@DocBrown cuối cùng bạn có thể dập tắt động lực của nhà phát triển đặt const &? Nếu anh ta làm điều đó chỉ để thực hiện mà không xem xét phần còn lại, nó có thể được coi là tối ưu hóa sớm. Bây giờ nếu anh ta đặt nó bởi vì anh ta biết đó sẽ là một tham số const thì anh ta chỉ tự ghi lại mã của mình và tạo cơ hội cho trình biên dịch để tối ưu hóa, điều này tốt hơn.
Walfrat

1
@ user949300: Rất ít hàm cho phép các đối số của chúng được sửa đổi đồng thời hoặc bằng các cuộc gọi lại được sử dụng và chúng nói rõ ràng như vậy.
Ded repeatator

16

TL; DR: Chuyển qua tham chiếu const vẫn là một ý tưởng tốt trong C ++, tất cả mọi thứ được xem xét. Không phải là một tối ưu hóa sớm.

TL; DR2: Hầu hết các câu ngạn ngữ đều không có ý nghĩa, cho đến khi chúng thực hiện.


Mục đích

Câu trả lời này chỉ cố gắng mở rộng mục được liên kết trên Nguyên tắc cốt lõi C ++ (lần đầu tiên được đề cập trong bình luận của amon) một chút.

Câu trả lời này không cố gắng giải quyết vấn đề làm thế nào để suy nghĩ và áp dụng đúng các câu ngạn ngữ khác nhau được lưu hành rộng rãi trong giới lập trình viên, đặc biệt là vấn đề hòa giải giữa các kết luận hoặc bằng chứng mâu thuẫn.


Khả năng ứng dụng

Câu trả lời này chỉ áp dụng cho các lệnh gọi hàm (phạm vi lồng nhau không thể tháo rời trên cùng một luồng).

(Lưu ý bên cạnh.) Khi những thứ có thể vượt qua có thể thoát khỏi phạm vi (nghĩa là có thời gian tồn tại vượt quá phạm vi bên ngoài), điều quan trọng hơn là đáp ứng nhu cầu của ứng dụng về quản lý trọn đời đối tượng trước mọi thứ khác. Thông thường, điều này đòi hỏi sử dụng các tài liệu tham khảo cũng có khả năng quản lý trọn đời, chẳng hạn như con trỏ thông minh. Một sự thay thế có thể là sử dụng một người quản lý. Lưu ý rằng, lambda là một loại phạm vi có thể tháo rời; lambda chụp hành xử như có phạm vi đối tượng. Do đó, hãy cẩn thận với chụp lambda. Ngoài ra, hãy cẩn thận với cách lambda được thông qua - bằng bản sao hoặc bằng cách tham khảo.


Khi nào vượt qua giá trị

Đối với các giá trị là vô hướng (nguyên thủy chuẩn phù hợp với thanh ghi máy và có giá trị ngữ nghĩa) mà không cần giao tiếp bằng cách biến đổi (tham chiếu chia sẻ), hãy chuyển theo giá trị.

Đối với các tình huống trong đó callee yêu cầu nhân bản một đối tượng hoặc tổng hợp, chuyển qua giá trị, trong đó bản sao của callee đáp ứng nhu cầu cho một đối tượng nhân bản.


Khi nào vượt qua bằng cách tham khảo, vv

đối với tất cả các tình huống khác, chuyển qua con trỏ, tham chiếu, con trỏ thông minh, tay cầm (xem: thành ngữ xử lý cơ thể), v.v ... Bất cứ khi nào lời khuyên này được thực hiện, hãy áp dụng nguyên tắc chính xác như bình thường.

Những thứ (tập hợp, đối tượng, mảng, cấu trúc dữ liệu) đủ lớn về dung lượng bộ nhớ phải luôn được thiết kế để tạo điều kiện thuận lợi cho việc chuyển qua tham chiếu, vì lý do hiệu suất. Lời khuyên này chắc chắn áp dụng khi nó có hàng trăm byte trở lên. Lời khuyên này là đường biên khi nó là hàng chục byte.


Mô hình bất thường

Có những mô hình lập trình mục đích đặc biệt nặng về sao chép theo ý định. Ví dụ: xử lý chuỗi, tuần tự hóa, giao tiếp mạng, cách ly, gói thư viện của bên thứ ba, giao tiếp giữa các bộ nhớ dùng chung, v.v. mảng byte.


Làm thế nào đặc tả ngôn ngữ ảnh hưởng đến câu trả lời này, trước khi tối ưu hóa được xem xét.

Sub-TL; DR Tuyên truyền một tham chiếu sẽ không gọi mã; đi qua tham chiếu const thỏa mãn tiêu chí này. Tuy nhiên, tất cả các ngôn ngữ khác đáp ứng tiêu chí này một cách dễ dàng.

(Các lập trình viên Novice C ++ nên bỏ qua phần này hoàn toàn.)

(Phần đầu của phần này được lấy cảm hứng một phần từ câu trả lời của gnasher729. Tuy nhiên, đã có một kết luận khác.)

C ++ cho phép các hàm tạo sao chép do người dùng định nghĩa và các toán tử gán.

(Đây là (là) một sự lựa chọn táo bạo (vừa) vừa tuyệt vời vừa đáng tiếc. Nó chắc chắn là một sự khác biệt so với chuẩn mực chấp nhận được ngày nay trong thiết kế ngôn ngữ.)

Ngay cả khi lập trình viên C ++ không định nghĩa một, trình biên dịch C ++ phải tạo ra các phương thức như vậy dựa trên các nguyên tắc ngôn ngữ, và sau đó xác định xem có cần thực thi mã bổ sung khác không memcpy. Ví dụ, a class/ structcó chứa một std::vectorthành viên phải có một hàm tạo sao chép và một toán tử gán không phải là nhỏ.

Trong các ngôn ngữ khác, các hàm tạo sao chép và nhân bản đối tượng không được khuyến khích (trừ khi thực sự cần thiết và / hoặc có ý nghĩa đối với ngữ nghĩa của ứng dụng), bởi vì các đối tượng có ngữ nghĩa tham chiếu, theo thiết kế ngôn ngữ. Các ngôn ngữ này thường sẽ có cơ chế thu gom rác dựa trên khả năng tiếp cận thay vì quyền sở hữu dựa trên phạm vi hoặc tính tham chiếu.

Khi một tham chiếu hoặc con trỏ (bao gồm tham chiếu const) được truyền xung quanh trong C ++ (hoặc C), lập trình viên được đảm bảo rằng sẽ không có mã đặc biệt nào (các hàm do người dùng xác định hoặc do trình biên dịch tạo ra), ngoại trừ việc truyền giá trị địa chỉ (tham chiếu hoặc con trỏ). Đây là một sự rõ ràng về hành vi mà các lập trình viên C ++ cảm thấy thoải mái.

Tuy nhiên, bối cảnh là ngôn ngữ C ++ phức tạp không cần thiết, do đó, sự rõ ràng trong hành vi này giống như một ốc đảo (môi trường sống có thể sống sót) ở đâu đó quanh khu vực bụi hạt nhân.

Để thêm nhiều phước lành (hoặc xúc phạm), C ++ giới thiệu các tham chiếu phổ quát (giá trị r) để tạo điều kiện cho các toán tử di chuyển do người dùng xác định (hàm tạo di chuyển và toán tử gán chuyển động) có hiệu suất tốt. Điều này có lợi cho trường hợp sử dụng có liên quan cao (việc di chuyển (chuyển) các đối tượng từ thể hiện này sang thể hiện khác), bằng cách giảm nhu cầu sao chép và nhân bản sâu. Tuy nhiên, trong các ngôn ngữ khác, thật phi logic khi nói về sự di chuyển của các vật thể như vậy.


(Phần ngoài chủ đề) Một phần dành riêng cho một bài viết, "Muốn tốc độ? Vượt qua giá trị!" được viết vào khoảng năm 2009.

Bài viết đó được viết vào năm 2009 và giải thích sự biện minh thiết kế cho giá trị r trong C ++. Bài viết đó trình bày một lập luận phản biện hợp lệ cho kết luận của tôi trong phần trước. Tuy nhiên, ví dụ mã của bài viết và yêu cầu về hiệu suất đã bị từ chối.

Sub-TL; DR Việc thiết kế ngữ nghĩa giá trị r trong C ++ cho phép một ngữ nghĩa phía người dùng thanh lịch đáng ngạc nhiên trên một Sortchức năng, ví dụ. Thanh lịch này là không thể mô hình (bắt chước) trong các ngôn ngữ khác.

Một chức năng sắp xếp được áp dụng cho toàn bộ cấu trúc dữ liệu. Như đã đề cập ở trên, sẽ rất chậm nếu có nhiều sự sao chép có liên quan. Là một tối ưu hóa hiệu suất (có liên quan thực tế), một hàm sắp xếp được thiết kế để phá hủy trong khá nhiều ngôn ngữ khác ngoài C ++. Phá hủy có nghĩa là cấu trúc dữ liệu đích được sửa đổi để đạt được mục tiêu sắp xếp.

Trong C ++, người dùng có thể chọn gọi một trong hai cách triển khai: một cách thực hiện phá hủy với hiệu suất tốt hơn hoặc một cách bình thường không sửa đổi đầu vào. (Mẫu được bỏ qua cho ngắn gọn.)

/*caller specifically passes in input argument destructively*/
std::vector<T> my_sort(std::vector<T>&& input)
{
    std::vector<T> result(std::move(input)); /* destructive move */
    std::sort(result.begin(), result.end()); /* in-place sorting */
    return result; /* return-value optimization (RVO) */
}

/*caller specifically passes in read-only argument*/ 
std::vector<T> my_sort(const std::vector<T>& input)
{
    /* reuse destructive implementation by letting it work on a clone. */
    /* Several things involved; e.g. expiring temporaries as r-value */
    /* return-value optimization, etc. */
    return my_sort(std::vector<T>(input));  
}

/*caller can select which to call, by selecting r-value*/
std::vector<T> v1 = {...};
std::vector<T> v2 = my_sort(v1); /*non-destructive*/
std::vector<T> v3 = my_sort(std::move(v1)); /*v1 is gutted*/    

Bên cạnh việc sắp xếp, sự tao nhã này cũng hữu ích trong việc thực hiện thuật toán tìm trung bình phá hoại trong một mảng (ban đầu chưa được sắp xếp), bằng cách phân vùng đệ quy.

Tuy nhiên, lưu ý rằng, hầu hết các ngôn ngữ sẽ áp dụng cách tiếp cận cây tìm kiếm nhị phân cân bằng để sắp xếp, thay vì áp dụng thuật toán sắp xếp phá hủy cho mảng. Do đó, sự liên quan thực tế của kỹ thuật này không cao như nó có vẻ.


Làm thế nào tối ưu hóa trình biên dịch ảnh hưởng đến câu trả lời này

Khi nội tuyến (và cả tối ưu hóa toàn bộ chương trình / tối ưu hóa thời gian liên kết) được áp dụng trên một số mức gọi hàm, trình biên dịch có thể thấy (đôi khi triệt để) luồng dữ liệu. Khi điều này xảy ra, trình biên dịch có thể áp dụng nhiều tối ưu hóa, một số trong đó có thể loại bỏ việc tạo toàn bộ các đối tượng trong bộ nhớ. Thông thường, khi tình huống này được áp dụng, sẽ không có vấn đề gì nếu các tham số được truyền theo giá trị hoặc bằng tham chiếu const, bởi vì trình biên dịch có thể phân tích toàn diện.

Tuy nhiên, nếu hàm cấp thấp hơn gọi thứ gì đó nằm ngoài phân tích (ví dụ: thứ gì đó trong thư viện khác ngoài biên dịch hoặc biểu đồ cuộc gọi quá đơn giản), thì trình biên dịch phải tối ưu hóa phòng thủ.

Các đối tượng lớn hơn giá trị thanh ghi máy có thể được sao chép theo hướng dẫn tải / lưu trữ bộ nhớ rõ ràng hoặc bằng một cuộc gọi đến memcpychức năng đáng kính . Trên một số nền tảng, trình biên dịch tạo các lệnh SIMD để di chuyển giữa hai vị trí bộ nhớ, mỗi lệnh di chuyển hàng chục byte (16 hoặc 32).


Thảo luận về vấn đề dài dòng hoặc lộn xộn thị giác

Các lập trình viên C ++ đã quen với điều này, tức là miễn là một lập trình viên không ghét C ++, thì việc viết hoặc đọc tham chiếu const trong mã nguồn không phải là khủng khiếp.

Các phân tích lợi ích chi phí có thể đã được thực hiện nhiều lần trước đây. Tôi không biết nếu có bất kỳ nghiên cứu khoa học nào cần được trích dẫn. Tôi đoán hầu hết các phân tích sẽ là không khoa học hoặc không thể tái sản xuất.

Đây là những gì tôi tưởng tượng (không có tài liệu tham khảo bằng chứng hoặc đáng tin cậy) ...

  • Có, nó ảnh hưởng đến hiệu suất của phần mềm được viết bằng ngôn ngữ này.
  • Nếu trình biên dịch có thể hiểu mục đích của mã, thì nó có khả năng đủ thông minh để tự động hóa điều đó
  • Thật không may, trong các ngôn ngữ thiên về tính đột biến (trái ngược với độ tinh khiết của chức năng), trình biên dịch sẽ phân loại hầu hết mọi thứ là bị đột biến, do đó, việc khấu trừ tự động sẽ loại bỏ hầu hết mọi thứ là không phải là hằng
  • Chi phí tinh thần phụ thuộc vào con người; những người nhận thấy điều này là một chi phí tinh thần cao sẽ từ chối C ++ như một ngôn ngữ lập trình khả thi.

Đây là một trong những tình huống mà tôi ước mình có thể chấp nhận hai câu trả lời thay vì chỉ phải chọn một ... tiếng thở dài
CharonX

8

Trong bài báo "StructuredProgrammingWithGoToStatements" của DonaldKnuth, ông đã viết: "Các lập trình viên lãng phí rất nhiều thời gian để suy nghĩ, hoặc lo lắng về tốc độ của các phần không chính xác của chương trình của họ và những nỗ lực này có hiệu quả thực sự có tác động tiêu cực khi gỡ lỗi và bảo trì Chúng ta nên quên đi những hiệu quả nhỏ, nói khoảng 97% thời gian: tối ưu hóa sớm là gốc rễ của mọi tội lỗi. Tuy nhiên, chúng ta không nên bỏ qua cơ hội của mình trong 3% quan trọng đó. " - Tối ưu hóa sớm

Điều này không khuyên các lập trình viên sử dụng các kỹ thuật chậm nhất hiện có. Đó là về việc tập trung vào sự rõ ràng khi viết chương trình. Thông thường, sự rõ ràng và hiệu quả là một sự đánh đổi: nếu bạn phải chọn chỉ một, hãy chọn sự rõ ràng. Nhưng nếu bạn có thể đạt được cả hai cách dễ dàng, thì không cần phải làm tê liệt sự rõ ràng (như báo hiệu rằng một cái gì đó là một hằng số) chỉ để tránh hiệu quả.


3
"nếu bạn phải chọn chỉ một, chọn rõ ràng." Thứ hai nên được ưu tiên thay thế, vì bạn có thể buộc phải chọn cái khác.
Ded repeatator

@Ded repeatator Cảm ơn bạn. Tuy nhiên, trong bối cảnh của OP, lập trình viên có quyền tự do lựa chọn.
Lawrence

Câu trả lời của bạn đọc chung chung hơn một chút so với điều đó ...
Ded repeatator

@Ded repeatator Ah, nhưng bối cảnh câu trả lời của tôi là (cũng) mà lập trình viên chọn. Nếu sự lựa chọn bị ép buộc đối với lập trình viên, thì đó sẽ không phải là "bạn" mà là lựa chọn :). Tôi đã xem xét sự thay đổi mà bạn đề xuất và sẽ không phản đối bạn chỉnh sửa câu trả lời của tôi cho phù hợp, nhưng tôi thích từ ngữ hiện tại cho sự rõ ràng của nó.
Lawrence

7

Chuyển qua tham chiếu ([const] [rvalue]) | (giá trị) phải là về ý định và lời hứa được thực hiện bởi giao diện. Nó không có gì để làm với hiệu suất.

Quy tắc ngón tay cái của Richy:

void foo(X x);          // I intend to own the x you gave me, whether by copy, move or direct initialisation on the call stack.     

void foo(X&& x);        // I intend to steal x from you. Do not use it other than to re-assign to it after calling me.

void foo(X const& x);   // I guarantee not to change your x

void foo(X& x);         // I may modify your x and I will leave it in a defined state

3

Về mặt lý thuyết, câu trả lời nên có. Và trên thực tế, đôi khi cũng vậy - vì thực tế, việc chuyển qua tham chiếu const thay vì chỉ truyền một giá trị có thể là một sự bi quan, ngay cả trong trường hợp giá trị được truyền quá lớn để phù hợp với một đăng ký (hoặc hầu hết các heuristic khác mọi người cố gắng sử dụng để xác định khi nào vượt qua giá trị hay không). Nhiều năm trước, David Abrahams đã viết một bài báo có tên "Muốn tốc độ? Vượt qua giá trị!" bao gồm một số trường hợp này. Nó không còn dễ tìm, nhưng nếu bạn có thể tìm thấy một bản sao thì nó đáng để đọc (IMO).

Tuy nhiên, trong trường hợp cụ thể chuyển qua tham chiếu const, tôi muốn nói thành ngữ được thiết lập tốt đến mức tình huống ít nhiều bị đảo ngược: trừ khi bạn biết loại sẽ là char/ short/ int/ long, mọi người sẽ thấy nó được truyền qua const theo tham chiếu theo mặc định, vì vậy có lẽ tốt nhất là đi cùng với điều đó trừ khi bạn có một lý do khá cụ thể để làm khá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.