Có phải thực tế xấu khi viết mã dựa trên tối ưu hóa trình biên dịch?


99

Tôi đã học một số C ++ và thường phải trả về các đối tượng lớn từ các hàm được tạo trong hàm. Tôi biết có thông qua tham chiếu, trả về một con trỏ và trả về các giải pháp loại tham chiếu, nhưng tôi cũng đã đọc rằng trình biên dịch C ++ (và tiêu chuẩn C ++) cho phép tối ưu hóa giá trị trả về, tránh sao chép các đối tượng lớn này qua bộ nhớ, do đó tránh sao chép các đối tượng lớn này qua bộ nhớ, nhờ đó tiết kiệm thời gian và bộ nhớ của tất cả điều đó.

Bây giờ, tôi cảm thấy rằng cú pháp rõ ràng hơn nhiều khi đối tượng được trả về rõ ràng theo giá trị và trình biên dịch nói chung sẽ sử dụng RVO và làm cho quá trình hiệu quả hơn. Có phải thực tế xấu khi dựa vào tối ưu hóa này? Nó làm cho mã rõ ràng hơn và dễ đọc hơn cho người dùng, điều này cực kỳ quan trọng, nhưng tôi có nên cảnh giác khi cho rằng trình biên dịch sẽ nắm bắt cơ hội RVO?

Đây có phải là một tối ưu hóa vi mô hay là điều tôi nên ghi nhớ khi thiết kế mã của mình?


7
Để trả lời cho chỉnh sửa của bạn, đó là một tối ưu hóa vi mô bởi vì ngay cả khi bạn đã cố gắng điểm chuẩn những gì bạn kiếm được trong nano giây, bạn hầu như không thấy nó. Đối với phần còn lại, tôi quá thối trong C ++ để cung cấp cho bạn câu trả lời nghiêm ngặt về lý do tại sao nó không hoạt động. Một trong số đó nếu có thể có trường hợp khi bạn cần phân bổ động và do đó sử dụng mới / con trỏ / tham chiếu.
Walfrat

4
@Walfrat ngay cả khi các đối tượng khá lớn, theo thứ tự megabyte? Mảng của tôi có thể trở nên to lớn vì bản chất của những vấn đề tôi đang giải quyết.
Matt

6
@ Tôi không muốn. Tài liệu tham khảo / con trỏ tồn tại chính xác cho điều này. Tối ưu hóa trình biên dịch được cho là vượt quá những gì các lập trình viên nên xem xét khi xây dựng một chương trình, mặc dù có, thường là hai lần hai thế giới trùng nhau.
Neil

5
@Matt Trừ khi bạn đang làm một cái gì đó cực kỳ cụ thể, giả sử yêu cầu các nhà phát triển có kinh nghiệm 10+ trong C / hạt nhân, các tương tác phần cứng thấp bạn không cần điều đó. Nếu bạn nghĩ rằng bạn thuộc về một cái gì đó rất cụ thể, hãy chỉnh sửa bài đăng của bạn và thêm một mô tả chính xác về những gì ứng dụng của bạn phải làm (thời gian thực? Tính toán nặng? ...)
Walfrat

37
Trong trường hợp cụ thể của RVO (N) RVO của C ++, vâng, việc dựa vào tối ưu hóa này là hoàn toàn hợp lệ. Điều này là do tiêu chuẩn C ++ 17 đặc biệt bắt buộc nó xảy ra, trong các tình huống mà các trình biên dịch hiện đại đã thực hiện nó.
Caleth

Câu trả lời:


130

Sử dụng nguyên tắc ít ngạc nhiên nhất .

Có phải bạn và chỉ bao giờ bạn là người sẽ sử dụng mã này, và bạn có chắc rằng bạn cũng vậy trong 3 năm sẽ không ngạc nhiên với những gì bạn làm không?

Rồi đi thẳng.

Trong tất cả các trường hợp khác, sử dụng cách tiêu chuẩn; nếu không, bạn và đồng nghiệp của bạn sẽ gặp khó khăn trong việc tìm lỗi.

Ví dụ, đồng nghiệp của tôi đã phàn nàn về mã của tôi gây ra lỗi. Hóa ra, anh ta đã tắt đánh giá Boolean ngắn mạch trong cài đặt trình biên dịch của mình. Tôi suýt tát anh.


88
@ Không phải là quan điểm của tôi, mọi người đều dựa vào đánh giá ngắn mạch. Và bạn không cần phải suy nghĩ kỹ về nó, nó nên được bật lên. Đó là một tiêu chuẩn defacto. Có, bạn có thể thay đổi nó, nhưng bạn không nên.
Pieter B

49
"Tôi đã thay đổi cách ngôn ngữ hoạt động, và mã thối bẩn của bạn đã bị hỏng ! Arghh!" Ồ Tát sẽ là thích hợp, gửi đồng nghiệp của bạn đến đào tạo Thiền, có rất nhiều ở đó.

