2010-08-15 33 views
7

Từ http://www.boost.org/community/implementation_variations.htmlModern CPU Nội Vòng gián tiếp tối ưu hóa

" ... khác biệt mã hóa như thay đổi một lớp từ ảo cho các thành viên phi ảo hoặc loại bỏ một mức gián tiếp không có khả năng thực hiện bất kỳ sự khác biệt thể đo lường được trừ sâu trong một vòng lặp bên trong. Và ngay cả trong vòng lặp bên trong, các CPU hiện đại thường thực thi các chuỗi mã cạnh tranh như vậy trong cùng một số chu kỳ đồng hồ! "

Tôi đang cố gắng hiểu phần "ngay cả trong vòng lặp bên trong". Cụ thể những cơ chế nào mà CPU thực hiện để thực thi hai mã (virtual vs non-virtual hoặc một mức bổ sung indirection) trong cùng một số chu kỳ đồng hồ? Tôi biết về hướng dẫn pipelining và bộ nhớ đệm, nhưng làm thế nào là nó có thể thực hiện một cuộc gọi ảo trong cùng một số chu kỳ đồng hồ như một cuộc gọi không ảo? Làm thế nào là sự mất tích "bị mất"?

Trả lời

4

Caching (ví dụ branch target caching), đơn vị song song tải (một phần của pipelining, mà còn những thứ như "hit dưới bỏ lỡ" mà không trì hoãn các đường ống dẫn), và out-of-order execution có nhiều khả năng để giúp biến đổi một load-load - branch vào cái gì đó gần với một số branch cố định. Hướng dẫn gấp/loại bỏ (thuật ngữ thích hợp cho điều này là gì?) Trong giai đoạn giải mã hoặc dự đoán nhánh của đường ống cũng có thể đóng góp. Tuy nhiên, tất cả điều này phụ thuộc vào rất nhiều thứ khác nhau: có bao nhiêu mục tiêu chi nhánh khác nhau (ví dụ: bạn có thể kích hoạt nhiều quá tải ảo khác nhau), bao nhiêu thứ bạn lặp lại (là bộ nhớ cache mục tiêu chi nhánh). "ấm"? làm thế nào về icache/dcache?), làm thế nào các bảng ảo hoặc bảng indirection được đặt ra trong bộ nhớ (là họ thân thiện với bộ nhớ cache, hoặc là mỗi tải vtable mới có thể đuổi một vtable cũ?), là bộ nhớ cache được đã bị vô hiệu nhiều lần do ping-ponging đa lõi, v.v ...

(Disclaimer: Tôi chắc chắn không phải là chuyên gia ở đây, và rất nhiều kiến ​​thức của tôi đến từ việc nghiên cứu bộ vi xử lý nhúng theo thứ tự) Nếu bạn có chỉnh sửa, hãy bình luận!)

Cách chính xác để xác định xem đó có phải là vấn đề đối với một chương trình cụ thể hay không là tất nhiên đối với hồ sơ. Nếu bạn có thể, làm như vậy với sự trợ giúp của bộ đếm phần cứng - họ có thể cho bạn biết rất nhiều về những gì đang xảy ra trong các giai đoạn khác nhau của đường ống.


Edit:

Như Hans passant chỉ ra trong một bình luận trên Modern CPU Inner Loop Indirection Optimizations, chìa khóa để nhận được hai điều này để có cùng một lượng thời gian là khả năng một cách hiệu quả "nghỉ hưu" nhiều hơn một hướng dẫn mỗi chu kỳ. Hướng dẫn loại bỏ có thể giúp với điều này, nhưng superscalar design có lẽ là quan trọng hơn (nhấn dưới bỏ lỡ là một ví dụ rất nhỏ và cụ thể, đơn vị tải đầy đủ dự phòng có thể là một tốt hơn).

Hãy lấy một tình huống lý tưởng, và giả sử một chi nhánh trực tiếp chỉ là một hướng dẫn:

branch dest 

... và một chi nhánh gián tiếp là ba (có thể bạn có thể lấy nó ra làm đôi, nhưng nó lớn hơn một):

load vtable from this 
load dest from vtable 
branch dest 

