Điều gì ủng hộ cho rằng C ++ có thể nhanh hơn JVM hoặc CLR với JIT? [đóng cửa]


119

Một chủ đề được nhắc lại trên SE tôi đã nhận thấy trong nhiều câu hỏi là lập luận liên tục rằng C ++ nhanh hơn và / hoặc hiệu quả hơn các ngôn ngữ cấp cao hơn như Java. Đối số là JVM hoặc CLR hiện đại có thể hiệu quả nhờ JIT và cứ thế cho số lượng nhiệm vụ ngày càng tăng và C ++ chỉ hiệu quả hơn bao giờ hết nếu bạn biết bạn đang làm gì và tại sao lại làm mọi thứ theo một cách nhất định hiệu suất sẽ tăng hiệu suất. Đó là điều hiển nhiên và có ý nghĩa hoàn hảo.

Tôi muốn biết một lời giải thích cơ bản (nếu có một điều như vậy ...) về lý do tại saolàm thế nào một số nhiệm vụ nhanh hơn trong C ++ so với JVM hoặc CLR? Có phải đơn giản là vì C ++ được biên dịch thành mã máy trong khi JVM hoặc CLR vẫn có chi phí xử lý của quá trình biên dịch JIT khi chạy?

Khi tôi cố gắng nghiên cứu chủ đề, tất cả những gì tôi tìm thấy là những lý lẽ tương tự tôi đã nêu ở trên mà không có bất kỳ thông tin chi tiết nào để hiểu chính xác làm thế nào C ++ có thể được sử dụng cho điện toán hiệu năng cao.


Hiệu suất cũng phụ thuộc vào sự phức tạp của chương trình.
pandu

23
Tôi đã thêm vào "C ++ chỉ hiệu quả hơn bao giờ hết nếu bạn biết những gì bạn đang làm và tại sao làm mọi thứ theo một cách nhất định sẽ làm tăng hiệu suất." bằng cách nói rằng đó không chỉ là vấn đề về kiến ​​thức, mà còn là vấn đề thời gian của nhà phát triển. Nó không phải lúc nào cũng hiệu quả để tối đa hóa tối ưu hóa. Đây là lý do tại sao các ngôn ngữ cấp cao hơn như Java và Python tồn tại (trong số các lý do khác) - để giảm lượng thời gian mà lập trình viên phải dành lập trình để hoàn thành một nhiệm vụ nhất định với chi phí tối ưu hóa được điều chỉnh cao.
Joel Cornett

4
@Joel Cornett: Tôi hoàn toàn đồng ý. Tôi chắc chắn làm việc hiệu quả hơn trong Java so với C ++ và tôi chỉ xem xét C ++ khi tôi cần viết mã thật nhanh. Mặt khác, tôi đã thấy mã C ++ được viết kém rất chậm: C ++ ít hữu dụng hơn trong tay các lập trình viên không có kỹ năng.
Giorgio

3
Bất kỳ đầu ra biên dịch nào có thể được tạo bởi JIT đều có thể được tạo bởi C ++, nhưng mã mà C ++ có thể tạo ra có thể không nhất thiết phải được tạo bởi JIT. Vì vậy, các khả năng và đặc tính hiệu suất của C ++ là một siêu ngôn ngữ của bất kỳ ngôn ngữ cấp cao nào. QED
tylerl

1
@Doval Về mặt kỹ thuật là đúng, nhưng theo quy tắc, bạn có thể đếm các yếu tố thời gian chạy có thể ảnh hưởng đến hiệu suất của chương trình trên một mặt. Thông thường không sử dụng nhiều hơn hai ngón tay. Vì vậy, trường hợp xấu nhất là bạn gửi nhiều nhị phân ... ngoại trừ việc bạn thậm chí không cần phải làm điều đó bởi vì khả năng tăng tốc không đáng kể, đó là lý do tại sao không ai bận tâm cả.
tylerl

Câu trả lời:


200

Đó là tất cả về bộ nhớ (không phải JIT). Ưu điểm của JIT so với C 'hầu hết chỉ giới hạn trong việc tối ưu hóa các cuộc gọi ảo hoặc không ảo thông qua nội tuyến, điều mà CPU BTB đã rất nỗ lực để thực hiện.

Trong các máy hiện đại, việc truy cập RAM rất chậm (so với bất kỳ thứ gì CPU làm), điều đó có nghĩa là các ứng dụng sử dụng bộ nhớ cache càng nhiều càng tốt (dễ dàng hơn khi sử dụng ít bộ nhớ hơn) có thể nhanh hơn hàng trăm lần so với các bộ nhớ không. Và có nhiều cách mà Java sử dụng nhiều bộ nhớ hơn C ++ và khiến việc viết các ứng dụng khai thác bộ đệm hoàn toàn khó khăn hơn:

  • Có một chi phí bộ nhớ tối thiểu 8 byte cho mỗi đối tượng và việc sử dụng các đối tượng thay vì nguyên thủy là bắt buộc hoặc được ưa thích ở nhiều nơi (cụ thể là các bộ sưu tập tiêu chuẩn).
  • Chuỗi bao gồm hai đối tượng và có tổng phí là 38 byte
  • UTF-16 được sử dụng nội bộ, điều đó có nghĩa là mỗi ký tự ASCII yêu cầu hai byte thay vì một byte (Oracle JVM gần đây đã đưa ra một tối ưu hóa để tránh điều này cho các chuỗi ASCII thuần túy).
  • Không có loại tham chiếu tổng hợp (tức là cấu trúc), và lần lượt, không có mảng các loại tham chiếu tổng hợp. Một đối tượng Java, hoặc mảng các đối tượng Java, có vị trí bộ đệm L1 / L2 rất kém so với cấu trúc và mảng C.
  • Các thế hệ Java sử dụng kiểu xóa, có vị trí bộ đệm kém so với khởi tạo kiểu.
  • Việc phân bổ đối tượng mờ đục và phải được thực hiện riêng cho từng đối tượng, do đó, một ứng dụng không thể cố tình bố trí dữ liệu của mình theo cách thân thiện với bộ đệm và vẫn coi đó là dữ liệu có cấu trúc.