109
@PieterB Tôi khá chắc chắn rằng thông số kỹ thuật ngôn ngữ C C ++ đảm bảo đánh giá ngắn mạch. Vì vậy, nó không chỉ là một tiêu chuẩn de facto, đó là các tiêu chuẩn. Không có nó, bạn thậm chí không sử dụng C / C ++ nữa, nhưng một thứ đáng ngờ giống như nó: P
marcelm

47
Chỉ để tham khảo, cách tiêu chuẩn ở đây là trả về theo giá trị.
DeadMG

28
@ dan04 đúng là ở Delphi. Các bạn, đừng để bị cuốn vào ví dụ về điểm tôi đã làm. Đừng làm những điều đáng ngạc nhiên không ai khác làm.
Pieter B

81

Đối với trường hợp cụ thể này, chắc chắn chỉ cần trả về theo giá trị.

  • RVO và NRVO là những tối ưu hóa nổi tiếng và mạnh mẽ thực sự phải được thực hiện bởi bất kỳ trình biên dịch tử tế nào, ngay cả trong chế độ C ++ 03.

  • Di chuyển ngữ nghĩa đảm bảo rằng các đối tượng được di chuyển ra khỏi chức năng nếu (N) RVO không diễn ra. Điều đó chỉ hữu ích nếu đối tượng của bạn sử dụng dữ liệu động bên trong (như std::vectorvậy), nhưng điều đó thực sự sẽ xảy ra nếu nó quá lớn - tràn ngăn xếp là một rủi ro với các đối tượng tự động lớn.

  • C ++ 17 thi hành RVO. Vì vậy, đừng lo lắng, nó sẽ không biến mất trong bạn và sẽ chỉ hoàn thành việc thiết lập hoàn toàn khi trình biên dịch được cập nhật.

Và cuối cùng, việc buộc một phân bổ động bổ sung phải trả về một con trỏ hoặc buộc loại kết quả của bạn là có thể xây dựng mặc định để bạn có thể chuyển nó dưới dạng tham số đầu ra là cả hai giải pháp xấu và không thành ngữ cho một vấn đề mà bạn có thể sẽ không bao giờ có.

Chỉ cần viết mã có ý nghĩa và cảm ơn các nhà văn trình biên dịch đã tối ưu hóa chính xác mã có ý nghĩa.


9
Để giải trí, hãy xem Borland Turbo C ++ 3.0 từ năm 1990-ish xử lý RVO như thế nào . Spoiler: Về cơ bản nó hoạt động tốt.
nwp

9
Chìa khóa ở đây không phải là một số tối ưu hóa cụ thể của trình biên dịch ngẫu nhiên hay "tính năng không có giấy tờ", mà là một thứ mà trong khi các tùy chọn về mặt kỹ thuật trong một số phiên bản của tiêu chuẩn C ++, đã bị ngành công nghiệp đẩy mạnh và hầu như mọi trình biên dịch chính đã làm điều đó cho một thời gian rất dài.

7
Tối ưu hóa này không mạnh mẽ như người ta có thể muốn. Vâng, nó khá đáng tin cậy trong các trường hợp rõ ràng nhất, nhưng tìm kiếm ví dụ tại bugzilla của gcc, có rất nhiều trường hợp hầu như không rõ ràng mà nó bị bỏ sót.
Marc Glisse

62

Bây giờ, tôi cảm thấy rằng cú pháp rõ ràng hơn nhiều khi đối tượng được trả về rõ ràng theo giá trị và trình biên dịch nói chung sẽ sử dụng RVO và làm cho quá trình hiệu quả hơn. Có phải thực tế xấu khi dựa vào tối ưu hóa này? Nó làm cho mã rõ ràng hơn và dễ đọc hơn cho người dùng, điều này cực kỳ quan trọng, nhưng tôi có nên cảnh giác khi cho rằng trình biên dịch sẽ nắm bắt cơ hội RVO?

Đây không phải là một số ít được biết đến, dễ thương, tối ưu hóa vi mô mà bạn đọc trong một số blog nhỏ, ít buôn bán và sau đó bạn sẽ cảm thấy thông minh và vượt trội về việc sử dụng.

Sau C ++ 11, RVO là cách tiêu chuẩn để viết mã mã này. Nó là phổ biến, dự kiến, được dạy, được đề cập trong các cuộc đàm phán, được đề cập trong các blog, được đề cập trong tiêu chuẩn, sẽ được báo cáo như là một lỗi biên dịch nếu không được thực hiện. Trong C ++ 17, ngôn ngữ tiến thêm một bước và bắt buộc sao chép cuộc bầu chọn trong các tình huống nhất định.

Bạn hoàn toàn nên dựa vào tối ưu hóa này.

Trên hết, trả về theo giá trị chỉ dẫn đến việc đọc và quản lý mã dễ dàng hơn nhiều so với mã trả về bằng tham chiếu. Giá trị ngữ nghĩa là một điều mạnh mẽ, mà chính nó có thể dẫn đến nhiều cơ hội tối ưu hóa hơn.


