Cảnh báo: Câu hỏi bạn đã hỏi thực sự khá phức tạp - có thể nhiều hơn những gì bạn nhận ra. Kết quả là, đây là một câu trả lời thực sự dài.
Từ một quan điểm lý thuyết thuần túy, có lẽ có một câu trả lời đơn giản cho điều này: (có lẽ) không có gì về C # thực sự ngăn cản nó nhanh như C ++. Tuy nhiên, mặc dù lý thuyết, có một số lý do thực tế rằng nó là chậm hơn ở một số điều dưới một số trường hợp.
Tôi sẽ xem xét ba lĩnh vực khác biệt cơ bản: tính năng ngôn ngữ, thực thi máy ảo và thu gom rác. Hai cái sau thường đi cùng nhau, nhưng có thể độc lập, vì vậy tôi sẽ xem xét chúng một cách riêng biệt.
Tính năng ngôn ngữ
C ++ chú trọng rất nhiều vào các khuôn mẫu và các tính năng trong hệ thống mẫu phần lớn nhằm mục đích cho phép thực hiện nhiều nhất có thể tại thời điểm biên dịch, vì vậy theo quan điểm của chương trình, chúng là "tĩnh". Lập trình meta mẫu cho phép thực hiện các phép tính hoàn toàn tùy ý tại thời điểm biên dịch (tức là hệ thống mẫu đã hoàn chỉnh Turing). Như vậy, về cơ bản bất kỳ thứ gì không phụ thuộc vào đầu vào từ người dùng đều có thể được tính toán tại thời điểm biên dịch, vì vậy trong thời gian chạy, nó chỉ đơn giản là một hằng số. Tuy nhiên, đầu vào cho điều này có thể bao gồm những thứ như thông tin kiểu, vì vậy phần lớn những gì bạn thực hiện thông qua phản chiếu trong thời gian chạy trong C # thường được thực hiện tại thời điểm biên dịch thông qua lập trình siêu mẫu trong C ++. Mặc dù vậy, chắc chắn có sự đánh đổi giữa tốc độ thời gian chạy và tính linh hoạt - những gì các mẫu có thể làm,
Sự khác biệt về đặc điểm ngôn ngữ có nghĩa là hầu hết mọi nỗ lực so sánh hai ngôn ngữ chỉ đơn giản bằng cách chuyển một số C # sang C ++ (hoặc ngược lại) đều có khả năng tạo ra kết quả ở đâu đó giữa vô nghĩa và sai lệch (và điều này cũng đúng với hầu hết các cặp ngôn ngữ khác cũng). Thực tế đơn giản là đối với bất kỳ thứ gì lớn hơn một vài dòng mã, hầu như không ai có khả năng sử dụng các ngôn ngữ theo cùng một cách (hoặc đủ gần với cùng một cách) mà so sánh như vậy cho bạn biết bất cứ điều gì về cách các ngôn ngữ đó làm việc trong cuộc sống thực.
Máy ảo
Giống như hầu hết mọi máy ảo hiện đại hợp lý, Microsoft dành cho .NET có thể và sẽ thực hiện biên dịch JIT (hay còn gọi là "động"). Tuy nhiên, điều này đại diện cho một số sự đánh đổi.
Về cơ bản, tối ưu hóa mã (giống như hầu hết các vấn đề tối ưu hóa khác) phần lớn là một vấn đề hoàn chỉnh NP. Đối với bất kỳ thứ gì ngoại trừ một chương trình đồ chơi / tầm thường, bạn gần như được đảm bảo rằng bạn sẽ không thực sự "tối ưu hóa" kết quả (tức là bạn sẽ không tìm thấy giá trị tối ưu thực sự) - trình tối ưu hóa sẽ đơn giản làm cho mã tốt hơn nó trước đây. Tuy nhiên, khá nhiều tối ưu hóa đã được biết đến rộng rãi, cần một lượng thời gian đáng kể (và thường là bộ nhớ) để thực thi. Với trình biên dịch JIT, người dùng đang đợi trong khi trình biên dịch chạy. Hầu hết các kỹ thuật tối ưu hóa đắt tiền hơn đều bị loại trừ. Biên dịch tĩnh có hai ưu điểm: trước hết, nếu nó chậm (ví dụ: xây dựng một hệ thống lớn), nó thường được thực hiện trên một máy chủ và không aidành thời gian chờ đợi nó. Thứ hai, một tệp thực thi có thể được tạo một lần và được nhiều người sử dụng nhiều lần. Đầu tiên giảm thiểu chi phí tối ưu hóa; lần thứ hai phân bổ chi phí nhỏ hơn nhiều so với số lần thực hiện lớn hơn nhiều.
Như đã đề cập trong câu hỏi ban đầu (và nhiều trang web khác) Việc biên dịch JIT có khả năng nâng cao nhận thức về môi trường mục tiêu, điều này nên (ít nhất là về mặt lý thuyết) bù đắp lợi thế này. Không nghi ngờ gì rằng yếu tố này có thể bù đắp ít nhất một phần nhược điểm của biên dịch tĩnh. Đối với một số loại mã và môi trường đích khá cụ thể, nó có thểthậm chí còn vượt trội hơn những lợi thế của biên dịch tĩnh, đôi khi khá đáng kể. Tuy nhiên, ít nhất là trong thử nghiệm và kinh nghiệm của tôi, điều này khá bất thường. Các tối ưu hóa phụ thuộc vào mục tiêu hầu hết dường như tạo ra sự khác biệt khá nhỏ hoặc chỉ có thể được áp dụng (tự động, dù sao) cho các loại vấn đề khá cụ thể. Lần hiển nhiên điều này sẽ xảy ra nếu bạn đang chạy một chương trình tương đối cũ trên một máy hiện đại. Một chương trình cũ được viết bằng C ++ có thể đã được biên dịch sang mã 32 bit và sẽ tiếp tục sử dụng mã 32 bit ngay cả trên bộ xử lý 64 bit hiện đại. Một chương trình được viết bằng C # sẽ được biên dịch thành mã byte, sau đó VM sẽ biên dịch thành mã máy 64-bit. Nếu chương trình này thu được lợi ích đáng kể từ việc chạy dưới dạng mã 64-bit, thì điều đó có thể mang lại lợi thế đáng kể. Trong một thời gian ngắn khi các bộ vi xử lý 64-bit còn khá mới, điều này đã xảy ra khá nhiều. Mặc dù vậy, mã gần đây có khả năng được hưởng lợi từ bộ xử lý 64 bit sẽ được biên dịch tĩnh thành mã 64 bit.
Sử dụng máy ảo cũng có khả năng cải thiện việc sử dụng bộ nhớ cache. Hướng dẫn cho một máy ảo thường nhỏ gọn hơn so với hướng dẫn máy bản địa. Nhiều mã trong số chúng có thể vừa với một lượng bộ nhớ đệm nhất định, vì vậy bạn có cơ hội tốt hơn để bất kỳ mã nhất định nào được đưa vào bộ nhớ đệm khi cần. Điều này có thể giúp duy trì việc thực thi mã VM được diễn giải cạnh tranh hơn (về tốc độ) so với hầu hết mọi người mong đợi ban đầu - bạn có thể thực thi rất nhiều lệnh trên một CPU hiện đại trong thời gian bỏ lỡ một bộ nhớ cache.
Cũng cần nhắc lại rằng yếu tố này không nhất thiết phải khác nhau giữa hai thứ. Không có gì ngăn cản (ví dụ) trình biên dịch C ++ tạo ra đầu ra dự định chạy trên một máy ảo (có hoặc không có JIT). Trên thực tế, C ++ / CLI của Microsoft gần như vậy - một (gần như) trình biên dịch C ++ phù hợp (mặc dù, với rất nhiều phần mở rộng) tạo ra đầu ra dự định chạy trên một máy ảo.
Điều ngược lại cũng đúng: Microsoft hiện có .NET Native, biên dịch mã C # (hoặc VB.NET) thành một tệp thực thi gốc. Điều này mang lại hiệu suất nói chung giống C ++ hơn nhiều, nhưng vẫn giữ được các tính năng của C # / VB (ví dụ: C # được biên dịch sang mã gốc vẫn hỗ trợ phản chiếu). Nếu bạn có mã C # chuyên sâu về hiệu suất, điều này có thể hữu ích.
Thu gom rác thải
Từ những gì tôi đã thấy, tôi muốn nói rằng thu gom rác là yếu tố được hiểu kém nhất trong ba yếu tố này. Chỉ cho một ví dụ rõ ràng, câu hỏi ở đây đề cập: "GC cũng không thêm nhiều chi phí, trừ khi bạn tạo và phá hủy hàng nghìn đối tượng [...]". Trong thực tế, nếu bạn tạo và phá hủy hàng nghìn đối tượng, chi phí thu gom rác nói chung sẽ khá thấp. .NET sử dụng một trình quét thế hệ, là một trình thu thập sao chép đa dạng. Bộ thu gom rác hoạt động bằng cách bắt đầu từ "địa điểm" (ví dụ: thanh ghi và ngăn xếp thực thi) mà con trỏ / tham chiếu được biết đếnđể có thể truy cập. Sau đó, nó "đuổi theo" các con trỏ đó đến các đối tượng đã được cấp phát trên heap. Nó kiểm tra các đối tượng đó để tìm các con trỏ / tham chiếu xa hơn, cho đến khi nó đi theo tất cả chúng đến tận cùng của bất kỳ chuỗi nào và tìm thấy tất cả các đối tượng có thể truy cập (ít nhất là có thể). Trong bước tiếp theo, nó lấy tất cả các đối tượng (hoặc ít nhất có thể ) đang được sử dụng và thu gọn heap bằng cách sao chép tất cả chúng vào một đoạn liền kề ở một đầu của bộ nhớ đang được quản lý trong heap. Phần còn lại của bộ nhớ sau đó sẽ được giải phóng (phải chạy các trình hoàn thiện mô-đun, nhưng ít nhất là trong mã được viết tốt, chúng đủ hiếm để tôi bỏ qua chúng trong thời điểm này).
Điều này có nghĩa là nếu bạn tạo và phá hủy nhiều đối tượng, việc thu gom rác sẽ tăng thêm rất ít chi phí. Thời gian thực hiện của một chu kỳ thu gom rác phụ thuộc gần như hoàn toàn vào số lượng các đối tượng đã được tạo ra nhưng không bị phá hủy. Hệ quả chính của việc tạo và phá hủy các đối tượng một cách vội vàng đơn giản là GC phải chạy thường xuyên hơn, nhưng mỗi chu kỳ vẫn sẽ nhanh. Nếu bạn tạo các đối tượng và không phá hủy chúng, GC sẽ chạy thường xuyên hơn và mỗi chu kỳ sẽ chậm hơn đáng kể vì nó dành nhiều thời gian hơn để theo đuổi các con trỏ đến các đối tượng có khả năng sống và dành nhiều thời gian hơn để sao chép các đối tượng vẫn đang được sử dụng.
Để chống lại điều này, công việc nhặt rác theo thế hệ hoạt động dựa trên giả định rằng các vật thể vẫn còn "sống" trong một thời gian có khả năng tiếp tục sống sót trong một thời gian nữa. Dựa trên điều này, nó có một hệ thống trong đó các đối tượng tồn tại một số chu kỳ thu gom rác sẽ được "sử dụng" và bộ thu gom rác bắt đầu đơn giản cho rằng chúng vẫn đang được sử dụng, vì vậy thay vì sao chép chúng ở mỗi chu kỳ, nó chỉ cần rời đi họ một mình. Đây là một giả định hợp lệ thường đủ rằng việc nhặt rác theo thế hệ thường có chi phí thấp hơn đáng kể so với hầu hết các hình thức GC khác.
Quản lý bộ nhớ "thủ công" thường không được hiểu rõ. Chỉ đối với một ví dụ, nhiều nỗ lực so sánh giả định rằng tất cả việc quản lý bộ nhớ thủ công cũng tuân theo một mô hình cụ thể (ví dụ: phân bổ phù hợp nhất). Điều này thường ít (nếu có) gần với thực tế hơn niềm tin của nhiều người về việc thu gom rác (ví dụ, giả định phổ biến rằng nó thường được thực hiện bằng cách sử dụng phương pháp đếm tham chiếu).
Với sự đa dạng của các chiến lược cho cả thu gom rác và quản lý bộ nhớ thủ công, rất khó để so sánh cả hai về tốc độ tổng thể. Việc cố gắng so sánh tốc độ phân bổ và / hoặc giải phóng bộ nhớ (tự nó) gần như được đảm bảo để tạo ra kết quả tốt nhất là vô nghĩa và tệ nhất là hoàn toàn sai lệch.
Chủ đề thưởng: Điểm chuẩn
Vì khá nhiều blog, trang web, bài báo trên tạp chí, v.v., tuyên bố cung cấp bằng chứng "khách quan" theo hướng này hay hướng khác, tôi cũng sẽ đưa vào chủ đề đó trị giá hai xu.
Hầu hết các điểm chuẩn này hơi giống như việc thanh thiếu niên quyết định đua xe của họ và ai thắng sẽ được giữ cả hai xe. Mặc dù vậy, các trang web khác nhau ở một điểm quan trọng: họ cho rằng những người công bố điểm chuẩn được lái cả hai chiếc xe. Một cơ hội kỳ lạ nào đó, xe của anh ta luôn thắng, và những người khác phải giải quyết "tin tôi đi, tôi đã thực sự lái chiếc xe của bạn nhanh như nó sẽ đi."
Thật dễ dàng để viết một điểm chuẩn kém tạo ra kết quả có nghĩa là không có gì. Hầu như bất kỳ ai có đủ kỹ năng cần thiết để thiết kế một điểm chuẩn tạo ra bất kỳ điều gì có ý nghĩa, cũng có kỹ năng tạo ra một điểm chuẩn mang lại kết quả mà anh ta quyết định. Trên thực tế, viết mã để tạo ra một kết quả cụ thể có lẽ dễ hơn mã thực sự tạo ra kết quả có ý nghĩa.
Như người bạn của tôi, James Kanze đã nói, "đừng bao giờ tin tưởng vào một điểm chuẩn mà bạn đã không làm sai lệch bản thân."
Phần kết luận
Không có câu trả lời đơn giản. Tôi chắc chắn một cách hợp lý rằng tôi có thể tung đồng xu để chọn người chiến thắng, sau đó chọn một số từ (giả sử) 1 đến 20 để biết tỷ lệ phần trăm mà nó sẽ thắng và viết một số mã trông giống như một điểm chuẩn hợp lý và công bằng, và đưa ra kết luận bị bỏ qua (ít nhất là trên một số bộ xử lý đích - một bộ xử lý khác có thể thay đổi tỷ lệ phần trăm một chút).
Như những người khác đã chỉ ra, đối với hầu hết các mã, tốc độ gần như không liên quan. Hệ quả của điều đó (thường bị bỏ qua nhiều hơn) là trong đoạn mã nhỏ mà tốc độ quan trọng, nó thường quan trọng rất nhiều . Ít nhất theo kinh nghiệm của tôi, đối với mã mà nó thực sự quan trọng, C ++ hầu như luôn luôn là người chiến thắng. Chắc chắn có những yếu tố ủng hộ C #, nhưng trong thực tế, chúng dường như bị lấn át bởi những yếu tố có lợi cho C ++. Bạn chắc chắn có thể tìm thấy các điểm chuẩn sẽ cho biết kết quả lựa chọn của bạn, nhưng khi bạn viết mã thực, hầu như bạn luôn có thể làm cho nó trong C ++ nhanh hơn trong C #. Có thể (hoặc có thể không) cần nhiều kỹ năng và / hoặc nỗ lực hơn để viết, nhưng hầu như luôn có thể.