Một số yếu tố liên quan đến bộ nhớ khác nhưng không liên quan đến bộ nhớ cache:

  • Không có phân bổ ngăn xếp, vì vậy tất cả dữ liệu không nguyên thủy mà bạn làm việc phải nằm trong đống và đi qua bộ sưu tập rác (một số JIT gần đây thực hiện phân bổ ngăn xếp phía sau hậu trường trong một số trường hợp nhất định).
  • Bởi vì không có loại tham chiếu tổng hợp, không có ngăn xếp vượt qua của loại tham chiếu tổng hợp. (Hãy suy nghĩ hiệu quả thông qua các đối số Vector)
  • Bộ sưu tập rác có thể làm tổn thương nội dung bộ đệm L1 / L2 và tạm dừng thế giới của GC làm tổn thương tính tương tác.
  • Chuyển đổi giữa các loại dữ liệu luôn yêu cầu sao chép; bạn không thể lấy một con trỏ tới một loạt byte bạn nhận được từ một socket và diễn giải chúng như là một float.

Một số trong số này là sự đánh đổi (không phải thực hiện quản lý bộ nhớ thủ công đáng để từ bỏ nhiều hiệu năng đối với hầu hết mọi người), một số có lẽ là kết quả của việc cố gắng giữ Java đơn giản và một số là lỗi thiết kế (mặc dù có thể chỉ trong nhận thức , cụ thể là UTF-16 là một mã hóa có độ dài cố định khi Java được tạo, điều này khiến cho quyết định chọn nó trở nên dễ hiểu hơn rất nhiều).

Điều đáng chú ý là nhiều sự đánh đổi này rất khác nhau đối với Java / JVM so với C # / CIL. .NET CIL có các cấu trúc kiểu tham chiếu, phân bổ / truyền ngăn xếp, các mảng của các cấu trúc được đóng gói và các tổng quát khởi tạo kiểu.


37
+1 - nhìn chung, đây là một câu trả lời tốt. Tuy nhiên, tôi không chắc điểm đạn "không có phân bổ ngăn xếp" là hoàn toàn chính xác. Các JIT Java thường thực hiện phân tích thoát để cho phép phân bổ ngăn xếp khi có thể - có lẽ điều bạn nên nói là ngôn ngữ Java không cho phép lập trình viên quyết định khi nào một đối tượng được phân bổ ngăn xếp so với phân bổ heap. Ngoài ra, nếu một trình thu gom rác thế hệ (mà tất cả các JVM hiện đại sử dụng) đang được sử dụng, "phân bổ heap" có nghĩa là một điều hoàn toàn khác (với các đặc tính hiệu suất hoàn toàn khác) so với trong môi trường C ++.
Daniel Pryden

5
Tôi sẽ nghĩ có hai thứ khác nhưng tôi chủ yếu làm việc với những thứ ở cấp độ cao hơn nhiều vì vậy hãy nói nếu tôi sai. Bạn thực sự không thể viết C ++ mà không phát triển một số nhận thức chung hơn về những gì thực sự xảy ra trong bộ nhớ và cách mã máy thực sự hoạt động trong khi các ngôn ngữ kịch bản hoặc máy ảo trừu tượng hóa tất cả những thứ đó khỏi sự chú ý của bạn. Bạn cũng có quyền kiểm soát chi tiết hơn nhiều về cách mọi thứ hoạt động trong khi trong VM hoặc ngôn ngữ được giải thích bạn đang dựa vào những gì tác giả thư viện cốt lõi có thể đã tối ưu hóa cho một kịch bản quá cụ thể.
Erik Reppen

18
+1. Một điều nữa tôi muốn thêm (nhưng không sẵn sàng gửi câu trả lời mới cho): lập chỉ mục mảng trong Java luôn bao gồm kiểm tra giới hạn. Với C và C ++, đây không phải là trường hợp.
riwalk

7
Điều đáng chú ý là phân bổ heap của Java nhanh hơn đáng kể so với phiên bản ngây thơ với C ++ (do nội bộ và mọi thứ), nhưng phân bổ bộ nhớ trong C ++ thể tốt hơn đáng kể nếu bạn biết bạn đang làm gì.
Brendan dài

10
@BrendanLong, đúng .. nhưng chỉ khi bộ nhớ sạch - một khi ứng dụng chạy được một lúc, việc cấp phát bộ nhớ sẽ chậm hơn do cần phải làm chậm mọi thứ vì nó phải giải phóng bộ nhớ, chạy bộ hoàn thiện và sau đó gọn nhẹ. Đây là một sự đánh đổi mang lại lợi ích cho điểm chuẩn nhưng (IMHO) nói chung làm chậm các ứng dụng.
gbjbaanb

67

Có phải đơn giản là vì C ++ được biên dịch thành mã lắp ráp / mã máy trong khi Java / C # vẫn có chi phí xử lý của quá trình biên dịch JIT khi chạy?

Một phần, nhưng nói chung, giả sử một trình biên dịch JIT hiện đại tuyệt vời, mã C ++ thích hợp vẫn có xu hướng hoạt động tốt hơn mã Java vì HAI lý do chính:

1) C ++ template cung cấp cơ sở vật chất tốt hơn cho việc viết mã đó là cả hai chunghiệu quả . Các mẫu cung cấp cho lập trình viên C ++ một bản tóm tắt rất hữu ích có chi phí thời gian chạy ZERO. (Các mẫu về cơ bản là kiểu gõ vịt thời gian biên dịch.) Ngược lại, thứ tốt nhất bạn có được với các tổng quát Java về cơ bản là các hàm ảo. Các hàm ảo luôn có phí hoạt động và thường không thể nội tuyến.

Nói chung, hầu hết các ngôn ngữ, bao gồm Java, C # và thậm chí C, khiến bạn chọn giữa hiệu quả và tính tổng quát / trừu tượng. Các mẫu C ++ cung cấp cho bạn cả hai (với chi phí thời gian biên dịch dài hơn.)

