Cách tốt nhất để đặt một thanh ghi thành 0 trong assembly x86: xor, mov hoặc và là gì?


119

Tất cả các hướng dẫn sau đều thực hiện tương tự: đặt %eaxthành không. Cách nào là tối ưu (yêu cầu ít chu kỳ máy nhất)?

xorl   %eax, %eax
mov    $0, %eax
andl   $0, %eax

6
Bạn có thể muốn đọc bài viết
Michael Petch

Câu trả lời:


222

Tóm tắt TL; DR : xor same, samelà sự lựa chọn tốt nhất cho tất cả các CPU . Không có phương pháp nào khác có bất kỳ lợi thế nào hơn nó, và nó có ít nhất một số lợi thế hơn bất kỳ phương pháp nào khác. Nó chính thức được đề xuất bởi Intel và AMD, và những gì các trình biên dịch làm. Ở chế độ 64 bit, vẫn sử dụng xor r32, r32, vì viết số 0 reg 32 ở trên 32 . xor r64, r64lãng phí một byte, vì nó cần tiền tố REX.

Thậm chí tệ hơn thế, Silvermont chỉ nhận dạng xor r32,r32là dep-break, không phải kích thước toán hạng 64-bit. Vì vậy, ngay cả khi tiền tố REX vẫn được yêu cầu bởi vì bạn đang sử dụng r8..r15, hãy sử dụng xor r10d,r10d, không phảixor r10,r10 .

Ví dụ về số nguyên GP:

xor   eax, eax       ; RAX = 0.  Including AL=0 etc.
xor   r10d, r10d     ; R10 = 0
xor   edx, edx       ; RDX = 0

; small code-size alternative:    cdq    ; zero RDX if EAX is already zero

; SUB-OPTIMAL
xor   rax,rax       ; waste of a REX prefix, and extra slow on Silvermont
xor   r10,r10       ; bad on Silvermont (not dep breaking), same as r10d everywhere else because a REX prefix is still needed for r10d or r10.
mov   eax, 0        ; doesn't touch FLAGS, but not faster and takes more bytes
 and   eax, 0        ; false dependency.  (Microbenchmark experiments might want this)
 sub   eax, eax      ; same as xor on most but not all CPUs; bad on Silvermont for example.

xor   al, al        ; false dep on some CPUs, not a zeroing idiom.  Use xor eax,eax
mov   al, 0         ; only 2 bytes, and probably better than xor al,al *if* you need to leave the rest of EAX/RAX unmodified

Việc làm sạch một thanh ghi vectơ thường được thực hiện tốt nhất pxor xmm, xmm. Đó thường là những gì gcc làm (ngay cả trước khi sử dụng với hướng dẫn FP).

xorps xmm, xmmcó thể có ý nghĩa. Nó ngắn hơn một byte pxor, nhưng xorpscần cổng thực thi 5 trên Intel Nehalem, trong khi pxorcó thể chạy trên bất kỳ cổng nào (0/1/5). (Độ trễ bỏ qua 2c của Nehalem giữa số nguyên và FP thường không liên quan, vì việc thực thi không theo thứ tự thường có thể ẩn nó khi bắt đầu một chuỗi phụ thuộc mới).

Trên các vi kiến ​​trúc SnB-family, cả hương vị của xor-zeroing đều không cần cổng thực thi. Trên AMD và Intel trước Nehalem P6 / Core2, xorpspxorđược xử lý theo cùng một cách (như lệnh vectơ-số nguyên).

Sử dụng phiên bản AVX của lệnh vectơ 128b cũng cho số không ở phần trên của reg, do đó, vpxor xmm, xmm, xmmlà một lựa chọn tốt cho việc làm 0 YMM (AVX1 / AVX2) hoặc ZMM (AVX512) hoặc bất kỳ phần mở rộng vectơ nào trong tương lai. vpxor ymm, ymm, ymmTuy nhiên, không mất thêm byte để mã hóa và chạy tương tự trên Intel, nhưng chậm hơn trên AMD trước Zen2 (2 uops). Việc làm 0 ZMM của AVX512 sẽ yêu cầu thêm byte (đối với tiền tố EVEX), do đó, việc làm 0 XMM hoặc YMM nên được ưu tiên.