giả sử một tình huống hoàn toàn hoàn hảo: * này và toàn bộ vtable là trong bộ nhớ cache L1, L1 Cache là đủ nhanh để hỗ trợ khấu hao theo một chu kỳ mỗi chi phí hướng dẫn cho hai tải. (Bạn thậm chí có thể giả định bộ xử lý sắp xếp lại các tải và trộn chúng với các lệnh trước đó để cho phép thời gian để chúng hoàn thành trước nhánh; nó không quan trọng đối với ví dụ này.) Giả sử bộ nhớ cache mục tiêu là nóng, và không có đường dẫn chi phí tuôn ra cho chi nhánh và lệnh chi nhánh đi xuống một chu kỳ đơn (khấu hao).

Thời gian lý thuyết tối thiểu cho ví dụ đầu tiên là 1 chu kỳ (khấu hao).

Lý thuyết tối thiểu cho ví dụ thứ hai, loại bỏ chỉ dẫn vắng mặt hoặc các đơn vị chức năng dự phòng hoặc thứ gì đó sẽ cho phép gỡ bỏ nhiều lệnh trên mỗi chu kỳ, là 3 chu kỳ (có 3 hướng dẫn)!

Tải gián tiếp sẽ luôn chậm hơn, vì có nhiều hướng dẫn hơn, cho đến khi bạn tiếp cận với thiết bị siêu âm cho phép gỡ bỏ nhiều lệnh trên mỗi chu kỳ.

Khi bạn có điều này, mức tối thiểu cho cả hai ví dụ sẽ trở thành cái gì đó giữa 0 và 1 chu kỳ, một lần nữa, với điều kiện mọi thứ khác là lý tưởng. Có thể cho rằng bạn phải có nhiều hoàn cảnh lý tưởng hơn cho ví dụ thứ hai để đạt được mức tối thiểu lý thuyết đó so với ví dụ đầu tiên, nhưng bây giờ có thể.

Trong một số trường hợp bạn quan tâm, có thể bạn sẽ không đạt đến mức tối thiểu đó cho một trong hai ví dụ. Bộ nhớ cache mục tiêu chi nhánh sẽ bị lạnh hoặc vtable sẽ không nằm trong bộ đệm dữ liệu hoặc máy sẽ không có khả năng sắp xếp lại hướng dẫn để tận dụng tối đa các đơn vị chức năng dự phòng.

... đây là nơi mà tiểu sử xuất hiện, thường là một ý tưởng hay.

Bạn có thể chỉ espouse một hoang tưởng nhẹ về ảo ở nơi đầu tiên. Xem Noel Llopis's article on data oriented design, xuất sắc Pitfalls of Object-Oriented Programming slidesMike Acton's grumpy-yet-educational presentations. Bây giờ bạn đã đột nhiên chuyển sang các mẫu mà CPU đã có thể hài lòng với, nếu bạn đang xử lý nhiều dữ liệu.

Tính năng ngôn ngữ cấp cao như ảo thường là sự cân bằng giữa tính biểu cảm và kiểm soát. Mặc dù vậy, tôi thực sự nghĩ rằng bằng cách tăng nhận thức của bạn về những gì ảo thực sự đang làm (đừng ngại đọc chế độ xem tháo gỡ theo thời gian, và chắc chắn xem qua hướng dẫn sử dụng kiến ​​trúc của CPU), bạn sẽ có xu hướng sử dụng nó khi nó có ý nghĩa và không khi nó không, và một profiler có thể bao gồm phần còn lại nếu cần thiết.

Tuyên bố một kích thước phù hợp với tất cả về "không sử dụng ảo" hoặc "sử dụng ảo không có khả năng tạo sự khác biệt có thể đo lường" khiến tôi khó chịu. Thực tế thường phức tạp hơn, và bạn sẽ ở trong tình huống mà bạn quan tâm đủ để hồ sơ hoặc tránh nó, hoặc bạn ở 95% kia, nơi nó có thể không đáng quan tâm ngoại trừ nội dung giáo dục có thể.

+0

