Hãy xem xét hai chương trình C nhỏ làm thay đổi một chút và phân chia.
#include <stdlib.h>
int main(int argc, char* argv[]) {
int i = atoi(argv[0]);
int b = i << 2;
}
#include <stdlib.h>
int main(int argc, char* argv[]) {
int i = atoi(argv[0]);
int d = i / 4;
}
Sau đó, mỗi cái được biên dịch gcc -S
để xem lắp ráp thực tế sẽ là gì.
Với phiên bản dịch chuyển bit, từ lệnh gọi atoi
trở về:
callq _atoi
movl $0, %ecx
movl %eax, -20(%rbp)
movl -20(%rbp), %eax
shll $2, %eax
movl %eax, -24(%rbp)
movl %ecx, %eax
addq $32, %rsp
popq %rbp
ret
Trong khi phiên bản phân chia:
callq _atoi
movl $0, %ecx
movl $4, %edx
movl %eax, -20(%rbp)
movl -20(%rbp), %eax
movl %edx, -28(%rbp) ## 4-byte Spill
cltd
movl -28(%rbp), %r8d ## 4-byte Reload
idivl %r8d
movl %eax, -24(%rbp)
movl %ecx, %eax
addq $32, %rsp
popq %rbp
ret
Chỉ cần nhìn vào điều này, có một vài hướng dẫn trong phiên bản phân chia so với dịch chuyển bit.
Chìa khóa là họ làm gì?
Trong phiên bản dịch chuyển bit, lệnh chính là shll $2, %eax
sự dịch chuyển trái logic - có sự phân chia và mọi thứ khác chỉ là các giá trị di chuyển xung quanh.
Trong phiên bản phân chia, bạn có thể thấy idivl %r8d
- nhưng ngay phía trên đó là một cltd
(chuyển đổi dài thành gấp đôi) và một số logic bổ sung xung quanh sự cố tràn và tải lại. Công việc bổ sung này, biết rằng chúng ta đang xử lý một toán học chứ không phải bit thường là cần thiết để tránh các lỗi khác nhau có thể xảy ra bằng cách chỉ thực hiện toán học bit.
Hãy thực hiện một số phép nhân nhanh chóng:
#include <stdlib.h>
int main(int argc, char* argv[]) {
int i = atoi(argv[0]);
int b = i >> 2;
}
#include <stdlib.h>
int main(int argc, char* argv[]) {
int i = atoi(argv[0]);
int d = i * 4;
}
Thay vì đi qua tất cả những điều này, có một dòng khác nhau:
$ diff mult.s bit.s
24c24
> sẽ $ 2,% eax
---
<sarl $ 2,% eax
Ở đây trình biên dịch đã có thể xác định rằng toán học có thể được thực hiện với một ca, tuy nhiên thay vì một sự thay đổi logic, nó thực hiện một sự thay đổi số học. Sự khác biệt giữa những điều này sẽ là rõ ràng nếu chúng ta chạy chúng - sarl
giữ nguyên dấu hiệu. Vì vậy, -2 * 4 = -8
trong khi shll
không.
Hãy xem xét điều này trong một kịch bản perl nhanh chóng:
#!/usr/bin/perl
$foo = 4;
print $foo << 2, "\n";
print $foo * 4, "\n";
$foo = -4;
print $foo << 2, "\n";
print $foo * 4, "\n";
Đầu ra:
16
16
18446744073709551600
-16
Um ... -4 << 2
là 18446744073709551600
mà không phải là chính xác những gì bạn đang có khả năng mong đợi khi giao dịch với nhân và chia. Nó đúng, nhưng nó không nhân số nguyên.
Và do đó hãy cảnh giác với tối ưu hóa sớm. Hãy để trình biên dịch tối ưu hóa cho bạn - nó biết bạn thực sự đang cố gắng làm gì và có khả năng sẽ làm tốt hơn với nó, với ít lỗi hơn.