3
Cảm ơn, điều này rất có ý nghĩa và phù hợp với "nguyên tắc ít ngạc nhiên nhất" được đề cập ở trên. Nó sẽ làm cho mã rất rõ ràng và dễ hiểu, và làm cho nó khó khăn hơn để gây rối với các shenanigans con trỏ.
Matt

3
@Matt Một phần lý do tôi nêu lên câu trả lời này là vì nó có đề cập đến "ngữ nghĩa giá trị". Khi bạn có thêm kinh nghiệm về C ++ (và lập trình nói chung), bạn sẽ thấy các tình huống không thường xuyên trong đó ngữ nghĩa giá trị không thể được sử dụng cho các đối tượng nhất định vì chúng có thể thay đổi và các thay đổi của chúng cần được hiển thị cho mã khác sử dụng cùng một đối tượng đó (một ví dụ về "tính đột biến được chia sẻ"). Khi những tình huống này xảy ra, các đối tượng bị ảnh hưởng sẽ cần được chia sẻ thông qua con trỏ (thông minh).
rwong

16

Tính chính xác của mã bạn viết không bao giờ nên phụ thuộc vào tối ưu hóa. Nó sẽ xuất kết quả chính xác khi được thực thi trên "máy ảo" C ++ mà họ sử dụng trong đặc tả.

Tuy nhiên, những gì bạn nói về nhiều hơn là một loại câu hỏi hiệu quả. Mã của bạn chạy tốt hơn nếu được tối ưu hóa với trình biên dịch tối ưu hóa RVO. Điều đó tốt, vì tất cả các lý do đã chỉ ra trong các câu trả lời khác.

Tuy nhiên, nếu bạn yêu cầu tối ưu hóa này (chẳng hạn như nếu trình xây dựng sao chép thực sự sẽ khiến mã của bạn bị lỗi), thì bây giờ bạn đang ở trạng thái bất chợt của trình biên dịch.

Tôi nghĩ rằng ví dụ tốt nhất về điều này trong thực tế của riêng tôi là tối ưu hóa cuộc gọi đuôi:

   int sillyAdd(int a, int b)
   {
      if (b == 0)
          return a;
      return sillyAdd(a + 1, b - 1);
   }

Đó là một ví dụ ngớ ngẩn, nhưng nó hiển thị một lệnh gọi đuôi, trong đó một hàm được gọi đệ quy ngay ở cuối hàm. Máy ảo C ++ sẽ chỉ ra rằng mã này hoạt động đúng, mặc dù tôi có thể gây ra một chút nhầm lẫn về lý do tại sao tôi lại bận tâm viết một thói quen bổ sung như vậy ngay từ đầu. Tuy nhiên, trong các triển khai thực tế của C ++, chúng tôi có một ngăn xếp và nó có không gian hạn chế. Nếu được thực hiện theo phương pháp sư phạm, chức năng này sẽ phải đẩy ít nhất b + 1các khung xếp chồng lên ngăn xếp khi nó bổ sung. Nếu tôi muốn tính toán sillyAdd(5, 7), đây không phải là một vấn đề lớn. Nếu tôi muốn tính toán sillyAdd(0, 1000000000), tôi có thể gặp rắc rối thực sự khi tạo ra StackOverflow (và không phải là loại tốt ).

Tuy nhiên, chúng ta có thể thấy rằng một khi chúng ta đạt đến dòng trả về cuối cùng đó, chúng ta thực sự đã hoàn thành mọi thứ trong khung stack hiện tại. Chúng tôi không thực sự cần phải giữ nó xung quanh. Tối ưu hóa cuộc gọi đuôi cho phép bạn "tái sử dụng" khung ngăn xếp hiện có cho chức năng tiếp theo. Theo cách này, chúng ta chỉ cần 1 khung stack, chứ không phải b+1. (Chúng ta vẫn phải thực hiện tất cả các phép cộng và phép trừ ngớ ngẩn đó, nhưng chúng không chiếm nhiều dung lượng hơn.) Thực tế, việc tối ưu hóa biến mã thành:

   int sillyAdd(int a, int b)
   {
      begin:
      if (b == 0)
          return a;
      // return sillyAdd(a + 1, b - 1);
      a = a + 1;
      b = b - 1;
      goto begin;  
   }

Trong một số ngôn ngữ, tối ưu hóa cuộc gọi đuôi được yêu cầu rõ ràng theo thông số kỹ thuật. C ++ không phải là một trong số đó. Tôi không thể dựa vào trình biên dịch C ++ để nhận ra cơ hội tối ưu hóa cuộc gọi đuôi này, trừ khi tôi đi từng trường hợp cụ thể. Với phiên bản Visual Studio của tôi, phiên bản phát hành thực hiện tối ưu hóa cuộc gọi đuôi, nhưng phiên bản gỡ lỗi thì không (theo thiết kế).

Do đó, sẽ rất tệ cho tôi khi phụ thuộc vào khả năng tính toán sillyAdd(0, 1000000000).