2) Thực tế là tiêu chuẩn C ++ không có nhiều điều để nói về bố cục nhị phân của chương trình C ++ được biên dịch mang lại cho trình biên dịch C ++ nhiều thời gian hơn so với trình biên dịch Java, cho phép tối ưu hóa tốt hơn (đôi khi gặp khó khăn hơn trong việc gỡ lỗi. ) Trong thực tế, bản chất của đặc tả ngôn ngữ Java thi hành hình phạt hiệu năng ở một số khu vực nhất định. Ví dụ: bạn không thể có một mảng các Đối tượng liền kề trong Java. Bạn chỉ có thể có một mảng các con trỏ đối tượng(tài liệu tham khảo), có nghĩa là việc lặp lại qua một mảng trong Java luôn phải chịu chi phí cho việc chuyển hướng. Tuy nhiên, ngữ nghĩa giá trị của C ++, cho phép các mảng liền kề. Một điểm khác biệt nữa là thực tế là C ++ cho phép các đối tượng được phân bổ trên ngăn xếp, trong khi Java thì không có nghĩa là trong thực tế, vì hầu hết các chương trình C ++ có xu hướng phân bổ các đối tượng trên ngăn xếp, chi phí phân bổ thường gần bằng không.

Một lĩnh vực mà C ++ có thể tụt hậu so với Java là bất kỳ tình huống nào mà nhiều đối tượng nhỏ cần được phân bổ trên heap. Trong trường hợp này, hệ thống thu gom rác của Java có thể sẽ mang lại hiệu năng tốt hơn so với tiêu chuẩn newdeletetrong C ++ vì Java GC cho phép phân bổ hàng loạt. Nhưng một lần nữa, một lập trình viên C ++ có thể bù lại điều này bằng cách sử dụng nhóm bộ nhớ hoặc bộ cấp phát bản ghi, trong khi đó, một lập trình viên Java không có sự truy đòi khi phải đối mặt với mẫu cấp phát bộ nhớ mà thời gian chạy Java không được tối ưu hóa.

Ngoài ra, xem câu trả lời tuyệt vời này để biết thêm thông tin về chủ đề này.


6
Câu trả lời tốt nhưng một điểm nhỏ: "Các mẫu C ++ cung cấp cho bạn cả hai (với chi phí thời gian biên dịch dài hơn.)" Tôi cũng sẽ thêm vào với chi phí cho kích thước chương trình lớn hơn. Có thể không phải lúc nào cũng là một vấn đề, nhưng nếu phát triển cho thiết bị di động, nó chắc chắn có thể.
Leo

9
@luiscubal: không, về mặt này, các tổng quát C # rất giống Java (trong đó cùng một đường dẫn mã "chung" được thực hiện bất kể loại nào được truyền qua.) Thủ thuật đối với các mẫu C ++ là mã được khởi tạo một lần cho mỗi loại nó được áp dụng cho. Vì vậy, std::vector<int>một mảng động được thiết kế chỉ dành cho ints và trình biên dịch có thể tối ưu hóa nó cho phù hợp. AC # List<int>vẫn chỉ là a List.
jalf

12
@jalf C # List<int>sử dụng một int[], không Object[]giống như Java. Xem stackoverflow.com/questions/116988/
Mạnh

5
@luiscubal: thuật ngữ của bạn không rõ ràng. JIT không hành động theo những gì tôi cho là "thời gian biên dịch". Tất nhiên, bạn đúng, được cung cấp một trình biên dịch JIT đủ thông minh và tích cực, thực sự không có giới hạn nào cho những gì nó có thể làm. Nhưng C ++ yêu cầu hành vi này. Hơn nữa, các mẫu C ++ cho phép lập trình viên chỉ định các chuyên ngành rõ ràng, cho phép tối ưu hóa rõ ràng bổ sung khi áp dụng. C # không có tương đương cho điều đó. Ví dụ, trong C ++, tôi có thể xác định một vector<N>nơi, đối với trường hợp cụ thể của vector<4>, tay mã của tôi thực hiện SIMD nên được sử dụng
jalf

5
@Leo: Mã phình thông qua các mẫu là một vấn đề 15 năm trước. Với templatization và inlining nặng, cộng với các trình biên dịch khả năng được chọn kể từ (như gấp các trường hợp giống hệt nhau), rất nhiều mã ngày nay nhỏ hơn thông qua các mẫu.
sbi

46

Điều mà các câu trả lời khác (6 cho đến nay) dường như đã quên đề cập đến, nhưng điều tôi cho là rất quan trọng để trả lời câu hỏi này, là một trong những triết lý thiết kế rất cơ bản của C ++, được Stroustrup xây dựng và sử dụng từ ngày 1:

Bạn không trả tiền cho những gì bạn không sử dụng.

Có một số nguyên tắc thiết kế cơ bản quan trọng khác có hình dạng rất lớn C ++ (như thế bạn không nên bị ép buộc vào một mô hình cụ thể), nhưng Bạn không trả tiền cho những gì bạn không sử dụng nằm ngay trong số những nguyên tắc quan trọng nhất.


Trong cuốn sách Thiết kế và tiến hóa của C ++ (thường được gọi là [D & E]), Stroustrup mô tả những gì anh ta cần mà đã đưa anh ta đến với C ++ ngay từ đầu. Nói theo cách riêng của tôi: Đối với luận án tiến sĩ của mình (một cái gì đó liên quan đến mô phỏng mạng, IIRC), anh ấy đã triển khai một hệ thống trong SIMULA, thứ mà anh ấy thích rất nhiều, vì ngôn ngữ rất tốt trong việc cho phép anh ấy thể hiện suy nghĩ của mình trực tiếp bằng mã. Tuy nhiên, chương trình kết quả chạy quá chậm và để có được bằng cấp, anh ta đã viết lại điều đó trong BCPL, tiền thân của C. Viết mã bằng BCPL, anh ta mô tả là một nỗi đau, nhưng chương trình kết quả đủ nhanh để cung cấp kết quả, cho phép anh ta hoàn thành tiến sĩ.

Sau đó, anh ta muốn một ngôn ngữ cho phép dịch các vấn đề trong thế giới thực thành mã trực tiếp nhất có thể, nhưng cũng cho phép mã này rất hiệu quả.
Để theo đuổi điều đó, anh đã tạo ra thứ mà sau này trở thành C ++.


