Cái nào nhanh hơn: x << 1 hoặc x << 10?


83

Tôi không muốn tối ưu hóa bất cứ điều gì, tôi thề, tôi chỉ muốn hỏi câu hỏi này vì tò mò. Tôi biết rằng trên hầu hết các phần cứng có một lệnh lắp ráp chút ca (ví dụ shl, shr), mà là một lệnh duy nhất. Nhưng liệu nó có quan trọng không (khôn ngoan nano giây, hoặc khôn ngoan CPU) bao nhiêu bit bạn dịch chuyển. Nói cách khác, cách nào sau đây nhanh hơn trên bất kỳ CPU nào?

x << 1;

x << 10;

Và xin đừng ghét tôi vì câu hỏi này. :)


17
Omg, tôi nhìn lướt qua mã và suy nghĩ đầu tiên của tôi là "toán tử in dòng". Tôi cần nghỉ ngơi chút.
Kos,

4
Tôi nghĩ rằng tôi nghe thấy ai đó nói "tối ưu hóa quá sớm" lờ mờ trong tâm trí của họ, hoặc có thể chỉ là trí tưởng tượng của tôi.
tia

5
@tia ông nói rằng ông sẽ không tối ưu hóa bất cứ điều gì :)

1
@Grigory vâng và đó là lý do tại sao chúng tôi không thấy ai ở đây bỏ qua câu hỏi với cụm từ đó. : D
tia

1
Như một ghi chú bên lề: Gần đây tôi đã nhận ra rằng việc chuyển sang trái và chuyển sang phải không nhất thiết phải tiêu tốn cùng một thời gian cpu. Trong trường hợp của tôi, việc chuyển sang phải chậm hơn nhiều. Đầu tiên tôi đã rất ngạc nhiên nhưng tôi nghĩ câu trả lời là chuyển phương tiện trái logic và chuyển đúng có thể có nghĩa là số học: stackoverflow.com/questions/141525/...
Christian Ammer

Câu trả lời:


84

Có thể phụ thuộc vào CPU.

Tuy nhiên, tất cả các CPU hiện đại (x86, ARM) đều sử dụng "bộ dịch chuyển thùng" - một mô-đun phần cứng được thiết kế đặc biệt để thực hiện các ca thay đổi tùy ý trong thời gian không đổi.

Vì vậy, điểm mấu chốt là ... không. Không khác nhau.


21
Tuyệt vời, bây giờ tôi có một hình ảnh nói với CPU của tôi để làm một cuộn thùng mắc kẹt trong đầu của tôi ...
Ignacio Vazquez-Abrams

11
Errr - RẤT NHIỀU phụ thuộc vào bộ xử lý. Trên một số bộ xử lý, đây là thời gian không đổi. Đối với những người khác, nó có thể là một chu kỳ mỗi ca (tôi đã từng sử dụng một ca thay đổi khoảng 60.000 vị trí như một cách s / w đo tốc độ đồng hồ của bộ xử lý). Và trên các bộ xử lý khác, có thể chỉ có các hướng dẫn cho các dịch chuyển bit đơn trong trường hợp dịch chuyển nhiều bit được ủy quyền cho một quy trình thư viện nằm trong một vòng lặp đi lặp lại.
quick_now

4
@quickly_now: Đó chắc chắn là một cách đo tốc độ đồng hồ không tốt. Không có bộ xử lý nào đủ ngu ngốc để thực sự làm được 60.000 ca; đơn giản là sẽ được chuyển đổi thành 60000 mod register_size. Ví dụ, một bộ xử lý 32 bit sẽ chỉ sử dụng 5 bit quan trọng nhất của số lần dịch chuyển.
casablanca

4
Máy truyền tin inmos có một toán tử shift lấy số lượng dịch chuyển là một toán hạng 32 bit. Bạn có thể làm 4 tỷ ca nếu muốn, mỗi ca 1 đồng hồ. "Không có bộ xử lý nào là đủ ngu ngốc". Xin lôi sai. Điều này đã làm. Tuy nhiên, bạn cần mã hóa phần đó trong trình hợp dịch. Các trình biên dịch đã thực hiện một sửa đổi / tối ưu hóa hợp lý (chỉ đặt kết quả thành 0, không làm gì cả).
quick_now,

5
Đáng buồn thay, Pentium 4 đã mất bộ chuyển số thùng, điều này góp phần vào tốc độ hướng dẫn trên mỗi đồng hồ nói chung của nó. Tôi cho rằng kiến ​​trúc Core Blah đã lấy lại được nó.
Russell Borogove