Ví dụ về XMM / YMM / ZMM

    # Good:
 xorps   xmm0, xmm0         ; smallest code size (for non-AVX)
 pxor    xmm0, xmm0         ; costs an extra byte, runs on any port on Nehalem.
 xorps   xmm15, xmm15       ; Needs a REX prefix but that's unavoidable if you need to use high registers without AVX.  Code-size is the only penalty.

   # Good with AVX:
 vpxor xmm0, xmm0, xmm0    ; zeros X/Y/ZMM0
 vpxor xmm15, xmm0, xmm0   ; zeros X/Y/ZMM15, still only 2-byte VEX prefix

#sub-optimal AVX
 vpxor xmm15, xmm15, xmm15  ; 3-byte VEX prefix because of high source reg
 vpxor ymm0, ymm0, ymm0     ; decodes to 2 uops on AMD before Zen2


    # Good with AVX512
 vpxor  xmm15,  xmm0, xmm0     ; zero ZMM15 using an AVX1-encoded instruction (2-byte VEX prefix).
 vpxord xmm30, xmm30, xmm30    ; EVEX is unavoidable when zeroing zmm16..31, but still prefer XMM or YMM for fewer uops on probable future AMD.  May be worth using only high regs to avoid needing vzeroupper in short functions.
    # Good with AVX512 *without* AVX512VL (e.g. KNL / Xeon Phi)
 vpxord zmm30, zmm30, zmm30    ; Without AVX512VL you have to use a 512-bit instruction.

# sub-optimal with AVX512 (even without AVX512VL)
 vpxord  zmm0, zmm0, zmm0      ; EVEX prefix (4 bytes), and a 512-bit uop.  Use AVX1 vpxor xmm0, xmm0, xmm0 even on KNL to save code size.

Xem vxorps-zeroing trên AMD Jaguar / Bulldozer / Zen với thanh ghi xmm có nhanh hơn ymm không?
Cách hiệu quả nhất để xóa một hoặc một vài đăng ký ZMM trên Knights Landing là gì?

Bán liên quan: Cách nhanh nhất để đặt giá trị __m256 thành tất cả MỘT bit
Đặt tất cả các bit trong thanh ghi CPU thành 1 một cách hiệu quả cũng bao gồm các thanh k0..7ghi mặt nạ AVX512 . SSE / AVX vpcmpeqdbị phá vỡ trên nhiều (mặc dù vẫn cần một uop để viết các 1), nhưng AVX512 vpternlogdcho các đăng ký ZMM thậm chí không bị phá vỡ. Bên trong một vòng lặp, hãy cân nhắc sao chép từ một thanh ghi khác thay vì tạo lại các thanh ghi bằng ALU uop, đặc biệt là với AVX512.

Nhưng zeroing rẻ: xor-zeroing một reg xmm bên trong một vòng lặp thường tốt như sao chép, ngoại trừ trên một số CPU AMD (Bulldozer và Zen) có loại bỏ mov cho các vector regs nhưng vẫn cần một uop ALU để ghi các số không cho xor -chính xác.


Có gì đặc biệt về các thành ngữ zeroing như xor on other uarches

Một số CPU nhận dạng sub same,samenhư một thành ngữ zeroing xor, nhưng tất cả các CPU nhận ra bất kỳ thành ngữ zeroing nào đều nhận raxor . Chỉ cần sử dụng xorđể bạn không phải lo lắng về việc CPU nào nhận ra thành ngữ zeroing nào.

