Có thực sự nhanh hơn khi sử dụng say (i << 3) + (i << 1) để nhân với 10 so với sử dụng i * 10 trực tiếp không?
Nó có thể có hoặc không có trên máy của bạn - nếu bạn quan tâm, hãy đo lường mức độ sử dụng trong thế giới thực của bạn.
Một nghiên cứu trường hợp - từ 486 đến lõi i7
Điểm chuẩn là rất khó để làm có ý nghĩa, nhưng chúng ta có thể xem xét một vài sự kiện. Từ http://www.penguin.cz/~literakl/intel/s.html#SAL và http://www.penguin.cz/~literakl/intel/i.html#IMUL chúng tôi có ý tưởng về chu kỳ đồng hồ x86 cần thiết cho sự thay đổi số học và nhân. Giả sử chúng tôi dính vào "486" (cái mới nhất được liệt kê), thanh ghi 32 bit và ngay lập tức, IMUL thực hiện 13-42 chu kỳ và IDIV 44. Mỗi SAL mất 2 và thêm 1, do đó, ngay cả với một vài trong số chúng cùng thay đổi bề ngoài như một người chiến thắng
Những ngày này, với lõi i7:
(từ http://software.intel.com/en-us/forums/showthread.php?t=61481 )
Độ trễ là 1 chu kỳ cho phép cộng số nguyên và 3 chu kỳ cho phép nhân số nguyên . Bạn có thể tìm thấy độ trễ và thông số trong Phụ lục C của "Hướng dẫn tham khảo tối ưu hóa kiến trúc Intel® 64 và IA-32", được đặt trên http://www.intel.com/products/ Processor /mans / .
(từ một số Intel blurb)
Sử dụng SSE, Core i7 có thể ban hành các lệnh cộng và nhân đồng thời, dẫn đến tốc độ cao nhất là 8 thao tác dấu phẩy động (FLOP) trên mỗi chu kỳ đồng hồ
Điều đó cho bạn một ý tưởng về những điều đã đến. Các câu đố tối ưu hóa - như thay đổi bit so với*
- đã được thực hiện nghiêm túc ngay cả vào những năm 90 chỉ là lỗi thời. Dịch chuyển bit vẫn nhanh hơn, nhưng đối với mul / div không có công suất hai lần vào thời điểm bạn thực hiện tất cả các ca của mình và thêm kết quả thì nó lại chậm hơn. Sau đó, nhiều hướng dẫn hơn có nghĩa là lỗi bộ nhớ cache nhiều hơn, các vấn đề tiềm ẩn hơn trong đường ống, sử dụng nhiều thanh ghi tạm thời hơn có thể có nghĩa là tiết kiệm và khôi phục nội dung đăng ký từ ngăn xếp ... nó nhanh chóng trở nên quá phức tạp để định lượng tất cả các tác động một cách dứt khoát nhưng chúng chủ yếu là tiêu cực.
chức năng trong mã nguồn so với thực hiện
Tổng quát hơn, câu hỏi của bạn được gắn thẻ C và C ++. Là ngôn ngữ thế hệ thứ 3, chúng được thiết kế đặc biệt để ẩn chi tiết của tập lệnh CPU bên dưới. Để đáp ứng Tiêu chuẩn ngôn ngữ của họ, họ phải hỗ trợ các hoạt động nhân và dịch chuyển (và nhiều hoạt động khác) ngay cả khi phần cứng cơ bản không hoạt động . Trong những trường hợp như vậy, họ phải tổng hợp kết quả cần thiết bằng nhiều hướng dẫn khác. Tương tự, họ phải cung cấp phần mềm hỗ trợ cho các hoạt động của dấu phẩy động nếu CPU thiếu nó và không có FPU. CPU hiện đại đều hỗ trợ *
và<<
, do đó, điều này có vẻ vô lý về mặt lý thuyết và lịch sử, nhưng điều quan trọng là quyền tự do lựa chọn thực hiện theo cả hai cách: ngay cả khi CPU có một lệnh thực hiện thao tác được yêu cầu trong mã nguồn trong trường hợp chung, trình biên dịch miễn phí chọn một cái gì đó khác mà nó thích bởi vì nó tốt hơn cho trường hợp cụ thể mà người biên soạn phải đối mặt.
Ví dụ (với ngôn ngữ lắp ráp giả thuyết)
source literal approach optimised approach
#define N 0
int x; .word x xor registerA, registerA
x *= N; move x -> registerA
move x -> registerB
A = B * immediate(0)
store registerA -> x
...............do something more with x...............
Các hướng dẫn như độc quyền hoặc ( xor
) không có mối quan hệ với mã nguồn, nhưng xor-ing bất cứ thứ gì với chính nó sẽ xóa tất cả các bit, do đó, nó có thể được sử dụng để đặt một cái gì đó thành 0. Mã nguồn ngụ ý địa chỉ bộ nhớ có thể không được sử dụng.
Những loại hack này đã được sử dụng miễn là có máy tính xung quanh. Trong những ngày đầu của 3GL, để đảm bảo nhà phát triển thu được đầu ra của trình biên dịch phải đáp ứng các nhà phát triển ngôn ngữ lắp ráp tối ưu hóa tay cứng hiện có. cộng đồng rằng mã được sản xuất không chậm hơn, dài dòng hơn hoặc tệ hơn. Trình biên dịch nhanh chóng chấp nhận rất nhiều tối ưu hóa - chúng trở thành một kho lưu trữ tập trung tốt hơn bất kỳ lập trình viên ngôn ngữ lắp ráp riêng lẻ nào, mặc dù luôn có khả năng họ bỏ lỡ một tối ưu hóa cụ thể xảy ra rất quan trọng trong một trường hợp cụ thể - đôi khi con người có thể hãy tìm ra và tìm kiếm thứ gì đó tốt hơn trong khi các trình biên dịch chỉ làm như họ đã nói cho đến khi ai đó cung cấp trải nghiệm trở lại cho họ.
Vì vậy, ngay cả khi dịch chuyển và thêm vẫn nhanh hơn trên một số phần cứng cụ thể, thì trình biên dịch có khả năng đã hoạt động chính xác khi nó vừa an toàn vừa có lợi.
Bảo trì
Nếu phần cứng của bạn thay đổi, bạn có thể biên dịch lại và nó sẽ xem xét CPU mục tiêu và đưa ra lựa chọn tốt nhất khác, trong khi bạn không bao giờ muốn xem lại "tối ưu hóa" của mình hoặc liệt kê môi trường biên dịch nào nên sử dụng phép nhân và nên thay đổi. Hãy nghĩ về tất cả các "tối ưu hóa" không thay đổi hai bit được viết cách đây hơn 10 năm, hiện đang làm chậm mã mà chúng đang chạy khi chạy trên các bộ xử lý hiện đại ...!
Rất may, các trình biên dịch tốt như GCC thường có thể thay thế một loạt các bit và số học bằng phép nhân trực tiếp khi bất kỳ tối ưu hóa nào được bật (tức là ...main(...) { return (argc << 4) + (argc << 2) + argc; }
-> imull $21, 8(%ebp), %eax
) để việc biên dịch lại có thể giúp ngay cả khi không sửa mã, nhưng điều đó không được đảm bảo.
Mã bẻ khóa kỳ lạ thực hiện phép nhân hoặc chia ít biểu hiện hơn nhiều so với những gì bạn đang cố gắng đạt được về mặt khái niệm, vì vậy các nhà phát triển khác sẽ bối rối vì điều đó, và một lập trình viên bối rối sẽ giới thiệu các lỗi hoặc loại bỏ thứ gì đó thiết yếu trong nỗ lực khôi phục sự tỉnh táo. Nếu bạn chỉ làm những việc không rõ ràng khi chúng thực sự có lợi, và sau đó ghi lại chúng thật tốt (nhưng đừng ghi lại những thứ khác trực quan), mọi người sẽ hạnh phúc hơn.
Giải pháp chung so với giải pháp từng phần
Nếu bạn có thêm kiến thức, chẳng hạn như bạn int
sẽ thực sự chỉ lưu trữ các giá trị x
, y
và z
, sau đó bạn có thể tìm ra một số hướng dẫn phù hợp với các giá trị đó và giúp bạn nhận được kết quả nhanh hơn so với khi trình biên dịch không có cái nhìn sâu sắc và cần một triển khai hoạt động cho tất cả các int
giá trị. Ví dụ, hãy xem xét câu hỏi của bạn:
Nhân và chia có thể đạt được bằng cách sử dụng các toán tử bit ...
Bạn minh họa phép nhân, nhưng làm thế nào về phân chia?
int x;
x >> 1; // divide by 2?
Theo tiêu chuẩn C ++ 5,8:
-3- Giá trị của E1 >> E2 là vị trí bit E2 dịch chuyển phải. Nếu E1 có loại không dấu hoặc nếu E1 có loại đã ký và giá trị không âm, thì giá trị của kết quả là phần không thể tách rời của thương số của E1 chia cho đại lượng 2 được nâng lên công suất E2. Nếu E1 có loại đã ký và giá trị âm, giá trị kết quả được xác định theo thực hiện.
Vì vậy, sự thay đổi bit của bạn có kết quả xác định khi thực hiện x
là âm: nó có thể không hoạt động theo cùng một cách trên các máy khác nhau. Nhưng, /
công trình dự đoán xa hơn. (Nó cũng có thể không hoàn toàn nhất quán, vì các máy khác nhau có thể có các cách biểu diễn khác nhau về số âm và do đó các phạm vi khác nhau ngay cả khi có cùng số bit tạo thành biểu diễn.)
Bạn có thể nói "Tôi không quan tâm ... đó int
là lưu trữ tuổi của nhân viên, điều đó không bao giờ có thể là tiêu cực". Nếu bạn có loại hiểu biết đặc biệt đó, thì có - >>
tối ưu hóa an toàn của bạn có thể được trình biên dịch chuyển qua trừ khi bạn thực hiện nó một cách rõ ràng trong mã của bạn. Tuy nhiên, nó rất rủi ro và hiếm khi hữu ích vì phần lớn thời gian bạn sẽ không có loại hiểu biết này và các lập trình viên khác làm việc với cùng một mã sẽ không biết rằng bạn đặt cược cho nhà cái một số kỳ vọng bất thường về dữ liệu bạn ' sẽ xử lý ... những gì dường như là một thay đổi hoàn toàn an toàn đối với họ có thể phản tác dụng vì "tối ưu hóa" của bạn.
Có bất kỳ loại đầu vào không thể được nhân hoặc chia theo cách này?
Có ... như đã đề cập ở trên, các số âm có hành vi được xác định khi thực hiện "chia" cho dịch chuyển bit.