Cảm ơn câu trả lời chi tiết. Tôi đã xem các liên kết trên "Thiết kế định hướng dữ liệu" và "Những cạm bẫy của OOP" nhưng tôi nghĩ có nhiều hơn những gì được nói. Điều này đòi hỏi nhiều công việc hơn, nhưng ban đầu tôi nghĩ rằng bố trí bộ nhớ cho hiệu quả bộ nhớ cache nên được xử lý bởi một lớp cơ bản mà không bị mất "thiết kế thông thường" cho hiệu quả. –

+0

Tôi nghĩ rằng tất cả phụ thuộc vào những gì bạn xem xét "thiết kế thông thường". =) Có lẽ tôi đã ở trong ngành công nghiệp gamedev quá lâu; nó thực sự là tất cả về dữ liệu ở đây. Bắt đầu xem xét các hệ thống khi di chuyển dữ liệu xung quanh và xử lý nó hiệu quả đôi khi làm cho thiết kế hệ thống dễ dàng hơn. Lặn vào khái niệm ngôn ngữ chức năng, loại chuyển đổi này dường như không xa tầm tay. – leander

4

Đường ống là cách chính.

Có thể mất 20 chu kỳ đồng hồ để tải hướng dẫn, giải mã, thực hiện tác vụ và tải các tham chiếu bộ nhớ gián tiếp. Nhưng do pipleline, bộ vi xử lý có thể thực thi các phần của 19 lệnh khác cùng một lúc trong các giai đoạn khác nhau của đường ống, cung cấp thông lượng tổng thể là 1 lệnh mỗi chu kỳ đồng hồ bất kể nó mất bao lâu để nạp lệnh đó thông qua đường ống.

+0

Tôi không chắc mình hoàn toàn đồng ý ở đây. Trong khi đường ống phân bổ chi phí của các hướng dẫn, chúng không phải (của riêng mình) có khả năng loại bỏ hoàn toàn chúng. Vì vậy, trong một CPU chỉ đường ống, một nhánh tải + tải + luôn mất nhiều thời gian hơn một chuỗi lệnh ngắn hơn (ví dụ như chỉ một nhánh). Do đó, cấu trúc đường ống giảm bớt sự bao gồm của những thứ khác như loại bỏ/gấp chỉ dẫn thông qua dự đoán nhánh ... Và với OOO bạn có thể tiến gần hơn đến 1cyc/instr lý tưởng ... Nhưng điều đó có vẻ giống như các công cụ vượt quá pipelining. Nó có thể chỉ là ngữ nghĩa tôi đang quibbling về. =) – leander

+0

Nó không thực sự quan trọng mà một số hướng dẫn sẽ không làm bất cứ điều gì trong tất cả các bước đường ống dẫn. Vấn đề là dù hướng dẫn đơn giản hay phức tạp là bao nhiêu, một hướng dẫn sẽ đến cuối đường ống và do đó được "hoàn thành" mọi chu kỳ đồng hồ. – jcoder

+1

Thiết kế siêu vô hướng đóng một vai trò quá. Cho phép lõi nghỉ hưu nhiều hơn một hướng dẫn cho mỗi chu kỳ. –

1

Điều gì xảy ra, tôi nghĩ rằng bộ xử lý có bộ nhớ cache đặc biệt chứa vị trí và mục tiêu của các nhánh và các bước nhảy gián tiếp. Nếu một bước nhảy gián tiếp được gặp ở mức $ 12345678 và lần cuối cùng nó gặp phải là $ 12348765, bộ xử lý có thể bắt đầu thực hiện đầu cơ các hướng dẫn tại địa chỉ $ 12348765 ngay cả trước khi nó giải quyết địa chỉ của chi nhánh. Trong nhiều trường hợp, trong vòng lặp bên trong của một hàm, một bước nhảy gián tiếp cụ thể sẽ luôn nhảy đến cùng một địa chỉ trong suốt thời gian của vòng lặp. Bộ nhớ cache nhảy gián tiếp có thể tránh được các hình phạt phân nhánh.

0

Nếu CPU đã có địa chỉ bộ nhớ trong bộ nhớ cache, sau đó thực hiện lệnh tải là tầm thường, nếu điều đó.