Tôi nghĩ sẽ hữu ích hơn cho người hỏi khi có câu trả lời khác biệt hơn, bởi vì tôi thấy một số giả định chưa được giải thích trong các câu hỏi và trong một số câu trả lời hoặc nhận xét.
Thời gian chạy tương đối của dịch chuyển và nhân không liên quan gì đến C. Khi tôi nói C, tôi không có nghĩa là trường hợp thực hiện cụ thể, chẳng hạn như phiên bản GCC đó, hoặc ngôn ngữ. Tôi không có ý lấy quảng cáo vô lý này, nhưng sử dụng một ví dụ cực đoan để minh họa: bạn có thể thực hiện trình biên dịch C hoàn toàn tuân thủ tiêu chuẩn và nhân lên mất một giờ, trong khi việc chuyển đổi mất một phần nghìn giây - hoặc ngược lại. Tôi không biết về bất kỳ hạn chế hiệu suất như vậy trong C hoặc C ++.
Bạn có thể không quan tâm đến tính kỹ thuật này trong tranh luận. Ý định của bạn có lẽ là chỉ kiểm tra hiệu suất tương đối của việc thực hiện thay đổi so với nhân và bạn đã chọn C, vì nó thường được coi là ngôn ngữ lập trình cấp thấp, vì vậy người ta có thể mong đợi mã nguồn của nó sẽ dịch sang các hướng dẫn tương ứng trực tiếp hơn. Những câu hỏi như vậy rất phổ biến và tôi nghĩ rằng một câu trả lời hay nên chỉ ra rằng ngay cả trong C, mã nguồn của bạn không chuyển thành hướng dẫn trực tiếp như bạn nghĩ trong một ví dụ cụ thể. Tôi đã cung cấp cho bạn một số kết quả tổng hợp có thể dưới đây.
Đây là nơi các bình luận đặt câu hỏi về tính hữu ích của việc thay thế sự tương đương này trong phần mềm trong thế giới thực. Bạn có thể thấy một số trong các bình luận cho câu hỏi của bạn, chẳng hạn như từ Eric Lippert. Nó phù hợp với phản ứng mà bạn thường sẽ nhận được từ các kỹ sư dày dạn hơn để đáp ứng với các tối ưu hóa như vậy. Nếu bạn sử dụng dịch chuyển nhị phân trong mã sản xuất như một phương tiện nhân lên và chia, mọi người rất có thể sẽ chùn bước trước mã của bạn và có một số phản ứng cảm xúc ("Tôi đã nghe thấy tuyên bố vô nghĩa này về JavaScript vì lợi ích của trời.") nó có thể không có ý nghĩa với các lập trình viên mới làm quen, trừ khi họ hiểu rõ hơn lý do cho những phản ứng đó.
Những lý do đó chủ yếu là sự kết hợp giữa khả năng đọc giảm và vô ích của việc tối ưu hóa như vậy, vì bạn có thể đã phát hiện ra khi so sánh hiệu suất tương đối của chúng. Tuy nhiên, tôi không nghĩ rằng mọi người sẽ có phản ứng mạnh mẽ như vậy nếu thay thế sự thay đổi cho phép nhân là ví dụ duy nhất của việc tối ưu hóa như vậy. Những câu hỏi như của bạn thường xuyên xuất hiện dưới nhiều hình thức và trong nhiều bối cảnh khác nhau. Tôi nghĩ điều mà nhiều kỹ sư cao cấp thực sự phản ứng mạnh mẽ như vậy, ít nhất là đôi khi tôi có, là có khả năng gây ra phạm vi tác hại rộng lớn hơn nhiều khi mọi người sử dụng tối ưu hóa vi mô một cách tự do trên cơ sở mã. Nếu bạn làm việc tại một công ty như Microsoft trên cơ sở mã lớn, bạn sẽ dành nhiều thời gian để đọc mã nguồn của các kỹ sư khác hoặc cố gắng xác định mã nhất định trong đó. Nó thậm chí có thể là mã của riêng bạn mà bạn sẽ cố gắng hiểu trong vài năm nữa, đặc biệt là vào một số thời điểm không thuận lợi nhất, chẳng hạn như khi bạn phải khắc phục sự cố ngừng sản xuất sau cuộc gọi mà bạn nhận được khi đang nhắn tin làm nhiệm vụ vào tối thứ sáu, chuẩn bị cho một đêm vui vẻ với bạn bè Nếu bạn dành nhiều thời gian cho việc đọc mã, bạn sẽ đánh giá cao nó có thể đọc được càng tốt. Hãy tưởng tượng đọc cuốn tiểu thuyết yêu thích của bạn, nhưng nhà xuất bản đã quyết định phát hành phiên bản mới nơi họ sử dụng abbrv. tất cả ovr th plc bcs thy thnk nó svs spc. Đó là giống như các phản ứng mà các kỹ sư khác có thể có đối với mã của bạn, nếu bạn rắc chúng với các tối ưu hóa như vậy. Như các câu trả lời khác đã chỉ ra, tốt hơn là nói rõ ý của bạn là gì,
Tuy nhiên, ngay cả trong những môi trường đó, bạn có thể thấy mình đang giải quyết một câu hỏi phỏng vấn mà bạn dự kiến sẽ biết điều này hoặc một số tương đương khác. Biết chúng không phải là xấu và một kỹ sư giỏi sẽ nhận thức được hiệu ứng số học của dịch chuyển nhị phân. Lưu ý rằng tôi đã không nói rằng điều này làm cho một kỹ sư giỏi, nhưng theo tôi thì một kỹ sư giỏi sẽ biết. Cụ thể, bạn vẫn có thể tìm thấy một số người quản lý, thường là vào cuối vòng phỏng vấn của bạn, người sẽ cười toe toét với bạn trước sự vui mừng tiết lộ "mẹo" kỹ thuật thông minh này cho bạn trong một câu hỏi mã hóa và chứng minh rằng anh ấy / cô ấy cũng vậy, từng là hoặc là một trong những kỹ sư hiểu biết và không "chỉ" một người quản lý. Trong những tình huống đó, chỉ cần cố gắng để trông ấn tượng và cảm ơn anh ấy / cô ấy cho cuộc phỏng vấn khai sáng.
Tại sao bạn không thấy sự khác biệt về tốc độ trong C? Câu trả lời rất có thể là cả hai đều dẫn đến cùng một mã lắp ráp:
int shift(int i) { return i << 2; }
int multiply(int i) { return i * 2; }
Cả hai có thể biên dịch thành
shift(int):
lea eax, [0+rdi*4]
ret
Trên GCC mà không tối ưu hóa, tức là sử dụng cờ "-O0", bạn có thể nhận được điều này:
shift(int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov eax, DWORD PTR [rbp-4]
sal eax, 2
pop rbp
ret
multiply(int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov eax, DWORD PTR [rbp-4]
add eax, eax
pop rbp
ret
Như bạn có thể thấy, việc chuyển "-O0" cho GCC không có nghĩa là nó sẽ không thông minh về loại mã mà nó tạo ra. Cụ thể, lưu ý rằng ngay cả trong trường hợp này, trình biên dịch đã tránh sử dụng một lệnh nhân. Bạn có thể lặp lại cùng một thí nghiệm với sự dịch chuyển của các số khác và thậm chí nhân với các số không phải là lũy thừa của hai. Rất có thể là trên nền tảng của bạn, bạn sẽ thấy sự kết hợp của ca và bổ sung, nhưng không có phép nhân. Có vẻ như một sự trùng hợp ngẫu nhiên đối với trình biên dịch rõ ràng là tránh sử dụng phép nhân trong tất cả các trường hợp đó nếu phép nhân và dịch chuyển thực sự có cùng chi phí, phải không? Nhưng tôi không có ý cung cấp giả thuyết, vì vậy chúng ta hãy tiếp tục.
Bạn có thể chạy lại bài kiểm tra của mình với đoạn mã trên và xem bây giờ bạn có nhận thấy sự khác biệt về tốc độ không. Mặc dù sau đó, bạn không kiểm tra sự thay đổi so với nhân, như bạn có thể thấy khi không có phép nhân, nhưng mã được tạo bởi một bộ cờ nhất định của GCC cho các hoạt động C của ca và nhân trong một trường hợp cụ thể . Vì vậy, trong một thử nghiệm khác, bạn có thể chỉnh sửa mã lắp ráp bằng tay và thay vào đó sử dụng hướng dẫn "imul" trong mã cho phương thức "nhân".
Nếu bạn muốn đánh bại một số thông minh của trình biên dịch, bạn có thể định nghĩa một phương pháp nhân và thay đổi tổng quát hơn và sẽ kết thúc bằng một cái gì đó như thế này:
int shift(int i, int j) { return i << j; }
int multiply(int i, int j) { return i * j; }
Mà có thể mang lại mã lắp ráp sau đây:
shift(int, int):
mov eax, edi
mov ecx, esi
sal eax, cl
ret
multiply(int, int):
mov eax, edi
imul eax, esi
ret
Cuối cùng chúng ta cũng có, ngay cả ở mức tối ưu hóa cao nhất của GCC 4.9, biểu thức trong hướng dẫn lắp ráp mà bạn có thể mong đợi khi ban đầu đặt ra trong bài kiểm tra của mình. Tôi nghĩ rằng bản thân nó có thể là một bài học quan trọng trong tối ưu hóa hiệu suất. Chúng ta có thể thấy sự khác biệt mà nó tạo ra để thay thế các biến cho các hằng số cụ thể trong mã của chúng ta, về mặt thông minh mà trình biên dịch có thể áp dụng. Tối ưu hóa vi mô như thay thế nhân bội là một số tối ưu hóa ở mức rất thấp mà trình biên dịch thường có thể dễ dàng thực hiện. Các tối ưu hóa khác có ảnh hưởng lớn hơn đến hiệu suất đòi hỏi sự hiểu biết về ý định của mãmà trình biên dịch thường không thể truy cập được hoặc chỉ có thể đoán được bởi một số heuristic. Đó là nơi bạn với tư cách là một kỹ sư phần mềm đến và nó chắc chắn không liên quan đến việc nhân thay thế bằng ca. Nó liên quan đến các yếu tố như tránh một cuộc gọi dự phòng đến một dịch vụ tạo ra I / O và có thể chặn một quy trình. Nếu bạn truy cập vào đĩa cứng của bạn hoặc, thần cấm, đến một cơ sở dữ liệu từ xa để lấy một số dữ liệu bổ sung mà bạn có thể có được từ những gì bạn đã có trong bộ nhớ, thời gian bạn chờ đợi vượt xa việc thực hiện hàng triệu hướng dẫn. Bây giờ, tôi nghĩ rằng chúng tôi đã đi lạc một chút so với câu hỏi ban đầu của bạn, nhưng tôi nghĩ rằng việc chỉ ra điều này cho một người hỏi, đặc biệt nếu chúng tôi cho rằng ai đó mới bắt đầu nắm bắt được bản dịch và thực thi mã,
Vì vậy, cái nào sẽ nhanh hơn? Tôi nghĩ rằng đó là một cách tiếp cận tốt mà bạn đã chọn để thực sự kiểm tra sự khác biệt hiệu suất. Nhìn chung, rất dễ bị bất ngờ bởi hiệu năng thời gian chạy của một số thay đổi mã. Có nhiều kỹ thuật bộ xử lý hiện đại sử dụng và sự tương tác giữa các phần mềm cũng có thể phức tạp. Ngay cả khi bạn sẽ nhận được kết quả hiệu suất có lợi cho một thay đổi nhất định trong một tình huống, tôi nghĩ thật nguy hiểm khi kết luận rằng loại thay đổi này sẽ luôn mang lại lợi ích hiệu suất. Tôi nghĩ thật nguy hiểm khi chạy thử nghiệm như vậy một lần, nói "Được rồi, giờ tôi biết cái nào nhanh hơn!" và sau đó áp dụng bừa bãi cùng một tối ưu hóa đó vào mã sản xuất mà không lặp lại các phép đo của bạn.
Vậy điều gì sẽ xảy ra nếu sự dịch chuyển nhanh hơn phép nhân? Chắc chắn có dấu hiệu tại sao điều đó là đúng. GCC, như bạn có thể thấy ở trên, dường như nghĩ (ngay cả khi không tối ưu hóa) rằng tránh nhân trực tiếp theo hướng dẫn khác là một ý tưởng tốt. Các Intel 64 và IA-32 Kiến trúc Optimization Reference Manual sẽ cung cấp cho bạn một ý tưởng về chi phí tương đối của hướng dẫn CPU. Một tài nguyên khác, tập trung nhiều hơn vào độ trễ và thông lượng hướng dẫn, là http://www.agner.org/optizes/in cản_tables.pdf. Lưu ý rằng chúng không phải là một công cụ dự báo tốt về thời gian chạy tuyệt đối, mà là hiệu suất của các hướng dẫn liên quan đến nhau. Trong một vòng lặp chặt chẽ, vì thử nghiệm của bạn đang mô phỏng, nên số liệu "thông lượng" phải phù hợp nhất. Đó là số chu kỳ mà một đơn vị thực thi thường sẽ bị ràng buộc khi thực hiện một lệnh đã cho.
Vậy điều gì sẽ xảy ra nếu sự dịch chuyển KHÔNG nhanh hơn phép nhân? Như tôi đã nói ở trên, các kiến trúc hiện đại có thể khá phức tạp và những thứ như dự đoán nhánh, bộ đệm, đường ống và các đơn vị thực thi song song có thể khiến bạn khó dự đoán hiệu suất tương đối của hai đoạn mã tương đương logic. Tôi thực sự muốn nhấn mạnh điều này, bởi vì đây là nơi tôi không hài lòng với hầu hết các câu trả lời cho những câu hỏi như thế này và với trại người hoàn toàn nói rằng điều đó đơn giản là không đúng (nữa) rằng việc dịch chuyển nhanh hơn nhân.
Không, theo như tôi biết, chúng tôi đã không phát minh ra một loại nước sốt kỹ thuật bí mật nào đó vào những năm 1970 hoặc bất cứ khi nào đột nhiên hủy bỏ sự khác biệt về chi phí của một đơn vị nhân và một chút dịch chuyển. Một phép nhân tổng quát, về mặt cổng logic và chắc chắn về mặt hoạt động logic, vẫn phức tạp hơn so với sự thay đổi với bộ chuyển động nòng súng trong nhiều tình huống, trên nhiều kiến trúc. Làm thế nào điều này chuyển thành thời gian chạy tổng thể trên máy tính để bàn có thể hơi mờ. Tôi không biết chắc chắn chúng được triển khai như thế nào trong các bộ xử lý cụ thể, nhưng đây là lời giải thích về phép nhân: Phép nhân số nguyên có thực sự giống với tốc độ như trên CPU hiện đại không
Trong khi đây là một lời giải thích của một Shifter thùng . Các tài liệu tôi đã tham chiếu trong đoạn trước đưa ra một cái nhìn khác về chi phí hoạt động tương đối, theo ủy quyền của hướng dẫn CPU. Các kỹ sư tại Intel dường như thường xuyên nhận được câu hỏi tương tự: diễn đàn trong khu vực dành cho nhà phát triển intel cho phép nhân số nguyên và bổ sung trong bộ xử lý bộ đôi lõi 2
Vâng, trong hầu hết các kịch bản thực tế và gần như chắc chắn trong JavaScript, cố gắng khai thác tính tương đương này để thực hiện có lẽ là một công việc vô ích. Tuy nhiên, ngay cả khi chúng tôi buộc phải sử dụng các hướng dẫn nhân và sau đó không thấy sự khác biệt về thời gian chạy, điều đó nhiều hơn do bản chất của số liệu chi phí chúng tôi đã sử dụng, chính xác, và không phải vì không có chênh lệch chi phí. Thời gian chạy đầu cuối là một số liệu và nếu đó là chỉ số duy nhất chúng tôi quan tâm, tất cả đều ổn. Nhưng điều đó không có nghĩa là tất cả sự khác biệt về chi phí giữa nhân và dịch chuyển đã đơn giản biến mất. Và tôi nghĩ rằng chắc chắn không phải là một ý tưởng tốt để truyền đạt ý tưởng đó cho người hỏi, bằng ngụ ý hay nói cách khác, người rõ ràng chỉ mới bắt đầu có ý tưởng về các yếu tố liên quan đến thời gian và chi phí của mã hiện đại. Kỹ thuật luôn luôn là về sự đánh đổi. Điều tra và giải thích về những gì các bộ xử lý hiện đại đã đánh đổi để thể hiện thời gian thực hiện mà chúng ta khi người dùng cuối cùng nhìn thấy có thể mang lại một câu trả lời khác biệt hơn. Và tôi nghĩ rằng một câu trả lời khác biệt hơn là "điều này không còn đúng nữa" được bảo đảm nếu chúng ta muốn thấy ít kỹ sư kiểm tra mã tối ưu hóa vi mô dễ đọc hơn, bởi vì nó hiểu một cách tổng quát hơn về bản chất của "tối ưu hóa" như vậy phát hiện ra những hiện thân đa dạng, đa dạng của nó hơn là chỉ đề cập đến một số trường hợp cụ thể là lỗi thời.