64

Một số bộ xử lý nhúng chỉ có hướng dẫn "shift-by-one". Trên các bộ xử lý như vậy, trình biên dịch sẽ thay đổi x << 3thành ((x << 1) << 1) << 1.

Tôi nghĩ Motorola MC68HCxx là một trong những dòng máy phổ biến hơn với hạn chế này. May mắn thay, những kiến ​​trúc như vậy hiện nay khá hiếm, hầu hết hiện nay bao gồm một bộ chuyển đổi thùng với kích thước thay đổi.

Intel 8051, có nhiều dẫn xuất hiện đại, cũng không thể dịch chuyển một số bit tùy ý.


12
Vẫn thường gặp trên vi điều khiển nhúng.
Ben Jackson

4
Những gì bạn có nghĩa là dưới "hiếm"? Theo thống kê, số lượng vi điều khiển 8-bit được bán ra nhiều hơn tất cả các loại MPU khác.
Vovanium

Bộ vi điều khiển 8-bit không được sử dụng nhiều cho sự phát triển mới, khi bạn có thể nhận được 16-bit với cùng mức giá cho mỗi đơn vị (ví dụ như MSP430 từ TI) với nhiều ROM chương trình hơn, RAM hoạt động nhiều hơn và nhiều khả năng hơn. Và thậm chí một số vi điều khiển 8-bit có bộ chuyển đổi thùng.
Ben Voigt

1
Kích thước từ của một bộ vi điều khiển không liên quan gì đến việc nó có bộ dịch chuyển thùng hay không, dòng MC68HCxx mà tôi đã đề cập cũng có các bộ xử lý 16 bit, tất cả chúng đều chỉ dịch chuyển một vị trí bit cùng một lúc.
Ben Voigt

Thực tế là hầu hết các MCU 8-bit không có bộ chuyển đổi thùng, mặc dù bạn nói đúng rằng có những cái cho bạn thích thì điều đó không đúng, và có những bộ chuyển đổi thùng 8-bit không có. Bitness được coi là một giá trị gần đúng đáng tin cậy cho các máy có bộ chuyển số thùng [ra]. Ngoài ra, thực tế là lõi CPU cho MCU thường không đặt ra sự lựa chọn cho mô hình, nhưng ngoại vi trên chip thì có. Và 8-bit thường được chọn cho các ngoại vi phong phú hơn với cùng một mức giá.
Vovanium

29

Có rất nhiều trường hợp về điều này.

  1. Nhiều MPU tốc độ cao có bộ chuyển số thùng, mạch điện tử giống bộ ghép kênh thực hiện bất kỳ sự thay đổi nào trong thời gian không đổi.

  2. Nếu MPU chỉ có 1 bit shift x << 10thường sẽ chậm hơn, vì nó chủ yếu được thực hiện bằng 10 ca hoặc sao chép byte với 2 ca.

  3. Nhưng có một trường hợp phổ biến được biết đến x << 10thậm chí còn nhanh hơn x << 1. Nếu x là 16 bit, chỉ 6 bit thấp hơn của nó được quan tâm (tất cả các bit khác sẽ được chuyển ra ngoài), do đó MPU chỉ cần tải byte thấp hơn, do đó chỉ thực hiện một chu kỳ truy cập duy nhất vào bộ nhớ 8 bit, trong khi x << 10cần hai chu kỳ truy cập. Nếu chu kỳ truy cập chậm hơn shift (và xóa byte thấp hơn),x << 10 sẽ nhanh hơn. Điều này có thể áp dụng cho các bộ vi điều khiển có ROM chương trình tích hợp nhanh trong khi truy cập RAM dữ liệu bên ngoài chậm.

  4. Ngoài trường hợp 3, trình biên dịch có thể quan tâm đến số lượng bit quan trọng x << 10và tối ưu hóa các hoạt động tiếp theo cho các bit có độ rộng thấp hơn, như thay thế phép nhân 16x16 bằng phép nhân 16x8 (vì byte thấp hơn luôn bằng 0).

Lưu ý, một số bộ vi điều khiển không có lệnh shift-left nào cả, chúng sử dụng add x,xthay thế.


Tôi không hiểu, tại sao x << 10 lại nhanh hơn thì x << 8 trong đó x << 8 trong đó bạn cần thực hiện tải từ byte thấp hơn từ 16 bit, và không tải và hai ca thay đổi. tôi không hiểu.
không có