2
Đây là một trường hợp góc thú vị, nhưng tôi không nghĩ bạn có thể khái quát nó theo quy tắc trong đoạn đầu tiên của bạn. Giả sử tôi có một chương trình cho một thiết bị nhỏ, nó sẽ tải nếu và chỉ khi tôi sử dụng tối ưu hóa giảm kích thước của trình biên dịch - có sai không khi làm như vậy? Có vẻ khá khoa trương khi nói rằng lựa chọn hợp lệ duy nhất của tôi là viết lại nó trong trình biên dịch chương trình, đặc biệt nếu việc viết lại đó thực hiện những điều tương tự như trình tối ưu hóa để giải quyết vấn đề.
sdenham

5
@sdenham Tôi cho rằng có một căn phòng nhỏ trong cuộc tranh luận. Nếu bạn không còn viết cho "C ++", mà là viết cho "Trình biên dịch WindRiver C ++ phiên bản 3.4.1", thì tôi có thể thấy logic ở đó. Tuy nhiên, theo nguyên tắc chung, nếu bạn đang viết một cái gì đó không hoạt động đúng theo thông số kỹ thuật, thì bạn đang ở trong một loại kịch bản rất khác. Tôi biết thư viện Boost có mã như vậy, nhưng họ luôn đặt nó trong #ifdefcác khối và có sẵn một cách giải quyết tuân thủ tiêu chuẩn.
Cort Ammon

4
đó có phải là một lỗi đánh máy trong khối mã thứ hai b = b + 1không?
stib

2
Bạn có thể muốn giải thích ý của bạn về "máy ảo C ++", vì đó không phải là một thuật ngữ được sử dụng trong bất kỳ tài liệu tiêu chuẩn nào. Tôi nghĩ rằng bạn đang nói về mô hình thực thi của C ++, nhưng không hoàn toàn chắc chắn - và thuật ngữ của bạn tương tự như một "máy ảo mã byte" liên quan đến một thứ hoàn toàn khác.
Toby Speight

1
@supercat Scala cũng có cú pháp đệ quy đuôi rõ ràng. C ++ là con thú riêng của nó, nhưng tôi nghĩ đệ quy đuôi là không phổ biến đối với các ngôn ngữ phi chức năng và bắt buộc đối với các ngôn ngữ chức năng, để lại một tập hợp nhỏ các ngôn ngữ trong đó có cú pháp đệ quy đuôi rõ ràng. Dịch theo nghĩa đen là đệ quy đuôi thành các vòng lặp và đột biến rõ ràng đơn giản là một lựa chọn tốt hơn cho nhiều ngôn ngữ.
prosfilaes

8

Trong thực tế các chương trình C ++ đang mong đợi một số tối ưu hóa trình biên dịch.

Nhìn đáng chú ý vào các tiêu đề tiêu chuẩn của việc triển khai các thùng chứa tiêu chuẩn của bạn . Với GCC , bạn có thể yêu cầu biểu mẫu được xử lý trước ( g++ -C -E) và biểu diễn bên trong GIMPLE ( g++ -fdump-tree-gimplehoặc Gimple SSA với -fdump-tree-ssa) của hầu hết các tệp nguồn (đơn vị dịch thuật kỹ thuật) bằng cách sử dụng các thùng chứa. Bạn sẽ ngạc nhiên bởi số lượng tối ưu hóa được thực hiện (với g++ -O2). Vì vậy, những người triển khai các container dựa vào các tối ưu hóa (và hầu hết thời gian, người triển khai thư viện chuẩn C ++ biết tối ưu hóa điều gì sẽ xảy ra và viết triển khai container với những người trong tâm trí; đôi khi anh ta cũng sẽ viết vượt qua tối ưu hóa trong trình biên dịch đối phó với các tính năng theo yêu cầu của thư viện C ++ tiêu chuẩn).

Trong thực tế, chính tối ưu hóa trình biên dịch làm cho C ++ và các thùng chứa tiêu chuẩn của nó đủ hiệu quả. Vì vậy, bạn có thể dựa vào họ.

Và tương tự như vậy đối với trường hợp RVO được đề cập trong câu hỏi của bạn.

Tiêu chuẩn C ++ được đồng thiết kế (đáng chú ý là bằng cách thử nghiệm tối ưu hóa đủ tốt trong khi đề xuất các tính năng mới) để hoạt động tốt với các tối ưu hóa có thể.

Ví dụ, hãy xem xét chương trình dưới đây:

#include <algorithm>
#include <vector>

extern "C" bool all_positive(const std::vector<int>& v) {
  return std::all_of(v.begin(), v.end(), [](int x){return x >0;});
}

biên dịch nó với g++ -O3 -fverbose-asm -S. Bạn sẽ thấy rằng hàm được tạo không chạy bất kỳ CALLlệnh máy nào . Vì vậy, hầu hết các bước C ++ (xây dựng một lambda đóng cửa, ứng dụng lặp đi lặp lại của nó, nhận beginvà các endtrình lặp, v.v ...) đã được tối ưu hóa. Mã máy chỉ chứa một vòng lặp (không xuất hiện rõ ràng trong mã nguồn). Nếu không tối ưu hóa như vậy, C ++ 11 sẽ không thành công.

phụ lục

(thêm 31 Tháng 12 st 2017)