Vì vậy, mục tiêu nêu trên không phải là chỉ đơn thuần là một trong những nguyên tắc thiết kế cơ bản cơ bản, nó rất gần với raison d'être cho C ++. Và nó có thể được tìm thấy ở mọi nơi trong ngôn ngữ: Các hàm chỉ virtualkhi bạn muốn chúng (vì gọi các hàm ảo đi kèm với một chi phí nhỏ) POD chỉ được khởi tạo tự động khi bạn yêu cầu rõ ràng điều này, ngoại lệ chỉ làm bạn mất hiệu suất khi bạn thực sự yêu cầu ném chúng (trong khi đó là một mục tiêu thiết kế rõ ràng để cho phép thiết lập / dọn dẹp stackframes rất rẻ), không có GC nào chạy bất cứ khi nào nó cảm thấy như vậy, v.v.

C ++ rõ ràng đã chọn không cung cấp cho bạn một số tiện ích ("tôi có phải biến phương thức này thành ảo ở đây không?") Để đổi lấy hiệu suất ("không, tôi không, và bây giờ trình biên dịch có thể thực hiện inlinevà tối ưu hóa cái quái đó toàn bộ! "), và, không ngạc nhiên, điều này thực sự dẫn đến tăng hiệu suất so với các ngôn ngữ thuận tiện hơn.


