Tôi đã xem Xử lý lỗi có hệ thống trong C ++ - Andrei Alexandrescu , anh ấy tuyên bố rằng Ngoại lệ trong C ++ rất chậm.
Điều này có còn đúng với C ++ 98 không?
Tôi đã xem Xử lý lỗi có hệ thống trong C ++ - Andrei Alexandrescu , anh ấy tuyên bố rằng Ngoại lệ trong C ++ rất chậm.
Điều này có còn đúng với C ++ 98 không?
Câu trả lời:
Mô hình chính được sử dụng ngày nay cho các ngoại lệ (Itanium ABI, VC ++ 64 bit) là các ngoại lệ của mô hình Zero-Cost.
Ý tưởng là thay vì mất thời gian bằng cách thiết lập một người bảo vệ và kiểm tra rõ ràng sự hiện diện của các ngoại lệ ở mọi nơi, trình biên dịch tạo ra một bảng phụ ánh xạ bất kỳ điểm nào có thể ném một ngoại lệ (Bộ đếm chương trình) vào danh sách các trình xử lý. Khi một ngoại lệ được ném ra, danh sách này sẽ được tham khảo để chọn trình xử lý phù hợp (nếu có) và ngăn xếp không được liên kết.
So với if (error)
chiến lược thông thường :
if
khi một ngoại lệ xảy raTuy nhiên, chi phí không phải là nhỏ để đo lường:
dynamic_cast
bài kiểm tra cho mỗi trình xử lý)Vì vậy, phần lớn bộ nhớ cache bị bỏ sót, và do đó không nhỏ so với mã CPU thuần túy.
Lưu ý: để biết thêm chi tiết, hãy đọc báo cáo TR18015, chương 5.4 Xử lý ngoại lệ (pdf)
Vì vậy, có, các ngoại lệ chậm trên con đường ngoại lệ , nhưng về mặt khác, chúng nhanh hơn so với kiểm tra rõ ràng ( if
chiến lược) nói chung.
Lưu ý: Andrei Alexandrescu có vẻ hỏi điều này "nhanh hơn". Cá nhân tôi đã thấy mọi thứ xoay chuyển theo cả hai cách, một số chương trình nhanh hơn với các ngoại lệ và những chương trình khác nhanh hơn với các nhánh, vì vậy thực sự có vẻ như mất khả năng tối ưu hóa trong một số điều kiện nhất định.
Có vấn đề gì không?
Tôi sẽ khẳng định nó không. Một chương trình nên được viết với tính dễ đọc chứ không phải hiệu suất (ít nhất, không phải là tiêu chí đầu tiên). Các ngoại lệ được sử dụng khi người ta cho rằng người gọi không thể hoặc sẽ không muốn xử lý lỗi ngay tại chỗ và chuyển nó lên ngăn xếp. Phần thưởng: trong C ++ 11, các ngoại lệ có thể được sắp xếp giữa các luồng bằng Thư viện chuẩn.
Mặc dù vậy, điều này khá tinh tế, tôi khẳng định rằng map::find
không nên ném nhưng tôi ổn với việc map::find
trả về một checked_ptr
cái ném nếu nỗ lực bỏ tham chiếu nó không thành công vì nó vô hiệu: trong trường hợp thứ hai, như trong trường hợp của lớp mà Alexandrescu đã giới thiệu, người gọi chọn giữa kiểm tra rõ ràng và dựa vào các ngoại lệ. Trao quyền cho người gọi mà không giao thêm trách nhiệm cho anh ta thường là một dấu hiệu của thiết kế tốt.
abort
sẽ cho phép bạn đo dấu chân kích thước nhị phân và kiểm tra xem thời gian tải / i-cache có hoạt động tương tự hay không. Tất nhiên, tốt hơn là không đánh bất kỳ abort
...
Khi câu hỏi được đăng, tôi đang trên đường đến bác sĩ, có một chiếc taxi đang đợi, vì vậy tôi chỉ có thời gian cho một bình luận ngắn. Nhưng bây giờ đã nhận xét và ủng hộ và phản đối, tôi tốt hơn nên thêm câu trả lời của riêng mình. Ngay cả khi câu trả lời của Matthieu đã là khá tốt.
Yêu cầu lại
“Tôi đã xem Xử lý lỗi có hệ thống trong C ++ - Andrei Alexandrescu , anh ấy tuyên bố rằng Ngoại lệ trong C ++ rất chậm.”
Nếu đó là những gì Andrei tuyên bố theo đúng nghĩa đen, thì anh ấy đã rất hiểu lầm, nếu không muốn nói là hoàn toàn sai lầm. Đối với một trường hợp ngoại lệ được nâng / ném ra luôn chậm so với các thao tác cơ bản khác trong ngôn ngữ, bất kể ngôn ngữ lập trình . Không chỉ trong C ++ trở lên trong C ++ mà còn trong các ngôn ngữ khác, như tuyên bố có mục đích chỉ ra.
Nói chung, hầu hết là bất kể ngôn ngữ nào, hai tính năng cơ bản của ngôn ngữ có thứ tự cấp độ chậm hơn so với phần còn lại, vì chúng dịch sang các lệnh gọi của các quy trình xử lý các cấu trúc dữ liệu phức tạp, là
ném ngoại lệ và
cấp phát bộ nhớ động.
Đáng mừng là trong C ++, người ta thường có thể tránh được cả hai mã quan trọng về thời gian.
Thật không may, không có điều đó như một bữa trưa miễn phí , ngay cả khi hiệu quả mặc định của C ++ đến khá gần. :-) Đối với hiệu quả đạt được bằng cách tránh ném ngoại lệ và cấp phát bộ nhớ động thường đạt được bằng cách mã hóa ở mức độ trừu tượng thấp hơn, sử dụng C ++ chỉ là một “C tốt hơn”. Và tính trừu tượng thấp hơn có nghĩa là “độ phức tạp” lớn hơn.
Độ phức tạp cao hơn có nghĩa là dành nhiều thời gian hơn cho việc bảo trì và ít hoặc không có lợi ích từ việc sử dụng lại mã, đó là chi phí tiền tệ thực tế, ngay cả khi khó ước tính hoặc đo lường. Tức là, với C ++, người ta có thể, nếu muốn, đánh đổi một số hiệu quả của lập trình viên để lấy hiệu quả thực thi. Làm như vậy hay không phần lớn là một quyết định về kỹ thuật và cảm nhận của bản thân, bởi vì trên thực tế, chỉ có thể dễ dàng ước tính và đo lường được lợi nhuận chứ không phải chi phí.
Có, ủy ban tiêu chuẩn hóa C ++ quốc tế đã xuất bản Báo cáo kỹ thuật về hiệu suất C ++, TR18015 .
Về cơ bản, điều đó có nghĩa là a throw
có thể mất rất nhiều thời gian ™ so với ví dụ như một int
bài tập, do việc tìm kiếm trình xử lý.
Như TR18015 thảo luận trong phần 5.4 “Ngoại lệ”, có hai chiến lược thực hiện xử lý ngoại lệ chính,
cách tiếp cận trong đó mỗi try
-block tự động thiết lập bắt ngoại lệ, để thực hiện tìm kiếm chuỗi trình xử lý động khi một ngoại lệ được đưa ra và
cách tiếp cận mà trình biên dịch tạo ra các bảng tra cứu tĩnh được sử dụng để xác định trình xử lý cho một ngoại lệ được ném.
Cách tiếp cận tổng quát và rất linh hoạt đầu tiên gần như bị bắt buộc trong Windows 32-bit, trong khi ở 64-bit và * nix-land, cách tiếp cận thứ hai hiệu quả hơn rất nhiều thường được sử dụng.
Cũng như báo cáo đó thảo luận, đối với mỗi cách tiếp cận, có ba lĩnh vực chính mà việc xử lý ngoại lệ ảnh hưởng đến hiệu quả:
try
-block,
các chức năng thông thường (cơ hội tối ưu hóa) và
throw
-biểu thức.
Về cơ bản, với phương pháp xử lý ngoại lệ động (Windows 32-bit), việc xử lý ngoại lệ có tác động đến try
các khối, chủ yếu là bất kể ngôn ngữ nào (vì điều này bị ép buộc bởi lược đồ Xử lý ngoại lệ có cấu trúc của Windows ), trong khi phương pháp tiếp cận bảng tĩnh có chi phí gần như bằng không cho try
- các khối. Thảo luận về điều này sẽ tốn nhiều không gian và nghiên cứu hơn là thực tế để có câu trả lời SO. Vì vậy, hãy xem báo cáo để biết chi tiết.
Thật không may, báo cáo từ năm 2006, đã hơi lỗi thời vào cuối năm 2012, và theo tôi biết thì không có bất cứ thứ gì mới hơn có thể so sánh được.
Một quan điểm quan trọng khác là tác động của việc sử dụng các ngoại lệ đối với hiệu suất rất khác với hiệu quả riêng biệt của các tính năng ngôn ngữ hỗ trợ, bởi vì, như báo cáo lưu ý,
“Khi xem xét việc xử lý ngoại lệ, nó phải được đối chiếu với các cách xử lý lỗi khác.”
Ví dụ:
Chi phí bảo trì do các kiểu lập trình khác nhau (tính đúng đắn)
if
Kiểm tra lỗi trang web cuộc gọi dự phòng so với kiểm tra tập trungtry
Các vấn đề về bộ đệm (ví dụ: mã ngắn hơn có thể nằm trong bộ đệm)
Báo cáo có một danh sách các khía cạnh khác nhau cần xem xét, nhưng dù sao thì cách thực tế duy nhất để có được những thông tin khó về hiệu quả thực thi có lẽ là triển khai cùng một chương trình bằng cách sử dụng ngoại lệ và không sử dụng ngoại lệ, trong một giới hạn quyết định về thời gian phát triển và với các nhà phát triển quen thuộc với từng cách, và sau đó ĐO LƯỜNG .
Tính đúng đắn hầu như luôn luôn vượt trội hơn hiệu quả.
Không có ngoại lệ, những điều sau có thể dễ dàng xảy ra:
Một số mã P có nghĩa là để lấy một tài nguyên hoặc tính toán một số thông tin.
Mã gọi C lẽ ra phải được kiểm tra thành công / thất bại, nhưng không.
Tài nguyên không tồn tại hoặc thông tin không hợp lệ được sử dụng trong mã theo sau C, gây ra tình trạng hỗn loạn chung.
Vấn đề chính là điểm (2), trong đó với lược đồ mã trả về thông thường, mã gọi C không bị buộc phải kiểm tra.
Có hai cách tiếp cận chính buộc phải kiểm tra như vậy:
Trường hợp P trực tiếp ném một ngoại lệ khi nó không thành công.
Trong đó P trả về một đối tượng mà C phải kiểm tra trước khi sử dụng giá trị chính của nó (nếu không thì là một ngoại lệ hoặc kết thúc).
Cách tiếp cận thứ hai là AFAIK, được Barton và Nackman mô tả lần đầu tiên trong cuốn sách của họ * Khoa học và Kỹ thuật C ++: Giới thiệu về Kỹ thuật và Ví dụ Nâng cao , trong đó họ giới thiệu một lớp được gọi Fallow
cho một kết quả hàm “khả thi”. Một lớp tương tự được gọi optional
hiện được cung cấp bởi thư viện Boost. Và bạn có thể dễ dàng Optional
tự mình triển khai một lớp, sử dụng std::vector
giá trị mang trong trường hợp kết quả không phải POD.
Với cách tiếp cận đầu tiên, mã gọi C không có lựa chọn nào khác ngoài việc sử dụng các kỹ thuật xử lý ngoại lệ. Tuy nhiên, với cách tiếp cận thứ hai, bản thân mã gọi C có thể quyết định xem nên thực hiện if
kiểm tra dựa trên hay xử lý ngoại lệ chung. Do đó, cách tiếp cận thứ hai hỗ trợ việc đánh đổi giữa hiệu quả thời gian lập trình và thực thi.
“Tôi muốn biết điều này có còn đúng với C ++ 98 không”
C ++ 98 là tiêu chuẩn C ++ đầu tiên. Đối với các trường hợp ngoại lệ, nó đã giới thiệu một hệ thống phân cấp tiêu chuẩn của các lớp ngoại lệ (tiếc là không hoàn hảo). Tác động chính đến hiệu suất là khả năng xảy ra các đặc tả ngoại lệ (bị loại bỏ trong C ++ 11), tuy nhiên, chúng không bao giờ được trình biên dịch Windows C ++ chính thực hiện đầy đủ. Visual C ++: Visual C ++ chấp nhận cú pháp đặc tả ngoại lệ C ++ 98, nhưng chỉ bỏ qua thông số kỹ thuật ngoại lệ.
C ++ 03 chỉ là một phiên bản kỹ thuật của C ++ 98. Điểm mới thực sự duy nhất trong C ++ 03 là khởi tạo giá trị . Không liên quan gì đến ngoại lệ.
Với các đặc tả ngoại lệ chung tiêu chuẩn C ++ 11 đã bị loại bỏ và thay thế bằng noexcept
từ khóa.
Tiêu chuẩn C ++ 11 cũng bổ sung hỗ trợ để lưu trữ và ném lại các ngoại lệ, điều này rất tốt để truyền các ngoại lệ C ++ trên các lệnh gọi lại ngôn ngữ C. Hỗ trợ này hạn chế hiệu quả cách thức lưu trữ ngoại lệ hiện tại. Tuy nhiên, theo như tôi biết thì điều đó không ảnh hưởng đến hiệu suất, ngoại trừ mức độ xử lý ngoại lệ mã mới hơn có thể dễ dàng được sử dụng hơn trên cả hai mặt của lệnh gọi lại ngôn ngữ C.
longjmp
đến trình xử lý.
try..finally
trúc có thể được thực hiện mà không cần tháo cuộn. F #, C # và Java đều triển khai try..finally
mà không cần sử dụng tính năng giải nén ngăn xếp. Bạn chỉ cần longjmp
xử lý (như tôi đã giải thích).
Bạn không bao giờ có thể khẳng định về hiệu suất trừ khi bạn chuyển đổi mã thành lắp ráp hoặc điểm chuẩn cho nó.
Đây là những gì bạn thấy: (băng ghế dự bị nhanh)
Mã lỗi không nhạy cảm với tỷ lệ xuất hiện. Các trường hợp ngoại lệ có một chút chi phí miễn là chúng không bao giờ được ném. Một khi bạn ném chúng đi, sự khốn khổ bắt đầu. Trong ví dụ này, nó được ném cho 0%, 1%, 10%, 50% và 90% các trường hợp. Khi các ngoại lệ được ném 90% thời gian, mã chậm hơn 8 lần so với trường hợp các ngoại lệ được ném 10% thời gian. Như bạn thấy, các ngoại lệ thực sự rất chậm. Không sử dụng chúng nếu chúng bị ném thường xuyên. Nếu ứng dụng của bạn không có yêu cầu về thời gian thực, hãy ném chúng đi nếu chúng rất hiếm khi xảy ra.
Bạn thấy nhiều ý kiến trái chiều về chúng. Nhưng cuối cùng, các trường hợp ngoại lệ có chậm không? Tôi không phán xét. Chỉ cần xem điểm chuẩn.
Nó phụ thuộc vào trình biên dịch.
Ví dụ, GCC được biết đến là có hiệu suất rất kém khi xử lý các ngoại lệ, nhưng điều này đã trở nên tốt hơn đáng kể trong vài năm qua.
Nhưng lưu ý rằng việc xử lý các ngoại lệ nên - như tên đã nói - là ngoại lệ chứ không phải là quy tắc trong thiết kế phần mềm của bạn. Khi bạn có một ứng dụng ném quá nhiều ngoại lệ mỗi giây đến mức nó ảnh hưởng đến hiệu suất và đây vẫn được coi là hoạt động bình thường, thì bạn nên nghĩ đến việc làm mọi thứ khác đi.
Các ngoại lệ là một cách tuyệt vời để làm cho mã dễ đọc hơn bằng cách loại bỏ tất cả các mã xử lý lỗi khó hiểu, nhưng ngay khi chúng trở thành một phần của quy trình chương trình bình thường, chúng trở nên thực sự khó theo dõi. Hãy nhớ rằng a throw
được goto catch
ngụy trang khá nhiều .
throw new Exception
này là một Java-ism. một quy tắc nên không bao giờ ném con trỏ.
Có, nhưng điều đó không quan trọng. Tại sao?
Đọc phần này:
https://blogs.msdn.com/b/ericlippert/archive/2008/09/10/vexing-exceptions.aspx
Về cơ bản, điều đó nói rằng việc sử dụng các ngoại lệ như Alexandrescu đã mô tả (giảm tốc 50 lần vì họ sử dụng catch
như else
) là sai. Điều đó đang được nói đối với ppl, những người thích làm như vậy, tôi ước C ++ 22 :) sẽ thêm một cái gì đó như:
(lưu ý rằng đây sẽ phải là ngôn ngữ cốt lõi vì về cơ bản nó là trình biên dịch tạo mã từ hiện có)
result = attempt<lexical_cast<int>>("12345"); //lexical_cast is boost function, 'attempt'
//... is the language construct that pretty much generates function from lexical_cast, generated function is the same as the original one except that fact that throws are replaced by return(and exception type that was in place of the return is placed in a result, but NO exception is thrown)...
//... By default std::exception is replaced, ofc precise configuration is possible
if (result)
{
int x = result.get(); // or result.result;
}
else
{
// even possible to see what is the exception that would have happened in original function
switch (result.exception_type())
//...
}
PS cũng lưu ý rằng ngay cả khi các ngoại lệ chậm đến mức đó ... nó không phải là vấn đề nếu bạn không dành nhiều thời gian cho phần đó của mã trong quá trình thực thi ... Ví dụ: nếu phân chia float chậm và bạn làm cho nó gấp 4 lần nhanh hơn, điều đó không thành vấn đề nếu bạn dành 0,3% thời gian của mình để thực hiện phân chia FP ...
Giống như trong silico cho biết việc triển khai của nó phụ thuộc vào, nhưng nói chung các ngoại lệ được coi là chậm đối với bất kỳ triển khai nào và không nên được sử dụng trong mã chuyên sâu về hiệu suất.
CHỈNH SỬA: Tôi không nói là không sử dụng chúng nhưng đối với mã chuyên sâu về hiệu suất, tốt nhất là nên tránh chúng.