Như mọi khi, nó phụ thuộc vào ngữ cảnh mã xung quanh : ví dụ: bạn có đang sử dụng x<<1
làm chỉ mục mảng không? Hoặc thêm nó vào một cái gì đó khác? Trong cả hai trường hợp, số lượng dịch chuyển nhỏ (1 hoặc 2) thường có thể tối ưu hóa nhiều hơn so với việc trình biên dịch cuối cùng chỉ phải dịch chuyển. Chưa kể đến sự cân bằng toàn bộ thông lượng so với độ trễ so với các nút thắt cổ chai phía trước. Hiệu suất của một mảnh nhỏ không phải là một chiều.
Hướng dẫn dịch chuyển phần cứng không phải là lựa chọn duy nhất của trình biên dịch để biên dịch x<<1
, nhưng các câu trả lời khác hầu hết đều giả định điều đó.
x << 1
hoàn toàn tương đương vớix+x
đối với số nguyên có dấu và phần bù của 2 số nguyên có dấu. Các trình biên dịch luôn biết họ đang nhắm mục tiêu vào phần cứng nào trong khi biên dịch, vì vậy họ có thể tận dụng các thủ thuật như thế này.
Trên Intel Haswell , add
có 4 thông lượng trên mỗi đồng hồ, nhưng shl
với số đếm tức thời chỉ có 2 thông lượng trên mỗi đồng hồ. (Xem http://agner.org/optimize/ để biết các bảng hướng dẫn và các liên kết khác trongx86gắn thẻ wiki). Dịch chuyển vectơ SIMD là 1 trên mỗi đồng hồ (2 trong Skylake), nhưng số nguyên vectơ SIMD thêm vào là 2 trên mỗi đồng hồ (3 trong Skylake). Tuy nhiên, độ trễ là như nhau: 1 chu kỳ.
Ngoài ra còn có một mã hóa thay đổi đặc biệt về shl
nơi ẩn số trong opcode. 8086 không có các ca đếm ngay lập tức, chỉ theo từng cái và từng cl
thanh ghi. Điều này chủ yếu phù hợp với dịch chuyển phải, vì bạn chỉ có thể thêm cho dịch chuyển trái trừ khi bạn đang thay đổi toán hạng bộ nhớ. Nhưng nếu giá trị cần thiết sau này, tốt hơn nên tải vào một thanh ghi trước. Nhưng dù sao, shl eax,1
hoặc add eax,eax
ngắn hơn một byte shl eax,10
, và kích thước mã có thể trực tiếp (giải mã / tắc nghẽn giao diện người dùng) hoặc gián tiếp (bộ nhớ cache mã L1I bỏ lỡ) ảnh hưởng đến hiệu suất.
Nói chung hơn, số lượng dịch chuyển nhỏ đôi khi có thể được tối ưu hóa thành một chỉ mục được chia tỷ lệ trong chế độ định địa chỉ trên x86. Hầu hết các kiến trúc khác đang được sử dụng phổ biến ngày nay là RISC và không có các chế độ đánh chỉ mục theo tỷ lệ, nhưng x86 là một kiến trúc đủ phổ biến để điều này đáng nói. (trứng nếu bạn đang lập chỉ mục một mảng gồm các phần tử 4 byte, thì có thể tăng hệ số tỷ lệ lên 1 cho int arr[]; arr[x<<1]
).
Nhu cầu sao chép + dịch chuyển là phổ biến trong các trường hợp x
vẫn cần giá trị gốc của . Nhưng hầu hết các lệnh số nguyên x86 hoạt động tại chỗ. (Đích đến là một trong những nguồn cho các lệnh như add
hoặc shl
.) Quy ước gọi Hệ thống V x86-64 chuyển args vào các thanh ghi, với edi
giá trị đối số đầu tiên và trả về ở trong eax
, do đó, một hàm trả về x<<10
cũng làm cho trình biên dịch phát ra bản sao + dịch chuyển mã.
Lệnh này LEA
cho phép bạn chuyển và thêm (với số lần dịch từ 0 đến 3, vì nó sử dụng mã hóa máy ở chế độ địa chỉ). Nó đưa kết quả vào một thanh ghi riêng biệt.
gcc và clang đều tối ưu hóa các chức năng này theo cùng một cách, như bạn có thể thấy trên trình khám phá trình biên dịch Godbolt :
int shl1(int x) { return x<<1; }
lea eax, [rdi+rdi] # 1 cycle latency, 1 uop
ret
int shl2(int x) { return x<<2; }
lea eax, [4*rdi] # longer encoding: needs a disp32 of 0 because there's no base register, only scaled-index.
ret
int times5(int x) { return x * 5; }
lea eax, [rdi + 4*rdi]
ret
int shl10(int x) { return x<<10; }
mov eax, edi # 1 uop, 0 or 1 cycle latency
shl eax, 10 # 1 uop, 1 cycle latency
ret
LEA với 2 thành phần có độ trễ 1 chu kỳ và thông lượng 2 mỗi xung nhịp trên các CPU Intel và AMD gần đây. (Gia đình Sandybridge và Xe ủi đất / Ryzen). Trên Intel, nó chỉ là 1 thông lượng trên mỗi đồng hồ với độ trễ 3c cho lea eax, [rdi + rsi + 123]
. (Liên quan: Tại sao mã C ++ này nhanh hơn so với lắp ráp viết tay của tôi để kiểm tra phỏng đoán Collatz? Hãy đi vào chi tiết điều này.)
Dù sao, sao chép + dịch chuyển bằng 10 cần một mov
hướng dẫn riêng . Nó có thể là không có độ trễ trên nhiều CPU gần đây, nhưng nó vẫn chiếm băng thông và kích thước mã front-end. ( MOV của x86 có thể thực sự "miễn phí" không? Tại sao tôi không thể tái tạo điều này? )
Cũng liên quan: Làm thế nào để nhân một thanh ghi với 37 chỉ bằng 2 lệnh leal liên tiếp trong x86? .
Trình biên dịch cũng có thể tự do chuyển đổi mã xung quanh để không có sự thay đổi thực tế hoặc nó được kết hợp với các hoạt động khác .
Ví dụ if(x<<1) { }
có thể sử dụng một and
để kiểm tra tất cả các bit ngoại trừ bit cao. Trên x86, bạn sẽ sử dụng một test
hướng dẫn, như test eax, 0x7fffffff
/ jz .false
thay vì shl eax,1 / jz
. Tính năng tối ưu hóa này hoạt động đối với bất kỳ số ca thay đổi nào và nó cũng hoạt động trên các máy có số ca dịch chuyển lớn chậm (như Pentium 4) hoặc không tồn tại (một số bộ điều khiển vi mô).
Nhiều ISA có hướng dẫn thao tác bit ngoài việc chỉ dịch chuyển. ví dụ PowerPC có rất nhiều lệnh trích xuất / chèn trường bit. Hoặc ARM có sự thay đổi của toán hạng nguồn như một phần của bất kỳ lệnh nào khác. (Vì vậy, lệnh shift / xoay chỉ là một dạng đặc biệt move
, sử dụng một nguồn đã thay đổi.)
Hãy nhớ rằng, C không phải là hợp ngữ . Luôn xem đầu ra của trình biên dịch được tối ưu hóa khi bạn đang điều chỉnh mã nguồn của mình để biên dịch hiệu quả.