4
Bạn không trả tiền cho những gì bạn không sử dụng. => và sau đó họ đã thêm RTTI :(
Matthieu M.

11
@Matthieu: Trong khi tôi hiểu được tình cảm của bạn, tôi không thể không chú ý rằng thậm chí điều đó đã được thêm vào một cách cẩn thận về hiệu suất. RTTI được chỉ định để có thể triển khai bằng các bảng ảo và do đó thêm rất ít chi phí nếu bạn không sử dụng nó. Nếu bạn không sử dụng đa hình, thì không có chi phí nào cả. Tui bỏ lỡ điều gì vậy?
sbi

9
@Matthieu: Tất nhiên, có lý do. Nhưng lý do này là hợp lý? Từ những gì tôi có thể thấy, "chi phí của RTTI", nếu không được sử dụng, là một con trỏ bổ sung trong bảng ảo của mỗi lớp đa hình, chỉ vào một số đối tượng RTTI được phân bổ tĩnh ở đâu đó. Trừ khi bạn muốn lập trình chip trong máy nướng bánh mì của tôi, làm thế nào điều này có thể có liên quan?
sbi

4
@Aaronaught: Tôi không biết phải trả lời như thế nào. Bạn có thực sự bỏ qua câu trả lời của tôi không vì nó chỉ ra triết lý cơ bản khiến Stroustrup et al thêm các tính năng theo cách cho phép thực hiện, thay vì liệt kê các cách và tính năng này một cách riêng lẻ?
sbi

9
@Aaronaught: Bạn có cảm tình của tôi.
sbi

29

Bạn có biết tài liệu nghiên cứu của Google về chủ đề đó?

Từ kết luận:

Chúng tôi thấy rằng liên quan đến hiệu suất, C ++ thắng nhờ lợi nhuận lớn. Tuy nhiên, nó cũng đòi hỏi những nỗ lực điều chỉnh sâu rộng nhất, nhiều trong số đó được thực hiện ở mức độ tinh vi sẽ không có sẵn cho các lập trình viên trung bình.

Đây ít nhất là một lời giải thích một phần, theo nghĩa "bởi vì các trình biên dịch C ++ trong thế giới thực tạo ra mã nhanh hơn các trình biên dịch Java bằng các biện pháp thực nghiệm".


4
Bên cạnh sự khác biệt về sử dụng bộ nhớ và bộ nhớ cache, một trong những điều quan trọng nhất là lượng tối ưu hóa được thực hiện. So sánh có bao nhiêu tối ưu hóa GCC / LLVM (và có thể là Visual C ++ / ICC) so với trình biên dịch Java HotSpot: nhiều hơn nữa, đặc biệt là về các vòng lặp, loại bỏ các nhánh dư thừa và phân bổ đăng ký. Trình biên dịch JIT thường không có thời gian cho những tối ưu hóa mạnh mẽ này, thậm chí còn nghĩ rằng họ có thể triển khai chúng tốt hơn bằng cách sử dụng thông tin thời gian chạy có sẵn.
Gratian Lup

2
@GratianLup: Tôi tự hỏi liệu điều đó (vẫn) có đúng với LTO không.
Ded

2
@GratianLup: Đừng quên tối ưu hóa thông tin dẫn đường cho C ++ ...
Deduplicator

23

Đây không phải là một bản sao câu hỏi của bạn, nhưng câu trả lời được chấp nhận trả lời hầu hết câu hỏi của bạn: Một đánh giá hiện đại về Java

Tóm lại:

Về cơ bản, ngữ nghĩa của Java cho rằng đó là ngôn ngữ chậm hơn C ++.

Vì vậy, tùy thuộc vào ngôn ngữ khác mà bạn so sánh với C ++, bạn có thể nhận được hoặc không cùng một câu trả lời.

Trong C ++, bạn có:

  • Khả năng làm nội tuyến thông minh,
  • tạo mã chung có địa phương mạnh (mẫu)
  • nhỏ và gọn nhất có thể
  • cơ hội để tránh bị gián đoạn
  • hành vi bộ nhớ dự đoán
  • tối ưu hóa trình biên dịch chỉ có thể do việc sử dụng trừu tượng mức cao (mẫu)

Đây là những tính năng hoặc tác dụng phụ của định nghĩa ngôn ngữ giúp nó có hiệu quả về mặt lý thuyết về bộ nhớ và tốc độ hơn bất kỳ ngôn ngữ nào:

  • sử dụng ồ ạt ("mọi thứ đều là ngôn ngữ tham chiếu / con trỏ được quản lý"): cảm ứng có nghĩa là CPU phải nhảy vào bộ nhớ để lấy dữ liệu cần thiết, làm tăng lỗi bộ nhớ cache của CPU, điều đó có nghĩa là làm chậm quá trình xử lý rất nhiều ngay cả khi nó có thể có dữ liệu nhỏ như C ++;
  • tạo các đối tượng kích thước lớn mà các thành viên được truy cập gián tiếp: đây là kết quả của việc có các tham chiếu theo mặc định, các thành viên là con trỏ để khi bạn có một thành viên, bạn có thể không nhận được dữ liệu gần lõi của đối tượng cha, một lần nữa kích hoạt lỗi bộ nhớ cache.
  • sử dụng một bộ sưu tập garbarge: nó chỉ làm cho khả năng dự đoán hiệu suất là không thể (theo thiết kế).

C ++ nội tuyến phức tạp của trình biên dịch làm giảm hoặc loại bỏ rất nhiều chỉ dẫn. Khả năng tạo một tập hợp dữ liệu nhỏ gọn giúp nó trở nên thân thiện với bộ đệm nếu bạn không trải đều các dữ liệu này trên bộ nhớ thay vì được đóng gói cùng nhau (cả hai đều có thể, C ++ chỉ cho phép bạn chọn). RAII làm cho hành vi bộ nhớ C ++ có thể dự đoán được, loại bỏ rất nhiều vấn đề trong trường hợp mô phỏng thời gian thực hoặc bán thời gian thực, đòi hỏi tốc độ cao. Các vấn đề về địa phương, nói chung có thể được tóm tắt bằng cách này: chương trình / dữ liệu càng nhỏ thì việc thực thi càng nhanh. C ++ cung cấp nhiều cách khác nhau để đảm bảo dữ liệu của bạn là nơi bạn muốn (trong một nhóm, một mảng hoặc bất cứ thứ gì) và nó nhỏ gọn.

Rõ ràng, có những ngôn ngữ khác có thể làm tương tự, nhưng chúng chỉ ít phổ biến hơn vì chúng không cung cấp nhiều công cụ trừu tượng như C ++, vì vậy chúng ít hữu ích hơn trong nhiều trường hợp.


7

Nó chủ yếu là về bộ nhớ (như Michael Borgwardt đã nói) với một chút không hiệu quả của JIT được thêm vào.

Một điều không được đề cập là bộ đệm - để sử dụng bộ đệm đầy đủ, bạn cần dữ liệu của mình được đặt liên tục (tức là tất cả cùng nhau). Bây giờ với hệ thống GC, bộ nhớ được phân bổ trên heap GC, rất nhanh, nhưng khi bộ nhớ được sử dụng, GC sẽ khởi động thường xuyên và loại bỏ các khối không còn được sử dụng và sau đó nén các phần còn lại lại với nhau. Bây giờ ngoài sự chậm chạp rõ ràng của việc di chuyển các khối được sử dụng lại với nhau, điều này có nghĩa là dữ liệu bạn đang sử dụng có thể không bị kẹt lại với nhau. Nếu bạn có một mảng gồm 1000 phần tử, trừ khi bạn phân bổ tất cả chúng cùng một lúc (và sau đó cập nhật nội dung của chúng thay vì xóa và tạo phần tử mới - sẽ được tạo ở cuối heap), các phần tử này sẽ bị phân tán khắp heap, do đó yêu cầu một số lần truy cập bộ nhớ để đọc tất cả vào bộ đệm CPU. Ứng dụng AC / C ++ rất có thể sẽ phân bổ bộ nhớ cho các yếu tố này và sau đó bạn cập nhật các khối với dữ liệu. (ok, có các cấu trúc dữ liệu giống như một danh sách hoạt động giống như phân bổ bộ nhớ GC, nhưng mọi người biết chúng chậm hơn các vectơ).

Bạn có thể thấy điều này trong hoạt động đơn giản bằng cách thay thế bất kỳ đối tượng StringBuilder nào bằng String ... Stringbuilders hoạt động bằng cách cấp phát bộ nhớ và lấp đầy nó, và là một thủ thuật hiệu suất đã biết cho các hệ thống java / .NET.

Đừng quên rằng mô hình 'xóa cũ và phân bổ các bản sao mới được sử dụng rất nhiều trong Java / C #, đơn giản vì mọi người được thông báo rằng việc cấp phát bộ nhớ thực sự rất nhanh do GC và do đó mô hình bộ nhớ phân tán được sử dụng ở mọi nơi ( tất nhiên ngoại trừ các nhà xây dựng chuỗi, vì vậy tất cả các thư viện của bạn có xu hướng lãng phí bộ nhớ và sử dụng rất nhiều bộ nhớ, không ai trong số đó nhận được lợi ích của sự liên tục. Đổ lỗi cho sự cường điệu xung quanh GC cho điều này - họ nói với bạn bộ nhớ là miễn phí, lol.

Bản thân GC rõ ràng là một cú hích hoàn hảo khác - khi nó chạy, nó không chỉ phải quét qua đống, mà còn phải giải phóng tất cả các khối không sử dụng, và sau đó nó phải chạy bất kỳ bộ hoàn thiện nào (mặc dù điều này được sử dụng riêng lần tiếp theo với ứng dụng tạm dừng) (Tôi không biết liệu nó có còn là một cú hích hoàn hảo không, nhưng tất cả các tài liệu tôi đọc đều nói chỉ sử dụng các công cụ cuối cùng nếu thực sự cần thiết) và sau đó nó phải di chuyển các khối đó vào vị trí để heap được nén và cập nhật tham chiếu đến vị trí mới của khối. Như bạn có thể thấy, rất nhiều công việc!

Các lượt truy cập hoàn hảo cho bộ nhớ C ++ được phân bổ theo bộ nhớ - khi bạn cần một khối mới, bạn phải đi bộ tìm kiếm không gian trống tiếp theo đủ lớn, với một đống bị phân mảnh nặng nề, điều này gần như không nhanh bằng một GC 'Chỉ phân bổ một khối khác ở cuối' nhưng tôi nghĩ nó không chậm như tất cả các công việc mà công cụ nén GC thực hiện và có thể được giảm thiểu bằng cách sử dụng nhiều đống khối có kích thước cố định (còn gọi là nhóm bộ nhớ).

Có nhiều hơn ... như tải các hội đồng ra khỏi GAC yêu cầu kiểm tra bảo mật, các đường dẫn thăm dò (bật sxstrace và chỉ nhìn vào những gì nó đang làm!) Và nói chung là sự áp đảo khác dường như phổ biến hơn nhiều với java / .net hơn C / C ++.


2
Nhiều điều bạn viết không đúng với người thu gom rác thế hệ hiện đại.
Michael Borgwardt

3
@MichaelBorgwardt như thế nào? Tôi nói "GC chạy thường xuyên" và "nó nén được đống". Phần còn lại của câu trả lời của tôi liên quan đến cách cấu trúc dữ liệu ứng dụng sử dụng bộ nhớ.
gbjbaanb

6

"Có phải đơn giản là vì C ++ được biên dịch thành mã lắp ráp / mã máy trong khi Java / C # vẫn có chi phí xử lý của quá trình biên dịch JIT khi chạy?" Về cơ bản, có!

Mặc dù vậy, lưu ý nhanh, Java có nhiều chi phí hơn là chỉ biên dịch JIT. Ví dụ, nó nhiều hơn nữa kiểm tra cho bạn (đó là cách nó làm những thứ như ArrayIndexOutOfBoundsExceptionsNullPointerExceptions). Bộ thu gom rác là một chi phí đáng kể khác.

Có một so sánh khá chi tiết ở đây .


2

Hãy nhớ rằng những điều sau đây chỉ là so sánh sự khác biệt giữa trình biên dịch gốc và JIT, và không bao gồm các chi tiết cụ thể của bất kỳ ngôn ngữ hoặc khung cụ thể nào. Có thể có những lý do chính đáng để chọn một nền tảng cụ thể ngoài điều này.

Khi chúng tôi tuyên bố rằng mã gốc nhanh hơn, chúng tôi đang nói về trường hợp sử dụng điển hình của mã được biên dịch tự nhiên so với mã được biên dịch JIT, trong đó người dùng sử dụng ứng dụng được biên dịch JIT điển hình, với kết quả ngay lập tức (ví dụ: không chờ trên trình biên dịch trước). Trong trường hợp đó, tôi không nghĩ ai có thể yêu cầu với khuôn mặt thẳng thắn, rằng mã được biên dịch JIT có thể khớp hoặc đánh bại mã gốc.

Giả sử chúng ta có một chương trình được viết bằng một số ngôn ngữ X và chúng ta có thể biên dịch nó với trình biên dịch gốc và một lần nữa với trình biên dịch JIT. Mỗi luồng công việc có cùng các giai đoạn liên quan, có thể được khái quát hóa như (Mã -> Đại diện trung gian -> Mã máy -> Thực thi). Sự khác biệt lớn giữa hai là giai đoạn nào được người dùng nhìn thấy và được lập trình viên nhìn thấy. Với trình biên dịch gốc, lập trình viên nhìn thấy tất cả trừ giai đoạn thực thi, nhưng với giải pháp JIT, người dùng sẽ nhìn thấy việc biên dịch thành mã máy, ngoài việc thực thi.

Khiếu nại rằng A nhanh hơn B đang đề cập đến thời gian để chương trình chạy, như người dùng đã thấy . Nếu chúng ta giả sử rằng cả hai đoạn mã thực hiện giống hệt nhau trong giai đoạn Thực thi, chúng ta phải giả sử rằng luồng công việc JIT chậm hơn đối với người dùng, vì anh ta cũng phải xem thời gian T của quá trình biên dịch thành mã máy, trong đó T> 0. Vì vậy, , đối với bất kỳ khả năng nào của luồng công việc JIT thực hiện giống như luồng công việc gốc, đối với người dùng, chúng ta phải giảm thời gian Thực thi mã, như Thi hành + Biên dịch thành mã máy, thấp hơn chỉ giai đoạn Thực thi của dòng công việc bản địa. Điều này có nghĩa là chúng ta phải tối ưu hóa mã tốt hơn trong quá trình biên dịch JIT so với biên dịch gốc.

Tuy nhiên, điều này là không khả thi, vì để thực hiện các tối ưu hóa cần thiết để tăng tốc Thực thi, chúng ta phải dành nhiều thời gian hơn trong quá trình biên dịch sang giai đoạn mã máy, và do đó, bất cứ khi nào chúng ta lưu lại do mã tối ưu hóa thực sự bị mất, vì chúng tôi thêm nó vào phần tổng hợp. Nói cách khác, "sự chậm chạp" của giải pháp dựa trên JIT không chỉ đơn thuần là do thời gian bổ sung cho quá trình biên dịch JIT, mà mã được tạo bởi quá trình biên dịch đó thực hiện chậm hơn so với giải pháp gốc.

Tôi sẽ sử dụng một ví dụ: Đăng ký phân bổ. Vì truy cập bộ nhớ chậm hơn hàng nghìn lần so với truy cập đăng ký, lý tưởng nhất là chúng tôi muốn sử dụng các thanh ghi bất cứ khi nào có thể và có càng ít truy cập bộ nhớ càng tốt, nhưng chúng tôi có số lượng đăng ký hạn chế và chúng tôi phải tràn vào bộ nhớ khi cần một đăng ký. Nếu chúng tôi sử dụng thuật toán phân bổ đăng ký mất 200ms để tính toán và kết quả là chúng tôi tiết kiệm được 2ms thời gian thực hiện - chúng tôi sẽ không tận dụng thời gian tốt nhất cho trình biên dịch JIT. Các giải pháp như thuật toán của Chaitin, có thể tạo mã được tối ưu hóa cao là không phù hợp.

Vai trò của trình biên dịch JIT là tạo ra sự cân bằng tốt nhất giữa thời gian biên dịch và chất lượng mã được sản xuất, tuy nhiên, với sự thiên vị lớn về thời gian biên dịch nhanh, vì bạn không muốn người dùng chờ đợi. Hiệu suất của mã được thực thi chậm hơn trong trường hợp JIT, vì trình biên dịch gốc không bị ràng buộc (nhiều) theo thời gian trong việc tối ưu hóa mã, do đó, có thể sử dụng các thuật toán tốt nhất. Khả năng biên dịch tổng thể + thực thi cho trình biên dịch JIT chỉ có thể đánh bại thời gian thực hiện đối với mã được biên dịch nguyên gốc là 0.

Nhưng máy ảo của chúng tôi không chỉ giới hạn trong quá trình biên dịch JIT. Họ sử dụng các kỹ thuật biên dịch trước thời gian, bộ nhớ đệm, trao đổi nóng và tối ưu hóa thích ứng. Vì vậy, hãy sửa đổi tuyên bố của chúng tôi rằng hiệu suất là những gì người dùng nhìn thấy và giới hạn thời gian thực hiện chương trình (giả sử chúng tôi đã biên soạn AOT). Chúng ta có thể làm cho mã thực thi tương đương với trình biên dịch gốc (hoặc có lẽ tốt hơn?). Một yêu cầu lớn đối với máy ảo là chúng có thể tạo ra mã chất lượng tốt hơn sau đó là trình biên dịch gốc, bởi vì nó có quyền truy cập vào nhiều thông tin hơn - đó là quá trình đang chạy, như tần suất một hàm nhất định có thể được thực thi. VM sau đó có thể áp dụng tối ưu hóa thích ứng cho mã cần thiết nhất thông qua trao đổi nóng.

Tuy nhiên, có một vấn đề với lập luận này - nó giả định rằng tối ưu hóa theo hướng dẫn hồ sơ và những thứ tương tự là một cái gì đó duy nhất cho VM, điều này không đúng. Chúng ta cũng có thể áp dụng nó cho việc biên dịch riêng - bằng cách biên dịch ứng dụng của chúng ta với cấu hình được kích hoạt, ghi lại thông tin và sau đó biên dịch lại ứng dụng với hồ sơ đó. Có lẽ cũng đáng để chỉ ra rằng trao đổi nóng mã không phải là thứ mà chỉ có trình biên dịch JIT mới có thể làm được, chúng ta có thể làm điều đó cho mã gốc - mặc dù các giải pháp dựa trên JIT để thực hiện điều này dễ dàng hơn và dễ dàng hơn cho nhà phát triển. Vì vậy, câu hỏi lớn là: VM có thể cung cấp cho chúng tôi một số thông tin mà trình biên dịch gốc không thể, điều này có thể tăng hiệu năng của mã của chúng tôi không?

Tôi không thể nhìn thấy nó. Chúng ta cũng có thể áp dụng hầu hết các kỹ thuật của một VM thông thường cho mã gốc - mặc dù quá trình này có liên quan nhiều hơn. Tương tự, chúng ta có thể áp dụng bất kỳ tối ưu hóa nào của trình biên dịch gốc trở lại VM sử dụng trình biên dịch AOT hoặc tối ưu hóa thích ứng. Thực tế là sự khác biệt giữa mã chạy tự nhiên và mã chạy trong VM không lớn như chúng ta đã tin. Cuối cùng họ dẫn đến cùng một kết quả, nhưng họ có một cách tiếp cận khác để đạt được điều đó. VM sử dụng cách tiếp cận lặp để tạo mã được tối ưu hóa, trong đó trình biên dịch gốc mong đợi nó ngay từ đầu (và có thể được cải thiện bằng cách tiếp cận lặp).

Một lập trình viên C ++ có thể lập luận rằng anh ta cần tối ưu hóa từ việc di chuyển và không nên chờ đợi VM tìm ra cách thực hiện chúng, nếu có. Đây có lẽ là một điểm hợp lệ với công nghệ hiện tại của chúng tôi, vì mức độ tối ưu hóa hiện tại trong máy ảo của chúng tôi kém hơn những gì trình biên dịch gốc có thể cung cấp - nhưng điều đó có thể không phải luôn luôn xảy ra nếu các giải pháp AOT trong máy ảo của chúng tôi cải thiện, v.v.


0

Bài viết này là một bản tóm tắt của một tập hợp các bài đăng trên blog đang cố gắng so sánh tốc độ của c ++ và c # và các vấn đề bạn phải khắc phục trong cả hai ngôn ngữ để có được mã hiệu suất cao. Tóm tắt là 'thư viện của bạn quan trọng hơn bất cứ điều gì, nhưng nếu bạn ở trong c ++, bạn có thể vượt qua điều đó.' hoặc 'ngôn ngữ hiện đại có thư viện tốt hơn và do đó có được kết quả nhanh hơn với nỗ lực thấp hơn' tùy thuộc vào triết lý của bạn.


0

Tôi nghĩ rằng câu hỏi thực sự ở đây không phải là "cái nào nhanh hơn?" nhưng "cái nào có tiềm năng tốt nhất cho hiệu suất cao hơn?". Nhìn vào các điều khoản đó, C ++ rõ ràng chiến thắng - nó được biên dịch thành mã gốc, không có JITting, đó là mức độ trừu tượng thấp hơn, v.v.

Đó là xa câu chuyện đầy đủ.

Vì C ++ được biên dịch, mọi tối ưu hóa trình biên dịch phải được thực hiện tại thời điểm biên dịch và tối ưu hóa trình biên dịch phù hợp với một máy có thể hoàn toàn sai đối với máy khác. Đây cũng là trường hợp mà bất kỳ tối ưu hóa trình biên dịch toàn cầu nào cũng có thể và sẽ ưu tiên các thuật toán hoặc mẫu mã nhất định hơn các thuật toán khác.

Mặt khác, chương trình JITted sẽ tối ưu hóa tại thời điểm JIT, do đó, nó có thể rút ra một số thủ thuật mà chương trình được biên dịch trước không thể và có thể tối ưu hóa rất cụ thể cho máy mà nó thực sự đang chạy và mã mà nó thực sự đang chạy. Khi bạn vượt qua được chi phí ban đầu của JIT, nó có tiềm năng trong một số trường hợp sẽ nhanh hơn.

Trong cả hai trường hợp, việc triển khai hợp lý thuật toán và các trường hợp khác của lập trình viên không ngu ngốc sẽ có thể là các yếu tố quan trọng hơn nhiều, tuy nhiên - ví dụ, hoàn toàn có thể viết mã chuỗi hoàn toàn chết não trong C ++, thậm chí sẽ được bao bọc một ngôn ngữ kịch bản diễn giải.


3
"Tối ưu hóa trình biên dịch phù hợp với một máy có thể hoàn toàn sai đối với máy khác" Chà, điều đó không thực sự đáng trách về ngôn ngữ. Mã thực sự quan trọng về hiệu năng có thể được biên dịch riêng cho từng máy mà nó sẽ chạy, đây là mã không có trí tuệ nếu bạn biên dịch cục bộ từ nguồn ( -march=native). - "đó là mức độ trừu tượng thấp hơn" không thực sự đúng. C ++ chỉ sử dụng các khái niệm trừu tượng mức cao như Java (hoặc trên thực tế, các khái niệm cao hơn: lập trình hàm? Lập trình siêu mẫu?), Nó chỉ thực hiện các trừu tượng ít "sạch" hơn Java.
rẽ trái

"Mã thực sự quan trọng về hiệu năng có thể được biên dịch riêng cho từng máy mà nó sẽ chạy, đây là mã không có trí tuệ nếu bạn biên dịch cục bộ từ nguồn" - điều này thất bại vì giả định cơ bản rằng người dùng cuối cũng là lập trình viên.
Maximus Minimus

Không nhất thiết là người dùng cuối, chỉ là người chịu trách nhiệm cài đặt chương trình. Trên máy tính để bàn và thiết bị di động, thường người dùng cuối, nhưng đây không phải là những ứng dụng duy nhất có, chắc chắn không phải là những ứng dụng quan trọng nhất về hiệu năng. Và bạn không thực sự cần phải là một lập trình viên để xây dựng một chương trình từ nguồn, nếu nó có các kịch bản xây dựng được viết đúng như tất cả các dự án phần mềm mở / miễn phí tốt.
leftaroundabout

1
Mặc dù về lý thuyết là có, một JIT có thể tạo ra nhiều thủ thuật hơn trình biên dịch tĩnh, trong thực tế (đối với .NET, tôi cũng không biết java), nhưng thực tế nó không thực hiện bất kỳ điều gì trong số này. Gần đây tôi đã thực hiện một loạt phân tách mã .NET JIT và có tất cả các loại tối ưu hóa như nâng mã khỏi các vòng lặp, loại bỏ mã chết, v.v., mà .NET JIT đơn giản không làm được. Tôi ước điều đó sẽ xảy ra, nhưng này, nhóm cửa sổ bên trong microsoft đã cố gắng giết .NET trong nhiều năm, vì vậy tôi không nín thở
Orion Edwards

-1

Biên dịch JIT thực sự có tác động tiêu cực đến hiệu suất. Nếu bạn thiết kế trình biên dịch "hoàn hảo" và trình biên dịch JIT "hoàn hảo", tùy chọn đầu tiên sẽ luôn giành được hiệu suất.

Cả Java và C # đều được hiểu thành các ngôn ngữ trung gian và sau đó được biên dịch thành mã gốc khi chạy, làm giảm hiệu suất.

Nhưng bây giờ, sự khác biệt không rõ ràng đối với C #: Microsoft CLR tạo ra mã gốc khác nhau cho các CPU khác nhau, do đó làm cho mã hiệu quả hơn cho máy chạy trên nó, điều này không phải lúc nào cũng được thực hiện bởi trình biên dịch C ++.

PS C # được viết rất hiệu quả và nó không có nhiều lớp trừu tượng. Điều này không đúng với Java, không hiệu quả. Vì vậy, trong trường hợp này, với CLR tuyệt vời của nó, các chương trình C # thường hiển thị hiệu suất tốt hơn các chương trình C ++. Để biết thêm về .Net và CLR, hãy xem "CLR qua C #" của Jeffrey Richter .


8
Nếu JIT thực sự có tác động tiêu cực đến hiệu suất, chắc chắn nó sẽ không được sử dụng?
Zavior

2
@Zavior - Tôi không thể nghĩ ra câu trả lời hay cho câu hỏi của bạn, nhưng tôi không thấy cách JIT không thể thêm chi phí hiệu suất bổ sung - JIT là một quá trình bổ sung phải hoàn thành trong thời gian chạy yêu cầu tài nguyên phát sinh ' T được dành để thực hiện chương trình, trong khi một ngôn ngữ được biên dịch đầy đủ là 'sẵn sàng để đi'.
Ẩn danh

3
JIT có tác động tích cực đến hiệu suất, không phải là tiêu cực, nếu bạn đặt nó vào ngữ cảnh - Đó là biên dịch mã byte thành mã máy trước khi chạy nó. Các kết quả cũng có thể được lưu trữ, cho phép nó chạy nhanh hơn mã byte tương đương được diễn giải.
Casey Kuball

3
JIT (hay đúng hơn là cách tiếp cận mã byte) không được sử dụng cho hiệu suất, nhưng để thuận tiện. Thay vì các tệp nhị phân trước khi xây dựng cho mỗi nền tảng (hoặc một tập hợp con chung, là tối ưu phụ cho mỗi nền tảng), bạn chỉ biên dịch nửa chừng và để trình biên dịch JIT làm phần còn lại. 'Viết một lần, triển khai ở bất cứ đâu' là lý do tại sao nó được thực hiện theo cách này. Sự tiện lợi có thể có chỉ với một thông dịch bytecode, nhưng JIT không làm cho nó nhanh hơn so với phiên dịch thô (mặc dù không nhất thiết phải đủ nhanh để đánh bại một giải pháp trước biên soạn; biên dịch JIT làm mất thời gian, và kết quả không phải lúc nào tạo nên cho nó).
tdammers

4
@Tdammmers, thực sự có một thành phần hiệu suất quá. Xem java.sun.com/products/hotspot/whitepaper.html . Tối ưu hóa có thể bao gồm những thứ như điều chỉnh động để cải thiện dự đoán nhánh và truy cập bộ đệm, nội tuyến động, khử ảo, vô hiệu hóa kiểm tra giới hạn và hủy kiểm soát vòng lặp. Yêu cầu là trong nhiều trường hợp, những khoản này có thể nhiều hơn chi trả cho chi phí của JIT.
Charles E. Grant
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.