xor(không giống như một thành ngữ zeroing được công nhận mov reg, 0) có một số lợi thế rõ ràng và tinh tế (danh sách tóm tắt, sau đó tôi sẽ mở rộng về những điều đó):

  • kích thước mã nhỏ hơn mov reg,0. (Tất cả các CPU)
  • tránh các hình phạt đăng ký một phần cho mã sau này. (Intel P6-family và SnB-family).
  • không sử dụng đơn vị thực thi, tiết kiệm năng lượng và giải phóng tài nguyên thực thi. (Intel SnB-family)
  • uop nhỏ hơn (không có dữ liệu ngay lập tức) để lại chỗ trống trong dòng bộ nhớ cache uop để mượn các hướng dẫn lân cận nếu cần. (Intel SnB-họ).
  • không sử dụng hết các mục nhập trong tệp đăng ký vật lý . (Ít nhất là Intel SnB-family (và P4), có thể cả AMD vì họ sử dụng thiết kế PRF tương tự thay vì giữ trạng thái thanh ghi trong ROB như vi kiến ​​trúc Intel P6-family.)

Kích thước mã máy nhỏ hơn (2 byte thay vì 5) luôn là một lợi thế: Mật độ mã cao hơn dẫn đến việc bỏ lỡ bộ đệm ẩn lệnh ít hơn, tìm nạp lệnh tốt hơn và có khả năng giải mã băng thông.


Lợi ích của việc không sử dụng đơn vị thực thi cho xor trên vi kiến ​​trúc Intel SnB-family là nhỏ, nhưng tiết kiệm điện năng. Nó có nhiều khả năng là vấn đề trên SnB hoặc IvB, chỉ có 3 cổng thực thi ALU. Haswell trở lên có 4 cổng thực thi có thể xử lý các lệnh ALU số nguyên, bao gồm mov r32, imm32, vì vậy với việc đưa ra quyết định hoàn hảo bởi bộ lập lịch (điều này không phải lúc nào cũng xảy ra trong thực tế), HSW vẫn có thể duy trì 4 uops mỗi đồng hồ ngay cả khi tất cả chúng đều cần ALU các cổng thực thi.

Xem câu trả lời của tôi về một câu hỏi khác về thanh ghi zeroing để biết thêm chi tiết.

Bài đăng trên blog của Bruce Dawson mà Michael Petch đã liên kết (trong một bình luận về câu hỏi) chỉ ra rằng nó xorđược xử lý ở giai đoạn đăng ký đổi tên mà không cần đơn vị thực thi (không có lỗi trong miền không sử dụng), nhưng bỏ lỡ thực tế là nó vẫn còn một điểm trong miền hợp nhất. Các CPU Intel hiện đại có thể phát hành và gỡ bỏ 4 uops miền hợp nhất trên mỗi xung nhịp. Đó là nơi bắt nguồn của giới hạn 4 số không trên mỗi đồng hồ. Độ phức tạp tăng lên của phần cứng đổi tên thanh ghi chỉ là một trong những lý do giới hạn độ rộng của thiết kế xuống còn 4. (Bruce đã viết một số bài đăng trên blog rất xuất sắc, như loạt bài của anh ấy về toán FP và các vấn đề x87 / SSE / làm tròn , mà tôi làm rất khuyến khích).


Trên các CPU dòng AMD Bulldozer , mov immediatechạy trên các cổng thực thi số nguyên EX0 / EX1 giống như xor. mov reg,regcũng có thể chạy trên AGU0 / 1, nhưng đó chỉ để sao chép thanh ghi, không phải để thiết lập ngay lập tức. Vì vậy, AFAIK, trên AMD lợi thế duy nhất để xorqua movlà mã hóa ngắn hơn. Nó cũng có thể tiết kiệm tài nguyên đăng ký vật lý, nhưng tôi chưa thấy bất kỳ thử nghiệm nào.


Các thành ngữ zeroing được công nhận tránh các hình phạt đăng ký một phần trên CPU Intel. Các thành ngữ này đổi tên các thanh ghi từng phần riêng biệt với các thanh ghi đầy đủ (họ P6 & SnB).

xorsẽ gắn thẻ thanh ghi là có các phần trên bằng không , do đó xor eax, eax/ inc al/ inc eaxtránh hình phạt thanh ghi một phần thông thường mà các CPU trước IvB có. Ngay cả khi không có xor, IvB chỉ cần một uop hợp nhất khi 8bits ( AH) cao được sửa đổi và sau đó toàn bộ thanh ghi được đọc, và Haswell thậm chí còn loại bỏ điều đó.