Xem CppCon 2017: Matt Godbolt Hồi Trình biên dịch của tôi đã làm gì cho tôi gần đây? Mở khóa cuộc trò chuyện nắp máy tính của Compiler .


4

Bất cứ khi nào bạn sử dụng trình biên dịch, sự hiểu biết là nó sẽ tạo ra mã máy hoặc mã byte cho bạn. Nó không đảm bảo bất cứ điều gì về mã được tạo ra như thế nào, ngoại trừ việc nó sẽ triển khai mã nguồn theo đặc điểm kỹ thuật của ngôn ngữ. Lưu ý rằng bảo đảm này là như nhau bất kể mức độ tối ưu hóa được sử dụng là gì, và vì vậy, nói chung, không có lý do gì để coi một đầu ra là "đúng" hơn đầu ra khác.

Hơn nữa, trong những trường hợp đó, như RVO, nơi được chỉ định trong ngôn ngữ, dường như sẽ vô nghĩa khi bạn tránh sử dụng nó, đặc biệt là nếu nó làm cho mã nguồn đơn giản hơn.

Rất nhiều nỗ lực được đưa vào để làm cho trình biên dịch tạo ra đầu ra hiệu quả, và rõ ràng mục đích là để các khả năng đó được sử dụng.

Có thể có lý do để sử dụng mã không được tối ưu hóa (ví dụ để gỡ lỗi), nhưng trường hợp được đề cập trong câu hỏi này dường như không phải là một (và nếu mã của bạn chỉ bị lỗi khi được tối ưu hóa và đó không phải là hậu quả của một số đặc thù của thiết bị bạn đang chạy nó, sau đó có một lỗi ở đâu đó và không chắc là trong trình biên dịch.)


3

Tôi nghĩ những người khác bao quát góc độ cụ thể về C ++ và RVO. Đây là một câu trả lời tổng quát hơn:

Khi nói đến tính chính xác, bạn không nên dựa vào tối ưu hóa trình biên dịch, hoặc hành vi cụ thể của trình biên dịch nói chung. May mắn thay, bạn dường như không làm điều này.

Khi nói đến hiệu suất, bạn phải dựa vào hành vi cụ thể của trình biên dịch nói chung và tối ưu hóa trình biên dịch nói riêng. Trình biên dịch tuân thủ tiêu chuẩn có thể tự do biên dịch mã của bạn theo bất kỳ cách nào nó muốn, miễn là mã được biên dịch hoạt động theo đặc tả ngôn ngữ. Và tôi không biết về bất kỳ đặc điểm kỹ thuật nào cho một ngôn ngữ chính quy định cụ thể mức độ nhanh chóng của từng thao tác.


1

Tối ưu hóa trình biên dịch chỉ nên ảnh hưởng đến hiệu suất, không phải kết quả. Dựa vào tối ưu hóa trình biên dịch để đáp ứng các yêu cầu phi chức năng không chỉ hợp lý, nó thường là lý do tại sao một trình biên dịch được chọn trên một trình biên dịch khác.

Các cờ xác định cách thức hoạt động cụ thể được thực hiện (ví dụ như điều kiện chỉ mục hoặc tràn), thường được gộp lại với tối ưu hóa trình biên dịch, nhưng không nên. Họ rõ ràng có hiệu lực kết quả tính toán.

Nếu tối ưu hóa trình biên dịch gây ra các kết quả khác nhau, đó là một lỗi - một lỗi trong trình biên dịch. Dựa vào một lỗi trong trình biên dịch, về lâu dài là một lỗi - điều gì xảy ra khi nó được sửa chữa?

Sử dụng cờ trình biên dịch thay đổi cách tính toán hoạt động nên được ghi lại tốt, nhưng được sử dụng khi cần thiết.


Thật không may, rất nhiều tài liệu biên dịch thực hiện công việc kém trong việc chỉ định những gì được hoặc không được đảm bảo trong các chế độ khác nhau. Hơn nữa, các trình biên dịch "hiện đại" dường như không biết gì về sự kết hợp của các đảm bảo mà các lập trình viên làm và không cần. Nếu một chương trình sẽ hoạt động tốt nếu x*y>ztự ý mang lại 0 hoặc 1 trong trường hợp tràn, với điều kiện là nó không có tác dụng phụ nào khác , yêu cầu lập trình viên phải ngăn chặn tràn bằng mọi giá hoặc buộc trình biên dịch đánh giá biểu thức theo cách cụ thể tối ưu hóa suy yếu không cần thiết so với việc nói rằng ...
supercat

... trình biên dịch có thể nghỉ ngơi của nó cư xử như thể x*ythúc đẩy toán hạng của nó đối với một số loại còn tùy ý (do đó cho phép hình thức treo và giảm sức mạnh đó sẽ thay đổi hành vi của một số trường hợp tràn bộ nhớ). Tuy nhiên, nhiều trình biên dịch yêu cầu các lập trình viên ngăn chặn tràn bằng mọi giá hoặc buộc các trình biên dịch cắt bớt tất cả các giá trị trung gian trong trường hợp tràn.
supercat