3
@none: Tôi không nói rằng x << 10 nhanh hơn x << 8.
Vovanium

9

Trên ARM, điều này có thể được thực hiện như một tác dụng phụ của một lệnh khác. Vì vậy, có khả năng, không có độ trễ nào cho cả hai.


1
Các lệnh có thực thi cùng một số chu kỳ không? Trên một vài kiến ​​trúc, cùng một lệnh sẽ chuyển thành một vài mã op khác nhau dựa trên các toán hạng và mất từ ​​1 đến 5 chu kỳ.
Nick T

@Nick Một lệnh ARM thường mất từ ​​1 đến 2 chu kỳ. Không chắc với các kiến ​​trúc mới hơn.
onemasse

2
@Nick T: Anh ấy nói về ARM, thich thay đổi không phải là hướng dẫn tận tình, mà là 'tính năng' của nhiều hướng dẫn xử lý dữ liệu. Tức là ADD R0, R1, R2 ASL #3thêm R1 và R2 dịch sang trái 3 bit.
Vovanium


7

Điều đó phụ thuộc vào cả CPU và trình biên dịch. Ngay cả khi CPU bên dưới có sự dịch chuyển bit tùy ý với bộ dịch chuyển thùng, điều này sẽ chỉ xảy ra nếu trình biên dịch tận dụng tài nguyên đó.

Hãy nhớ rằng việc chuyển bất cứ thứ gì ra ngoài chiều rộng theo bit của dữ liệu là "hành vi không xác định" trong C và C ++. Dịch chuyển bên phải của dữ liệu đã ký cũng được "xác định thực hiện". Thay vì quá lo lắng về tốc độ, hãy lo lắng rằng bạn đang nhận được câu trả lời giống nhau trên các cách triển khai khác nhau.

Trích dẫn từ ANSI C phần 3.3.7:

3.3.7 Các toán tử dịch chuyển theo chiều bit

Cú pháp

      shift-expression:
              additive-expression
              shift-expression <<  additive-expression
              shift-expression >>  additive-expression

Ràng buộc

Mỗi toán hạng phải có kiểu tích phân.

Ngữ nghĩa

Các thăng hạng tích hợp được thực hiện trên mỗi toán hạng. Loại kết quả là của toán hạng bên trái được thăng hạng. Nếu giá trị của toán hạng bên phải là số âm hoặc lớn hơn hoặc bằng độ rộng tính bằng bit của toán hạng bên trái được thăng cấp, thì hành vi là không xác định.

Kết quả của E1 << E2 là vị trí bit E2 dịch trái E1; các bit trống được điền bằng các số không. Nếu E1 có kiểu không dấu, giá trị của kết quả là E1 nhân với số lượng, 2 được nâng lên thành công suất E2, giảm modulo ULONG_MAX + 1 nếu E1 có kiểu không dấu dài, ngược lại UINT_MAX + 1. (Các hằng số ULONG_MAX và UINT_MAX được xác định trong tiêu đề.)

Kết quả của E1 >> E2 là các vị trí bit E2 dịch sang phải E1. Nếu E1 có kiểu không dấu hoặc nếu E1 có kiểu có dấu và giá trị không âm, giá trị của kết quả là phần tích phân của thương của E1 chia cho đại lượng, 2 được nâng lên lũy thừa E2. Nếu E1 có kiểu có dấu và giá trị âm, giá trị kết quả được xác định bởi việc triển khai.

Vì thế:

x = y << z;

"<<": y × 2 z ( không xác định nếu xảy ra tràn);

x = y >> z;

">>": triển khai được xác định cho có dấu (thường là kết quả của phép chuyển số học: y / 2 z ).


Tôi không nghĩ 1u << 100là UB. Nó chỉ là 0.
Armen Tsirunyan

@Armen Tsirunyan: Một bit shift 1u << 100như một bit shift có thể là tràn; 1u << 100như dịch chuyển số học là 0. Theo ANSI C, <<là một sự dịch chuyển bit. vi.wikipedia.org/wiki/Arithmetic_shift
con sói,

2
@Armen Tsirunyan: Xem ANSI phần 3.3.7 - Nếu giá trị của toán hạng bên phải là âm hoặc lớn hơn hoặc bằng độ rộng tính bằng bit của toán hạng bên trái được thăng cấp, hành vi là không xác định. Vì vậy, ví dụ của bạn là UB trên bất kỳ hệ thống ANSI C nào trừ khi có loại 101+ bit.
the wolf