Từ hướng dẫn về microarch của Agner Fog, trang 98 (phần Pentium M, được tham chiếu bởi các phần sau bao gồm cả SnB):

Bộ xử lý nhận ra XOR của một thanh ghi với chính nó khi đặt nó thành 0. Một thẻ đặc biệt trong thanh ghi nhớ rằng phần cao của thanh ghi bằng 0 để EAX = AL. Thẻ này được ghi nhớ ngay cả trong một vòng lặp:

    ; Example    7.9. Partial register problem avoided in loop
    xor    eax, eax
    mov    ecx, 100
LL:
    mov    al, [esi]
    mov    [edi], eax    ; No extra uop
    inc    esi
    add    edi, 4
    dec    ecx
    jnz    LL

(từ pg82): Bộ xử lý ghi nhớ rằng 24 bit trên của EAX bằng 0 miễn là bạn không gặp phải sự cố gián đoạn, báo cáo sai hoặc sự kiện tuần tự hóa khác.

pg82 của hướng dẫn đó cũng xác nhận rằng mov reg, 0không được công nhận là một thành ngữ zeroing, ít nhất là trên P6 đầu thiết kế như PIII hoặc PM. Tôi sẽ rất ngạc nhiên nếu họ sử dụng các bóng bán dẫn để phát hiện nó trên các CPU sau này.


xorđặt cờ , có nghĩa là bạn phải cẩn thận khi kiểm tra các điều kiện. Vì setcctiếc là chỉ khả dụng với đích 8bit , bạn thường cần phải cẩn thận để tránh bị phạt đăng ký một phần.

Sẽ thật tuyệt nếu x86-64 đặt lại một trong những mã opc đã loại bỏ (như AAM) cho một bit 16/32/64 setcc r/m, với vị từ được mã hóa trong trường 3 bit của thanh ghi nguồn của trường r / m (cách một số lệnh toán hạng đơn khác sử dụng chúng như các bit opcode). Nhưng họ đã không làm điều đó và điều đó sẽ không giúp ích gì cho x86-32.

Tốt nhất, bạn nên sử dụng xor/ đặt cờ / setcc/ đọc thanh ghi đầy đủ:

...
call  some_func
xor     ecx,ecx    ; zero *before* the test
test    eax,eax
setnz   cl         ; cl = (some_func() != 0)
add     ebx, ecx   ; no partial-register penalty here

Điều này có hiệu suất tối ưu trên tất cả các CPU (không có lỗi, hợp nhất uops hoặc phụ thuộc sai).

Mọi thứ phức tạp hơn khi bạn không muốn thử trước một lệnh thiết lập cờ . Ví dụ: bạn muốn phân nhánh theo một điều kiện và sau đó setcc theo điều kiện khác từ các cờ giống nhau. ví dụ cmp/jle, setevà bạn không có một thanh ghi dự phòng, hoặc bạn muốn giữ nguyên xorđường dẫn mã không được sử dụng.

Không có thành ngữ zeroing nào được công nhận mà không ảnh hưởng đến cờ, vì vậy lựa chọn tốt nhất phụ thuộc vào vi kiến ​​trúc đích. Trên Core2, việc chèn một uop hợp nhất có thể gây ra hiện tượng đình trệ 2 hoặc 3 chu kỳ. Nó có vẻ rẻ hơn trên SnB, nhưng tôi đã không mất nhiều thời gian để đo lường. Việc sử dụng mov reg, 0/ setccsẽ có một hình phạt đáng kể đối với các CPU Intel cũ hơn và vẫn còn tệ hơn trên Intel mới hơn.