1

Không.

Đó là những gì tôi làm tất cả các thời gian. Nếu tôi cần truy cập một khối 16 bit tùy ý trong bộ nhớ, tôi sẽ làm điều này

void *ptr = get_pointer();
uint16_t u16;
memcpy(&u16, ptr, sizeof(u16)); // ntohs omitted for simplicity

... Và dựa vào trình biên dịch làm bất cứ điều gì có thể để tối ưu hóa đoạn mã đó. Mã này hoạt động trên ARM, i386, AMD64 và thực tế trên mọi kiến ​​trúc đơn lẻ ngoài kia. Về lý thuyết, một trình biên dịch không tối ưu hóa thực sự có thể gọi memcpy, dẫn đến hiệu suất hoàn toàn xấu, nhưng đó không phải là vấn đề đối với tôi, vì tôi sử dụng tối ưu hóa trình biên dịch.

Hãy xem xét lựa chọn thay thế:

void *ptr = get_pointer();
uint16_t *u16ptr = ptr;
uint16_t u16;
u16 = *u16ptr;  // ntohs omitted for simplicity

Mã thay thế này không hoạt động trên các máy yêu cầu căn chỉnh phù hợp, nếu get_pointer()trả về một con trỏ không liên kết. Ngoài ra, có thể có vấn đề răng cưa trong thay thế.

Sự khác biệt giữa -O2 và -O0 khi sử dụng memcpythủ thuật là rất lớn: hiệu suất tổng kiểm tra IP 3,2 Gbps so với hiệu suất tổng kiểm tra IP 67 Gbps. Trong một trật tự của sự khác biệt lớn!

Đôi khi bạn có thể cần giúp trình biên dịch. Vì vậy, ví dụ, thay vì dựa vào trình biên dịch để bỏ các vòng lặp, bạn có thể tự làm điều đó. Hoặc bằng cách triển khai thiết bị của Duff nổi tiếng , hoặc bằng một cách sạch sẽ hơn.

Hạn chế của việc dựa vào tối ưu hóa trình biên dịch là nếu bạn chạy gdb để gỡ lỗi mã của mình, bạn có thể phát hiện ra rằng rất nhiều thứ đã được tối ưu hóa. Vì vậy, bạn có thể cần phải biên dịch lại với -O0, có nghĩa là hiệu suất sẽ hoàn toàn bị thu hút khi gỡ lỗi. Tôi nghĩ rằng đây là một nhược điểm đáng để thực hiện, xem xét lợi ích của việc tối ưu hóa trình biên dịch.

Dù bạn làm gì, hãy chắc chắn rằng cách của bạn thực sự không phải là hành vi không xác định. Chắc chắn việc truy cập một số khối bộ nhớ ngẫu nhiên dưới dạng số nguyên 16 bit là hành vi không xác định do các vấn đề răng cưa và căn chỉnh.


0

Tất cả các nỗ lực ở mã hiệu quả được viết bằng bất cứ thứ gì ngoại trừ lắp ráp đều phụ thuộc rất nhiều vào tối ưu hóa trình biên dịch, bắt đầu bằng phân bổ thanh ghi hiệu quả cơ bản nhất để tránh tràn ngăn xếp thừa khắp nơi và ít nhất là tốt, nếu không xuất sắc, lựa chọn hướng dẫn. Mặt khác, chúng ta sẽ quay trở lại thập niên 80, nơi chúng ta phải đưa ra registergợi ý khắp nơi và sử dụng số lượng biến tối thiểu trong một hàm để giúp trình biên dịch C cổ xưa hoặc thậm chí sớm hơn khi gototối ưu hóa phân nhánh hữu ích.

Nếu chúng tôi không cảm thấy như chúng tôi có thể dựa vào khả năng tối ưu hóa mã của mình, tất cả chúng tôi vẫn sẽ mã hóa các đường dẫn thực thi quan trọng về hiệu năng trong lắp ráp.

Đây thực sự là một vấn đề đáng tin cậy mà bạn cảm thấy tối ưu hóa có thể được thực hiện được sắp xếp tốt nhất bằng cách định hình và xem xét các khả năng của trình biên dịch mà bạn có và thậm chí có thể tháo rời nếu có một điểm nóng mà bạn không thể tìm ra nơi trình biên dịch dường như đã không thực hiện một tối ưu hóa rõ ràng.

RVO là một cái gì đó đã có từ rất lâu đời, và, ít nhất là loại trừ các trường hợp rất phức tạp, là thứ gì đó trình biên dịch đã được áp dụng đáng tin cậy cho các lứa tuổi. Nó chắc chắn không đáng để làm việc xung quanh một vấn đề không tồn tại.

Err về phía dựa vào trình tối ưu hóa, không sợ nó