@ cà rốt-pot: OK, bạn đã thuyết phục tôi :)
Armen Tsirunyan

Liên quan: x << (y & 31)vẫn có thể biên dịch thành một lệnh shift đơn lẻ mà không có lệnh AND, nếu trình biên dịch biết lệnh shift của kiến ​​trúc đích che dấu số (như x86 thì có). (Tốt hơn là không viết mã mặt nạ; lấy nó từ CHAR_BIT * sizeof(x) - 1hoặc thứ gì đó.) Điều này hữu ích để viết thành ngữ xoay biên dịch thành một lệnh duy nhất mà không cần bất kỳ C UB nào bất kể đầu vào. ( stackoverflow.com/questions/776508/… ).
Peter Cordes

7

Có thể hình dung rằng, trên bộ xử lý 8-bit, x<<1thực sự có thể chậm hơn nhiều so x<<10với giá trị 16-bit.

Ví dụ, một bản dịch hợp lý x<<1có thể là:

byte1 = (byte1 << 1) | (byte2 >> 7)
byte2 = (byte2 << 1)

trong khi x<<10sẽ đơn giản hơn:

byte1 = (byte2 << 2)
byte2 = 0

Lưu ý cách x<<1thay đổi thường xuyên hơn và thậm chí xa hơn x<<10. Hơn nữa, kết quả của x<<10không phụ thuộc vào nội dung của byte1. Điều này cũng có thể tăng tốc hoạt động.


5

Trên một số thế hệ CPU Intel (P2 hoặc P3? Tuy nhiên, không phải AMD, nếu tôi nhớ đúng), các hoạt động bithift chậm đến mức đáng kinh ngạc. Bitshift 1 bit luôn phải nhanh chóng vì nó chỉ có thể sử dụng phép cộng. Một câu hỏi khác cần xem xét là liệu sự dịch chuyển bit theo một số lượng bit không đổi có nhanh hơn sự thay đổi độ dài thay đổi hay không. Ngay cả khi các mã opcodes có cùng tốc độ, trên x86, toán hạng bên phải không thích hợp của một bithift phải chiếm thanh ghi CL, điều này đặt ra các ràng buộc bổ sung đối với việc phân bổ thanh ghi và có thể làm chậm chương trình theo cách đó.


1
Đó là Pentium 4. Các CPU có nguồn gốc từ PPro (như P2 và P3) có sự thay đổi nhanh chóng. Và vâng, sự thay đổi về số lượng biến trên x86 chậm hơn so với mức có thể, trừ khi bạn có thể sử dụng BMI2 shlx/ shrx/ sarx(Haswell trở lên và Ryzen). Ngữ nghĩa CISC (cờ không được sửa đổi nếu đếm = 0) làm tổn thương x86 ở đây. shl r32, cllà 3 uops trên Sandybridge-family (mặc dù Intel tuyên bố nó có thể hủy bỏ một trong các uops nếu kết quả cờ không được sử dụng). AMD có đơn UOP shl r32, cl(nhưng chậm đúp ca cho mở rộng chính xác, shld r32, r32, cl)
Peter Cordes

1
Sự thay đổi (số lượng thậm chí có thể thay đổi) chỉ là một lần lặp lại duy nhất trên P6-family, nhưng việc đọc kết quả cờ của shl r32, clhoặc với một kết quả ngay lập tức khác với 1 sẽ ngăn chặn giao diện người dùng cho đến khi ca kết thúc ! ( stackoverflow.com/questions/36510095/… ). Các trình biên dịch biết điều này và sử dụng một testlệnh riêng biệt thay vì sử dụng kết quả cờ của một sự thay đổi. (Nhưng đây chất thải hướng dẫn trên CPU mà nó không phải là một vấn đề, xem stackoverflow.com/questions/40354978/... )
Peter Cordes

3

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<<1là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 << 1hoà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 , addcó 4 thông lượng trên mỗi đồng hồ, nhưng shlvớ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 tronggắ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ề shlnơ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 clthanh 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,1hoặc add eax,eaxngắ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 xvẫ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ư addhoặc shl.) Quy ước gọi Hệ thống V x86-64 chuyển args vào các thanh ghi, với edigiá trị đối số đầu tiên và trả về ở trong eax, do đó, một hàm trả về x<<10cũ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 LEAcho 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 movhướ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 testhướng dẫn, như test eax, 0x7fffffff/ jz .falsethay 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ả.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.