Sử dụng setcc/ movzx r32, r8có lẽ là giải pháp thay thế tốt nhất cho dòng Intel P6 & SnB, nếu bạn không thể xor-0 trước hướng dẫn cài đặt cờ. Điều đó sẽ tốt hơn là lặp lại thử nghiệm sau một xor-zeroing. (Thậm chí không xem xét sahf/ lahfhoặc pushf/ popf). IvB có thể loại bỏ movzx r32, r8(tức là xử lý nó bằng cách đổi tên thanh ghi mà không có đơn vị thực thi hoặc độ trễ, như xor-zeroing). Haswell trở về sau chỉ loại bỏ các movlệnh thông thường , do đó movzxcần một đơn vị thực thi và có độ trễ khác 0, làm cho kiểm tra / setcc/ movzxtệ hơn xor/ kiểm tra / setcc, nhưng ít nhất vẫn tốt như kiểm tra / mov r,0/ setcc(và tốt hơn nhiều trên các CPU cũ hơn).

Việc sử dụng setcc/ movzxkhông có zeroing trước là không tốt trên AMD / P4 / Silvermont, bởi vì chúng không theo dõi các deps riêng biệt cho các thanh ghi phụ. Sẽ có một giá trị sai trên giá trị cũ của sổ đăng ký. Sử dụng mov reg, 0/ setcccho zeroing / dependency-break có lẽ là lựa chọn thay thế tốt nhất khi xor/ test / setcckhông phải là một lựa chọn.

Tất nhiên, nếu bạn không cần setccđầu ra của rộng hơn 8 bit, bạn không cần phải làm gì cả. Tuy nhiên, hãy cẩn thận với các phụ thuộc sai trên các CPU không phải P6 / SnB nếu bạn chọn một thanh ghi gần đây là một phần của một chuỗi phụ thuộc dài. (Và hãy cẩn thận gây ra một phần đăng ký ngừng trệ hoặc tăng thêm nếu bạn gọi một hàm có thể lưu / khôi phục thanh ghi mà bạn đang sử dụng.)


andvới số 0 ngay lập tức không được đặt tên đặc biệt độc lập với giá trị cũ trên bất kỳ CPU nào mà tôi biết, vì vậy nó không phá vỡ các chuỗi phụ thuộc. Nó không có ưu điểm xorvà nhiều nhược điểm.

Nó chỉ hữu ích để viết microbenchmarks khi bạn muốn phụ thuộc như một phần của bài kiểm tra độ trễ, nhưng muốn tạo một giá trị đã biết bằng cách thêm 0 và thêm.


Xem http://agner.org/optimize/ để biết thông tin chi tiết về microarch , bao gồm cả những thành ngữ zeroing nào được coi là phá vỡ sự phụ thuộc (ví dụ: sub same,sametrên một số nhưng không phải tất cả CPU, trong khi xor same,sameđược công nhận trên tất cả.) Có movphá vỡ chuỗi phụ thuộc trên giá trị cũ của thanh ghi (bất kể giá trị nguồn, bằng không hay không, vì đó là cách movhoạt động). xorchỉ phá vỡ chuỗi phụ thuộc trong trường hợp đặc biệt mà src và dest là cùng một thanh ghi, đó là lý do tại sao movbị loại khỏi danh sách các trình ngắt phụ thuộc được công nhận đặc biệt . (Ngoài ra, vì nó không được công nhận là một thành ngữ zeroing, với những lợi ích khác mang lại.)

Điều thú vị là thiết kế P6 lâu đời nhất (từ PPro đến Pentium III) không nhận ra xor-zeroing như một bộ ngắt phụ thuộc, chỉ là một thành ngữ zeroing nhằm mục đích tránh các gian hàng đăng ký một phần , vì vậy trong một số trường hợp, nó đáng sử dụng cả hai mov và sau đó xor-zeroing theo thứ tự đó để phá vỡ dep và sau đó bằng không một lần nữa + thiết lập bit thẻ bên trong mà các bit cao bằng 0 nên EAX = AX = AL.

Xem Ví dụ 6.17 của Agner Fog. trong pdf microarch của anh ấy. Anh ấy nói rằng điều này cũng áp dụng cho P2, P3 và thậm chí (sớm?) PM. Một nhận xét về bài đăng trên blog được liên kết nói rằng chỉ có PPro mới có sự giám sát này, nhưng tôi đã thử nghiệm trên Katmai PIII và @Fanael đã thử nghiệm trên Pentium M và cả hai chúng tôi đều nhận thấy rằng nó không phá vỡ sự phụ thuộc về độ trễ -liên kết imulchuỗi. Điều này xác nhận kết quả của Agner Fog, thật không may.