Ngược lại, tôi nói sai ở khía cạnh phụ thuộc quá nhiều vào tối ưu hóa trình biên dịch hơn là quá ít và đề xuất này đến từ một anh chàng làm việc trong các lĩnh vực rất quan trọng về hiệu suất, trong đó hiệu quả, khả năng duy trì và chất lượng cảm nhận của khách hàng là tất cả một mờ lớn. Tôi thà rằng bạn quá tin tưởng vào trình tối ưu hóa của mình và tìm thấy một số trường hợp khó hiểu mà bạn dựa quá nhiều vào việc phụ thuộc quá ít và cứ mãi lo lắng về sự sợ hãi mê tín trong suốt quãng đời còn lại. Điều đó ít nhất sẽ giúp bạn tiếp cận một hồ sơ và điều tra đúng nếu mọi thứ không được thực thi nhanh nhất có thể và có được kiến ​​thức có giá trị, không phải là mê tín, trên đường đi.

Bạn đang làm tốt để dựa vào trình tối ưu hóa. Giữ nó lên Đừng trở thành như kẻ đó bắt đầu yêu cầu nội tuyến rõ ràng mọi chức năng được gọi trong một vòng lặp trước khi thậm chí thoát khỏi nỗi sợ sai lầm về những thiếu sót của trình tối ưu hóa.

Hồ sơ

Profiling thực sự là đường vòng nhưng câu trả lời cuối cùng cho câu hỏi của bạn. Vấn đề người mới bắt đầu muốn viết mã hiệu quả thường không phải là tối ưu hóa, không phải là tối ưu hóa vì họ phát triển tất cả các loại linh cảm sai lầm về sự thiếu hiệu quả, trong khi trực quan của con người, là sai về mặt tính toán. Phát triển kinh nghiệm với trình biên dịch sẽ bắt đầu thực sự mang lại cho bạn sự đánh giá đúng đắn về không chỉ các khả năng tối ưu hóa của trình biên dịch mà bạn có thể tự tin dựa vào, mà cả các khả năng (cũng như các hạn chế) của phần cứng. Thậm chí còn có nhiều giá trị hơn trong việc tìm hiểu những gì không đáng để tối ưu hóa hơn là học những gì.


-1

Phần mềm có thể được viết bằng C ++ trên các nền tảng rất khác nhau và cho nhiều mục đích khác nhau.

Nó hoàn toàn phụ thuộc vào mục đích của phần mềm. Nếu nó dễ dàng để duy trì, mở rộng, vá, tái cấu trúc et.c. hoặc là những thứ khác quan trọng hơn, như hiệu suất, chi phí hoặc khả năng tương thích với một số phần cứng cụ thể hoặc thời gian cần thiết để phát triển.


-2

Tôi nghĩ rằng câu trả lời nhàm chán cho điều này là: 'nó phụ thuộc'.

Có phải là thực tế xấu khi viết mã dựa trên tối ưu hóa trình biên dịch có khả năng bị tắt và nơi mà lỗ hổng không được ghi lại và nơi mã được đề cập không phải là đơn vị được kiểm tra để nếu nó bị hỏng bạn có biết không? Có lẽ.

Có phải thực tế xấu khi viết mã dựa trên tối ưu hóa trình biên dịch không có khả năng bị tắt , đó là tài liệuđược kiểm tra đơn vị ? Có thể không.


-6

Trừ khi có nhiều bạn không nói với chúng tôi, đây là thực tế tồi, nhưng không phải vì lý do bạn đề xuất.

Có thể không giống như các ngôn ngữ khác mà bạn đã sử dụng trước đây, trả về giá trị của một đối tượng trong C ++ mang lại một bản sao của đối tượng. Nếu sau đó bạn sửa đổi đối tượng, bạn đang sửa đổi một đối tượng khác . Đó là, nếu tôi có Obj a; a.x=1;Obj b = a;, sau đó tôi làm b.x += 2; b.f();, thì a.xvẫn bằng 1, không phải 3.

Vì vậy, không, sử dụng một đối tượng làm giá trị thay vì tham chiếu hoặc con trỏ không cung cấp cùng chức năng và bạn có thể gặp phải các lỗi trong phần mềm của mình.

Có lẽ bạn biết điều này và nó không ảnh hưởng tiêu cực đến trường hợp sử dụng cụ thể của bạn. Tuy nhiên, dựa trên từ ngữ trong câu hỏi của bạn, có vẻ như bạn có thể không nhận thức được sự khác biệt; từ ngữ như "tạo một đối tượng trong hàm."

"tạo một đối tượng trong hàm" nghe giống như new Obj;"trả lại đối tượng theo giá trị" nghe giống nhưObj a; return a;

Obj a;Obj* a = new Obj;là những thứ rất, rất khác nhau; cái trước có thể dẫn đến hỏng bộ nhớ nếu không được sử dụng và hiểu đúng, và cái sau có thể dẫn đến rò rỉ bộ nhớ nếu không được sử dụng và hiểu đúng.


8
Tối ưu hóa giá trị trả về (RVO) là một ngữ nghĩa được xác định rõ trong đó trình biên dịch xây dựng một đối tượng được trả về một cấp trên khung ngăn xếp, đặc biệt tránh các bản sao đối tượng không cần thiết. Đây là hành vi được xác định rõ đã được hỗ trợ từ lâu trước khi được ủy quyền trong C ++ 17. Thậm chí 10 - 15 năm trước, tất cả các trình biên dịch chính đều hỗ trợ tính năng này và đã làm như vậy một cách nhất quán.