TL: DR:

Nếu nó thực sự làm cho mã của bạn đẹp hơn hoặc lưu các hướng dẫn, thì chắc chắn, bằng không movđể tránh chạm vào cờ, miễn là bạn không gây ra vấn đề hiệu suất ngoài kích thước mã. Tránh các cờ làm tắc nghẽn là lý do hợp lý duy nhất để không sử dụng xor, nhưng đôi khi bạn có thể xor-0 trước thứ đặt cờ nếu bạn có một thanh ghi dự phòng.

mov-zero trước setcccó độ trễ tốt hơn so với movzx reg32, reg8sau (ngoại trừ trên Intel khi bạn có thể chọn các thanh ghi khác nhau), nhưng kích thước mã kém hơn.


7
Hầu hết các lệnh số học OP R, S bị CPU không theo thứ tự buộc phải đợi nội dung của thanh ghi R được lấp đầy bởi các lệnh trước đó với thanh ghi R là đích; đây là một phụ thuộc dữ liệu. Điểm mấu chốt là chip Intel / AMD có phần cứng đặc biệt để phá vỡ sự phụ thuộc dữ liệu phải-chờ-đợi vào thanh ghi R khi gặp XOR R, R và không nhất thiết phải làm như vậy đối với các lệnh zeroing thanh ghi khác. Điều này có nghĩa là lệnh XOR có thể được lên lịch để thực thi ngay lập tức và đây là lý do tại sao Intel / AMD khuyên bạn nên sử dụng nó.
Ira Baxter vào

3
@IraBaxter: Đúng vậy, và chỉ để tránh bất kỳ sự nhầm lẫn nào (vì tôi đã thấy quan niệm sai lầm này trên SO), mov reg, srccũng phá vỡ chuỗi dep cho các CPU OO (bất kể src là imm32 [mem], hay một thanh ghi khác). Việc phá vỡ sự phụ thuộc này không được đề cập trong sổ tay hướng dẫn tối ưu hóa vì nó không phải là trường hợp đặc biệt chỉ xảy ra khi src và dest là cùng một thanh ghi. Nó luôn xảy ra cho các hướng dẫn không phụ thuộc vào đích của chúng. (trừ trường hợp thực hiện của Intel popcnt/lzcnt/tzcntcó một dep sai trên dest.)
Peter Cordes

2
@Zboson: "Độ trễ" của một lệnh không có phụ thuộc chỉ quan trọng nếu có bong bóng trong đường dẫn. Thật tuyệt vời khi loại bỏ mov, nhưng đối với các lệnh zeroing, lợi ích của độ trễ bằng 0 chỉ phát huy tác dụng sau khi một cái gì đó như dự đoán sai chi nhánh hoặc I $ miss, trong đó việc thực thi đang chờ các lệnh được giải mã, thay vì dữ liệu đã sẵn sàng. Nhưng có, loại bỏ mov không làm cho movmiễn phí, chỉ có độ trễ bằng không. Phần "không sử dụng cổng thực thi" thường không quan trọng. Thông lượng miền kết hợp có thể dễ dàng là điểm nghẽn, đặc biệt. với tải hoặc cửa hàng trong hỗn hợp.
Peter Cordes

2
Theo Agner, KNL không công nhận tính độc lập của thanh ghi 64-bit. Vì vậy, xor r64, r64không chỉ lãng phí một byte. Như bạn nói xor r32, r32là sự lựa chọn tốt nhất đặc biệt là với KNL. Xem phần 15.7 "Các trường hợp độc lập đặc biệt" trong hướng dẫn sử dụng micrarch này nếu bạn muốn đọc thêm.
Z boson

3
à, MIPS cũ tốt ở đâu , với "thanh ghi số không" khi bạn cần.
hayalci
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.