@Snowman Tôi không nói về quản lý bộ nhớ cấp thấp, vật lý và tôi không thảo luận về sự phình to hay tốc độ bộ nhớ. Như tôi đã trình bày cụ thể trong câu trả lời của mình, tôi đang nói về dữ liệu logic. Về mặt logic , việc cung cấp giá trị của một đối tượng đang tạo ra một bản sao của nó, bất kể trình biên dịch được triển khai như thế nào hoặc lắp ráp nào được sử dụng đằng sau hậu trường. Những thứ cấp thấp phía sau hậu trường là một thứ, và cấu trúc logic và hành vi của ngôn ngữ là một thứ khác; chúng có liên quan, nhưng chúng không giống nhau - cả hai nên được hiểu.
Aaron

6
câu trả lời của bạn cho biết "trả về giá trị của một đối tượng trong C ++ mang lại một bản sao của đối tượng" hoàn toàn sai trong ngữ cảnh của RVO - đối tượng được tạo trực tiếp tại vị trí gọi và không có bản sao nào được tạo. Bạn có thể kiểm tra điều này bằng cách xóa hàm tạo sao chép và trả về đối tượng được xây dựng trong returncâu lệnh vốn là một yêu cầu cho RVO. Hơn nữa, sau đó bạn tiếp tục nói về từ khóa newvà con trỏ, đó không phải là những gì RVO nói về. Tôi tin rằng bạn không hiểu câu hỏi, hoặc RVO, hoặc có thể cả hai.

-7

Pieter B hoàn toàn chính xác trong việc khuyến nghị ít kinh ngạc nhất.

Để trả lời câu hỏi cụ thể của bạn, điều này (rất có thể) có nghĩa là gì trong C ++ là bạn nên trả lại một std::unique_ptrđối tượng được xây dựng.

Lý do là điều này rõ ràng hơn đối với một nhà phát triển C ++ như những gì đang diễn ra.

Mặc dù cách tiếp cận của bạn rất có thể sẽ hiệu quả, nhưng bạn đang báo hiệu một cách hiệu quả rằng đối tượng là một loại giá trị nhỏ khi thực tế thì không. Trên hết, bạn đang vứt bỏ mọi khả năng trừu tượng hóa giao diện. Điều này có thể ổn với mục đích hiện tại của bạn nhưng thường rất hữu ích khi xử lý ma trận.

Tôi đánh giá cao rằng nếu bạn đến từ các ngôn ngữ khác, tất cả các sigils có thể gây nhầm lẫn ban đầu. Nhưng hãy cẩn thận đừng cho rằng, bằng cách không sử dụng chúng, bạn sẽ làm cho mã của mình rõ ràng hơn. Trong thực tế, điều ngược lại có khả năng là đúng.


Nhập gia tùy tục.

14
Đây không phải là một câu trả lời tốt cho các loại mà bản thân chúng không thực hiện phân bổ động. Việc OP cảm thấy điều tự nhiên trong trường hợp sử dụng của anh ta là trả về theo giá trị cho thấy các đối tượng của anh ta có thời lượng lưu trữ tự động ở phía người gọi. Đối với các đối tượng đơn giản, không quá lớn, ngay cả việc triển khai giá trị sao chép-trả lại ngây thơ sẽ là các đơn đặt hàng có cường độ nhanh hơn phân bổ động. (Nếu, mặt khác, hàm trả về một container, sau đó trả lại một unique_pointer thậm chí có thể có lợi thế so với một trình biên dịch trở lại ngây thơ theo giá trị.)
Peter A. Schneider

9
@Matt Trong trường hợp bạn không nhận ra đây không phải là cách thực hành tốt nhất. Việc phân bổ bộ nhớ và buộc ngữ nghĩa con trỏ đối với người dùng là không cần thiết.
nwp

5
Trước hết, khi sử dụng con trỏ thông minh, người ta nên trả lại std::make_unique, không phải là std::unique_ptrtrực tiếp. Thứ hai, RVO không phải là một số tối ưu hóa bí mật, cụ thể của nhà cung cấp: nó được đưa vào tiêu chuẩn. Ngay cả khi trở lại khi nó không, nó đã được hỗ trợ rộng rãi và hành vi dự kiến. Không có điểm nào là trả về một std::unique_ptrkhi con trỏ không cần thiết ở vị trí đầu tiên.

4
@Snowman: Không có "khi nó không". Mặc dù gần đây nó chỉ trở thành bắt buộc , mọi tiêu chuẩn C ++ đã từng nhận ra [N] RVO và tạo điều kiện để kích hoạt nó (ví dụ: trình biên dịch luôn được cấp phép rõ ràng để bỏ qua việc sử dụng hàm tạo sao chép trên giá trị trả về, ngay cả khi nó có tác dụng phụ có thể nhìn thấy).
Jerry Coffin
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.