Là <nhanh hơn <=?


1574

if( a < 901 )nhanh hơn if( a <= 900 ).

Không chính xác như trong ví dụ đơn giản này, nhưng có những thay đổi hiệu suất nhỏ trên mã phức tạp vòng lặp. Tôi cho rằng điều này phải làm một cái gì đó với mã máy được tạo trong trường hợp nó thậm chí đúng.


153
Tôi thấy không có lý do tại sao câu hỏi này nên được đóng lại (và đặc biệt là không bị xóa, vì các phiếu hiện đang hiển thị) vì ý nghĩa lịch sử của nó, chất lượng của câu trả lời và thực tế là các câu hỏi hàng đầu khác về hiệu suất vẫn mở. Nhiều nhất nó nên được khóa. Ngoài ra, ngay cả khi bản thân câu hỏi bị hiểu sai / ngây thơ, thì thực tế nó xuất hiện trong một cuốn sách có nghĩa là thông tin sai lệch ban đầu tồn tại ở đó trong các nguồn "đáng tin cậy" ở đâu đó, và do đó câu hỏi này mang tính xây dựng trong đó giúp làm sáng tỏ điều đó.
Jason C

32
Bạn chưa bao giờ nói với chúng tôi cuốn sách nào bạn đang đề cập đến.
Jonathon Reinhart

160
<nhanh hơn hai lần so với gõ <=.
Đức Thanh

6
Đó là sự thật trên 8086.
Joshua

7
Số lượng upvote rõ ràng cho thấy rằng có hàng trăm người bị lạm dụng quá mức.
m93a

Câu trả lời:


1704

Không, nó sẽ không nhanh hơn trên hầu hết các kiến ​​trúc. Bạn đã không chỉ định, nhưng trên x86, tất cả các phép so sánh tích phân thường sẽ được thực hiện theo hai hướng dẫn máy:

  • Một testhoặc cmpchỉ dẫn, mà thiết lậpEFLAGS
  • Và một lệnh Jcc(nhảy) , tùy thuộc vào loại so sánh (và bố cục mã):
    • jne - Nhảy nếu không bằng -> ZF = 0
    • jz - Nhảy nếu không (bằng) -> ZF = 1
    • jg - Nhảy nếu lớn hơn -> ZF = 0 and SF = OF
    • (Vân vân...)

Ví dụ (Đã chỉnh sửa cho ngắn gọn) Được biên dịch với$ gcc -m32 -S -masm=intel test.c

    if (a < b) {
        // Do something 1
    }

Biên dịch thành:

    mov     eax, DWORD PTR [esp+24]      ; a
    cmp     eax, DWORD PTR [esp+28]      ; b
    jge     .L2                          ; jump if a is >= b
    ; Do something 1
.L2:

    if (a <= b) {
        // Do something 2
    }

Biên dịch thành:

    mov     eax, DWORD PTR [esp+24]      ; a
    cmp     eax, DWORD PTR [esp+28]      ; b
    jg      .L5                          ; jump if a is > b
    ; Do something 2
.L5:

Vì vậy, sự khác biệt duy nhất giữa hai là một jgso với một jgehướng dẫn. Hai người sẽ mất cùng một khoảng thời gian.


Tôi muốn giải quyết nhận xét rằng không có gì chỉ ra rằng các hướng dẫn nhảy khác nhau có cùng thời gian. Câu hỏi này hơi khó trả lời, nhưng đây là những gì tôi có thể đưa ra: Trong Tham chiếu tập lệnh của Intel , tất cả chúng được nhóm lại với nhau theo một hướng dẫn chung, Jcc(Nhảy nếu điều kiện được đáp ứng). Việc phân nhóm tương tự được thực hiện cùng nhau trong Hướng dẫn tham khảo tối ưu hóa , trong Phụ lục C. Độ trễ và thông lượng.

Độ trễ - Số chu kỳ đồng hồ được yêu cầu cho lõi thực thi để hoàn thành việc thực hiện tất cả các ops tạo thành một lệnh.

Thông lượng - Số chu kỳ đồng hồ cần thiết để chờ trước khi các cổng phát hành được tự do chấp nhận lại cùng một hướng dẫn. Đối với nhiều hướng dẫn, thông lượng của một hướng dẫn có thể nhỏ hơn đáng kể so với độ trễ của nó

Các giá trị cho Jcclà:

      Latency   Throughput
Jcc     N/A        0.5

với chú thích sau đây về Jcc:

7) Việc lựa chọn các hướng dẫn nhảy có điều kiện nên dựa trên khuyến nghị của mục 3.4.1, Tối ưu hóa dự đoán chi nhánh, để cải thiện khả năng dự đoán của các chi nhánh. Khi các nhánh được dự đoán thành công, độ trễ jcccó hiệu lực bằng không.

Vì vậy, không có gì trong các tài liệu của Intel từng đối xử với một Jcchướng dẫn khác với các hướng dẫn khác.

Nếu người ta nghĩ về mạch thực tế được sử dụng để thực hiện các hướng dẫn, người ta có thể giả định rằng sẽ có các cổng AND / OR đơn giản trên các bit khác nhau EFLAGSđể xác định xem các điều kiện có được đáp ứng hay không. Do đó, không có lý do gì mà một lệnh kiểm tra hai bit sẽ mất nhiều thời gian hơn hoặc ít hơn một lần kiểm tra chỉ một lần (Bỏ qua độ trễ lan truyền cổng, ít hơn nhiều so với thời gian đồng hồ.)


Chỉnh sửa: Điểm nổi

Điều này cũng đúng với dấu phẩy động x87: (Khá nhiều mã giống như trên, nhưng doublethay vì int.)

        fld     QWORD PTR [esp+32]
        fld     QWORD PTR [esp+40]
        fucomip st, st(1)              ; Compare ST(0) and ST(1), and set CF, PF, ZF in EFLAGS
        fstp    st(0)
        seta    al                     ; Set al if above (CF=0 and ZF=0).
        test    al, al
        je      .L2
        ; Do something 1
.L2:

        fld     QWORD PTR [esp+32]
        fld     QWORD PTR [esp+40]
        fucomip st, st(1)              ; (same thing as above)
        fstp    st(0)
        setae   al                     ; Set al if above or equal (CF=0).
        test    al, al
        je      .L5
        ; Do something 2
.L5:
        leave
        ret

239
@Dyppl thực sự jgjnlelà cùng một hướng dẫn , 7F:-)
Jonathon Reinhart

17
Chưa kể rằng trình tối ưu hóa có thể sửa đổi mã nếu thực sự một tùy chọn nhanh hơn tùy chọn khác.
Elazar Leibovich

3
chỉ vì một cái gì đó dẫn đến cùng một lượng hướng dẫn không nhất thiết có nghĩa là tổng thời gian thực hiện tất cả các hướng dẫn đó sẽ giống nhau. Trên thực tế, nhiều hướng dẫn có thể được thực hiện nhanh hơn. Hướng dẫn trên mỗi chu kỳ không phải là một số cố định, nó thay đổi tùy theo hướng dẫn.
jontejj

22
@jontejj Tôi biết rất nhiều về điều đó. Bạn thậm chí đã đọc câu trả lời của tôi? Tôi đã không nêu bất cứ điều gì về cùng một số hướng dẫn, tôi đã nói rằng chúng được biên dịch về cơ bản chính xác cùng một Barsutcions , ngoại trừ một lệnh nhảy đang nhìn vào một lá cờ và lệnh nhảy khác đang nhìn vào hai lá cờ. Tôi tin rằng tôi đã đưa ra nhiều bằng chứng đầy đủ hơn để cho thấy rằng chúng giống hệt nhau về mặt ngữ nghĩa.
Jonathon Reinhart

2
@jontejj Bạn làm điểm rất tốt. Để có nhiều khả năng hiển thị như câu trả lời này nhận được, tôi có lẽ nên cho nó một chút dọn dẹp. Cảm ơn vì bạn đã phản hồi.
Jonathon Reinhart

593

Trong lịch sử (chúng ta đang nói về những năm 1980 và đầu những năm 1990), có một số kiến trúc trong đó điều này là đúng. Vấn đề gốc là so sánh số nguyên vốn được thực hiện thông qua phép trừ số nguyên. Điều này dẫn đến các trường hợp sau đây.

Comparison     Subtraction
----------     -----------
A < B      --> A - B < 0
A = B      --> A - B = 0
A > B      --> A - B > 0

Bây giờ, khi A < Bphép trừ phải mượn một bit cao để phép trừ được chính xác, giống như bạn mang và mượn khi cộng và trừ bằng tay. Bit "mượn" này thường được gọi là bit mang và có thể kiểm tra được bằng một lệnh rẽ nhánh. Một bit thứ hai được gọi là bit 0 sẽ được đặt nếu phép trừ được xác định bằng 0 có nghĩa là đẳng thức.

Thường có ít nhất hai hướng dẫn nhánh có điều kiện, một đến nhánh trên bit mang và một trên bit 0.

Bây giờ, để đi vào trọng tâm của vấn đề, hãy mở rộng bảng trước để bao gồm kết quả thực hiện và bit không.

Comparison     Subtraction  Carry Bit  Zero Bit
----------     -----------  ---------  --------
A < B      --> A - B < 0    0          0
A = B      --> A - B = 0    1          1
A > B      --> A - B > 0    1          0

Vì vậy, việc thực hiện một nhánh cho A < Bcó thể được thực hiện trong một lệnh, bởi vì bit carry chỉ rõ ràng trong trường hợp này, nghĩa là,

;; Implementation of "if (A < B) goto address;"
cmp  A, B          ;; compare A to B
bcz  address       ;; Branch if Carry is Zero to the new address

Nhưng, nếu chúng ta muốn thực hiện một so sánh nhỏ hơn hoặc bằng nhau, chúng ta cần thực hiện một kiểm tra bổ sung về cờ 0 để bắt trường hợp bằng.

;; Implementation of "if (A <= B) goto address;"
cmp A, B           ;; compare A to B
bcz address        ;; branch if A < B
bzs address        ;; also, Branch if the Zero bit is Set

Vì vậy, trên một số máy, sử dụng so sánh "nhỏ hơn" có thể lưu một lệnh máy . Điều này có liên quan trong kỷ nguyên tốc độ của bộ xử lý sub-megahertz và tỷ lệ tốc độ CPU-bộ nhớ 1: 1, nhưng ngày nay nó gần như không liên quan.


10
Ngoài ra, các kiến ​​trúc như x86 thực hiện các hướng dẫn như jge, trong đó kiểm tra cả cờ 0 và ký hiệu / mang.
greyfade

3
Ngay cả khi nó đúng với một kiến ​​trúc nhất định. Tỷ lệ cược mà không ai trong số các nhà văn biên dịch từng nhận thấy, và thêm một tối ưu hóa để thay thế chậm hơn bằng nhanh hơn?
Jon Hanna

8
Điều này đúng trên 8080. Nó có hướng dẫn nhảy về 0 và nhảy trừ, nhưng không có gì có thể kiểm tra cả hai cùng một lúc.

4
Đây cũng là trường hợp của họ vi xử lý 6502 và 65816, mở rộng cho Motorola 68HC11 / 12, quá.
Lucas

31
Ngay cả trên 8080, một <=bài kiểm tra có thể được thực hiện trong một lệnh với việc hoán đổi toán hạng và kiểm tra not <(tương đương >=) Đây là điều mong muốn <=với các toán hạng được hoán đổi : cmp B,A; bcs addr. Đó là lý do thử nghiệm này đã bị Intel bỏ qua, họ coi đó là dự phòng và bạn không thể đủ khả năng hướng dẫn dự phòng vào những thời điểm đó :-)
Gunther Piez

92

Giả sử chúng ta đang nói về các loại số nguyên nội bộ, không có cách nào có thể nhanh hơn loại kia. Chúng rõ ràng giống hệt nhau về mặt ngữ nghĩa. Cả hai đều yêu cầu trình biên dịch làm chính xác điều tương tự. Chỉ có một trình biên dịch bị hỏng khủng khiếp sẽ tạo ra mã kém hơn cho một trong số đó.

Nếu có một số nền tảng <nhanh hơn <=các kiểu số nguyên đơn giản, trình biên dịch sẽ luôn chuyển đổi <=thành <các hằng số. Bất kỳ trình biên dịch nào không chỉ là một trình biên dịch tồi (cho nền tảng đó).


6
Tôi đồng ý. Không có <hoặc không <=có tốc độ cho đến khi trình biên dịch quyết định tốc độ họ sẽ có. Đây là một tối ưu hóa rất đơn giản cho trình biên dịch khi bạn cho rằng chúng thường thực hiện tối ưu hóa mã chết, tối ưu hóa cuộc gọi đuôi, nâng vòng lặp (và không kiểm soát, đôi khi), tự động song song các vòng lặp khác nhau, v.v ... Tại sao phải lãng phí thời gian để tối ưu hóa sớm ? Nhận một nguyên mẫu đang chạy, lập hồ sơ để xác định vị trí tối ưu hóa quan trọng nhất, thực hiện các tối ưu hóa đó theo thứ tự quan trọng và hồ sơ một lần nữa trên đường để đo lường tiến độ ...
tự kỷ

Vẫn còn một số trường hợp cạnh trong đó việc so sánh có một giá trị không đổi có thể chậm hơn trong <=, ví dụ: khi chuyển đổi từ (a < C)sang (a <= C-1)(đối với một số hằng số C) gây Ckhó khăn hơn để mã hóa trong tập lệnh. Ví dụ, một tập lệnh có thể biểu diễn các hằng số đã ký từ -127 đến 128 ở dạng so sánh, nhưng các hằng số nằm ngoài phạm vi đó phải được tải bằng cách sử dụng mã hóa dài hơn, chậm hơn hoặc hoàn toàn khác. Vì vậy, một so sánh như (a < -127)có thể không có một sự chuyển đổi đơn giản.
BeeOnRope

@BeeOnRope Vấn đề là không phải là liệu các hoạt động biểu diễn mà khác nhau do có hằng số khác nhau trong số họ có thể ảnh hưởng đến hiệu suất nhưng dù bày tỏ sự cùng hoạt động sử dụng các hằng số khác nhau có thể ảnh hưởng đến hiệu suất. Vì vậy, chúng tôi không so sánh a > 127với a > 128vì bạn không có lựa chọn nào ở đó, bạn sử dụng thứ bạn cần. Chúng tôi đang so sánh a > 127với a >= 128, mà không thể yêu cầu mã hóa khác nhau hoặc hướng dẫn khác nhau bởi vì họ có bảng sự thật như vậy. Bất kỳ mã hóa của cái này đều là mã hóa của cái kia.
David Schwartz

Tôi đã trả lời một cách chung chung cho tuyên bố của bạn rằng "Nếu có một nền tảng nào đó trong đó [<= chậm hơn] thì trình biên dịch sẽ luôn chuyển đổi <=thành <hằng số". Theo tôi biết, sự biến đổi đó liên quan đến việc thay đổi hằng số. Ví dụ, a <= 42được biên dịch là a < 43<nhanh hơn. Trong một số trường hợp cạnh, một phép biến đổi như vậy sẽ không có kết quả vì hằng số mới có thể yêu cầu các hướng dẫn chậm hơn hoặc chậm hơn. Tất nhiên a > 127a >= 128tương đương và một trình biên dịch nên mã hóa cả hai biểu mẫu theo cách nhanh nhất (giống nhau), nhưng điều đó không phù hợp với những gì tôi đã nói.
BeeOnRope

67

Tôi thấy rằng không phải là nhanh hơn. Trình biên dịch tạo cùng một mã máy trong mỗi điều kiện với một giá trị khác nhau.

if(a < 901)
cmpl  $900, -4(%rbp)
jg .L2

if(a <=901)
cmpl  $901, -4(%rbp)
jg .L3

Ví dụ của tôi iflà từ GCC trên nền tảng x86_64 trên Linux.

Các nhà văn biên dịch là những người khá thông minh, và họ nghĩ về những điều này và nhiều người khác mà hầu hết chúng ta đều coi là điều hiển nhiên.

Tôi nhận thấy rằng nếu nó không phải là hằng số, thì cùng một mã máy được tạo ra trong cả hai trường hợp.

int b;
if(a < b)
cmpl  -4(%rbp), %eax
jge   .L2

if(a <=b)
cmpl  -4(%rbp), %eax
jg .L3

9
Lưu ý rằng điều này là cụ thể cho x86.
Michael Petrotta

10
Tôi nghĩ bạn nên sử dụng điều đó if(a <=900)để chứng minh rằng nó tạo ra chính xác asm :)
Lipis

2
@AdrianCornish Xin lỗi .. Tôi đã chỉnh sửa nó .. nó ít nhiều giống nhau .. nhưng nếu bạn thay đổi lần thứ hai nếu thành <= 900 thì mã asm sẽ hoàn toàn giống nhau :) Bây giờ khá giống nhau .. nhưng bạn biết .. cho OCD :)
Lipis

3
@Boann Điều đó có thể được giảm xuống nếu (đúng) và loại bỏ hoàn toàn.
Qsario

5
Không ai chỉ ra rằng tối ưu hóa này chỉ áp dụng cho các so sánh không đổi . Tôi có thể đảm bảo nó sẽ không được thực hiện như thế này để so sánh hai biến.
Jonathon Reinhart

51

Đối với mã dấu phẩy động, việc so sánh <= thực sự có thể chậm hơn (theo một hướng dẫn) ngay cả trên các kiến ​​trúc hiện đại. Đây là chức năng đầu tiên:

int compare_strict(double a, double b) { return a < b; }

Trên PowerPC, đầu tiên, nó thực hiện so sánh dấu phẩy động (cập nhật cr, thanh ghi điều kiện), sau đó di chuyển thanh ghi điều kiện sang GPR, chuyển bit "được so sánh nhỏ hơn" vào vị trí, sau đó trả về. Phải mất bốn hướng dẫn.

Bây giờ hãy xem xét chức năng này thay thế:

int compare_loose(double a, double b) { return a <= b; }

Điều này đòi hỏi công việc tương tự như compare_stricttrên, nhưng bây giờ có hai bit quan tâm: "nhỏ hơn" và "bằng". Điều này đòi hỏi một lệnh bổ sung ( cror- thanh ghi điều kiện theo bit OR) để kết hợp hai bit này thành một. Vì vậy, compare_looseyêu cầu năm hướng dẫn, trong khi compare_strictyêu cầu bốn.

Bạn có thể nghĩ rằng trình biên dịch có thể tối ưu hóa chức năng thứ hai như vậy:

int compare_loose(double a, double b) { return ! (a > b); }

Tuy nhiên, điều này sẽ xử lý NaN không chính xác. NaN1 <= NaN2NaN1 > NaN2cần phải đánh giá thành sai.


May mắn thay, nó không hoạt động như thế này trên x86 (x87). fucomipđặt ZF và CF.
Jonathon Reinhart

3
@JonathonReinhart: Tôi nghĩ rằng bạn đang hiểu lầm gì PowerPC đang làm - thanh ghi điều kiện cr tương đương với lá cờ như ZFCFtrên x86. (Mặc dù CR linh hoạt hơn.) Những gì người đăng đang nói về việc chuyển kết quả sang GPR: có hai hướng dẫn trên PowerPC, nhưng x86 có hướng dẫn di chuyển có điều kiện.
Dietrich Epp

@DietrichEpp Điều tôi muốn nói sau câu nói của tôi là: Bạn có thể ngay lập tức nhảy lên dựa trên giá trị của EFLAGS. Xin lỗi vì không được rõ ràng.
Jonathon Reinhart

1
@JonathonReinhart: Có, và bạn cũng có thể nhảy ngay lập tức dựa trên giá trị của CR. Câu trả lời không phải là nói về việc nhảy, đó là nơi mà các hướng dẫn thêm đến từ.
Dietrich Epp

34

Có lẽ tác giả của cuốn sách không tên đó đã đọc a > 0chạy nhanh hơn a >= 1và nghĩ rằng đó là sự thật trên toàn cầu.

Nhưng đó là vì a 0có liên quan (vì CMPcó thể, tùy thuộc vào kiến ​​trúc, được thay thế, ví dụ như OR) và không phải vì <.


1
Chắc chắn, trong một bản dựng "gỡ lỗi", nhưng sẽ cần một trình biên dịch tồi (a >= 1)để chạy chậm hơn (a > 0), vì cái trước có thể được chuyển đổi thành cái sau bởi trình tối ưu hóa ..
BeeOnRope

2
@BeeOnRope Đôi khi tôi ngạc nhiên về những điều phức tạp mà trình tối ưu hóa có thể tối ưu hóa và những điều dễ dàng mà nó không thực hiện được.
glglgl

1
Thật vậy, và luôn luôn đáng để kiểm tra đầu ra asm cho rất ít chức năng mà nó quan trọng. Điều đó nói rằng, việc chuyển đổi ở trên là rất cơ bản và đã được thực hiện ngay cả trong các trình biên dịch đơn giản trong nhiều thập kỷ.
BeeOnRope

32

Ít nhất, nếu đây là sự thật thì trình biên dịch có thể tối ưu hóa một cách tầm thường một <= b thành! (A> b), và vì vậy ngay cả khi bản so sánh thực sự chậm hơn, với tất cả nhưng trình biên dịch ngây thơ nhất bạn sẽ không nhận thấy sự khác biệt .


Tại sao! (A> b) là phiên bản tối ưu hóa của a <= b. Không phải! (A> b) 2 thao tác trong một?
Abhishek Singh

6
@AbhishekSingh NOTchỉ được thực hiện bởi hướng dẫn khác ( jeso với jne)
Pavel Gatnar

15

Họ có cùng tốc độ. Có thể trong một số kiến ​​trúc đặc biệt những gì anh ấy / cô ấy nói là đúng, nhưng trong gia đình x86 ít nhất tôi biết họ giống nhau. Bởi vì để làm điều này, CPU sẽ thực hiện một phép con (a - b) và sau đó kiểm tra các cờ của thanh ghi cờ. Hai bit của thanh ghi đó được gọi là ZF (zero Flag) và SF (cờ ký hiệu) và nó được thực hiện trong một chu kỳ, bởi vì nó sẽ thực hiện nó với một thao tác mặt nạ.


14

Điều này sẽ phụ thuộc nhiều vào kiến ​​trúc cơ bản mà C được biên dịch. Một số bộ xử lý và kiến ​​trúc có thể có các hướng dẫn rõ ràng bằng hoặc nhỏ hơn và bằng, thực thi theo số chu kỳ khác nhau.

Điều đó sẽ khá bất thường, vì trình biên dịch có thể làm việc xung quanh nó, làm cho nó không liên quan.


1
NẾU có một sự khác biệt trong các kiểu. 1) nó sẽ không thể được phát hiện. 2) Bất kỳ trình biên dịch nào có giá trị muối của nó sẽ thực hiện chuyển đổi từ dạng chậm sang dạng nhanh hơn mà không thay đổi ý nghĩa của mã. Vì vậy, hướng dẫn kết quả trồng sẽ giống hệt nhau.
Martin York

Đồng ý hoàn toàn, nó sẽ là một sự khác biệt khá tầm thường và ngớ ngẩn trong mọi trường hợp. Chắc chắn không có gì để đề cập trong một cuốn sách nên là nền tảng bất khả tri.
Telgin

@lttlrck: Tôi hiểu rồi. Mất tôi một lúc (ngớ ngẩn với tôi). Không, chúng không thể được phát hiện bởi vì có rất nhiều điều khác xảy ra khiến cho phép đo của chúng không thể thay thế được. Gian hàng bộ xử lý / bộ nhớ cache / tín hiệu / quá trình hoán đổi. Do đó, trong một tình huống HĐH bình thường, mọi thứ ở cấp độ chu kỳ đơn lẻ không thể đo lường được. Nếu bạn có thể loại bỏ tất cả các nhiễu đó từ các phép đo của mình (chạy trên chip có bộ nhớ trên bo mạch và không có HĐH) thì bạn vẫn phải lo lắng về thời gian của mình nhưng về mặt lý thuyết nếu bạn chạy đủ lâu, bạn có thể thấy thứ gì đó.
Martin York

12

TL; DR trả lời

Đối với hầu hết các kết hợp kiến ​​trúc, trình biên dịch và ngôn ngữ, nó sẽ không nhanh hơn.

Câu trả lời đầy đủ

Câu trả lời khác đã tập trung vào x86 kiến trúc, và tôi không biết ARM kiến trúc (mà ví dụ lắp ráp của bạn có vẻ là) cũng đủ để bình luận cụ thể về các mã được tạo, nhưng đây là một ví dụ về một vi-tối ưu hóa rất kiến trúc cụ thể, và có khả năng là một tối ưu hóa chống lại nó như là một tối ưu hóa .

Như vậy, tôi sẽ đề xuất rằng loại tối ưu hóa vi mô này là một ví dụ về lập trình sùng bái hàng hóa hơn là thực hành kỹ thuật phần mềm tốt nhất.

Có thể có một số kiến trúc trong đó đây là một tối ưu hóa, nhưng tôi biết ít nhất một kiến ​​trúc mà điều ngược lại có thể đúng. Kiến trúc Transputer đáng kính chỉ có các hướng dẫn mã máy bằnglớn hơn hoặc bằng , vì vậy tất cả các so sánh phải được xây dựng từ các nguyên thủy này.

Thậm chí sau đó, trong hầu hết các trường hợp, trình biên dịch có thể ra lệnh cho các hướng dẫn đánh giá theo cách mà trong thực tế, không có sự so sánh nào có lợi thế hơn bất kỳ trường hợp nào khác. Trong trường hợp xấu nhất, có thể cần phải thêm một lệnh ngược (REV) để hoán đổi hai mục trên cùng trên ngăn xếp toán hạng . Đây là một lệnh byte đơn, chỉ mất một chu kỳ để chạy, do đó có chi phí nhỏ nhất có thể.

Việc tối ưu hóa vi mô như thế này là tối ưu hóa hay chống tối ưu hóa tùy thuộc vào kiến ​​trúc cụ thể bạn đang sử dụng, do đó, thường là một ý tưởng tồi để tập thói quen sử dụng tối ưu hóa vi mô cụ thể, nếu không bạn có thể theo bản năng sử dụng một khi nó không phù hợp để làm như vậy, và có vẻ như đây chính xác là những gì cuốn sách bạn đang đọc đang ủng hộ.


6

Bạn không thể nhận thấy sự khác biệt ngay cả khi có bất kỳ. Bên cạnh đó, trong thực tế, bạn sẽ phải thực hiện thêm a + 1hoặc a - 1làm cho tình trạng ổn định trừ khi bạn sẽ sử dụng một số hằng số ma thuật, đây là một cách thực hành rất tệ.


1
Thực hành xấu là gì? Tăng hoặc giảm một truy cập? Làm thế nào để bạn lưu trữ ký hiệu chỉ số sau đó?
jcolebrand

5
Anh ta có nghĩa là nếu bạn đang so sánh 2 loại biến. Tất nhiên là tầm thường nếu bạn đặt giá trị cho một vòng lặp hoặc thứ gì đó. Nhưng nếu bạn có x <= y và y không xác định, việc 'tối ưu hóa' nó thành x <y + 1
JustinDanielson

@JustinDanielson đồng ý. Không đề cập đến xấu xí, khó hiểu, vv
Jonathon Reinhart

4

Bạn có thể nói rằng dòng đó là chính xác trong hầu hết các ngôn ngữ kịch bản, vì ký tự phụ dẫn đến việc xử lý mã chậm hơn một chút. Tuy nhiên, như câu trả lời hàng đầu đã chỉ ra, nó sẽ không có tác dụng trong C ++ và bất cứ điều gì được thực hiện với ngôn ngữ kịch bản có lẽ không liên quan đến tối ưu hóa.


Tôi hơi không đồng ý. Trong lập trình cạnh tranh, các ngôn ngữ kịch bản thường đưa ra giải pháp nhanh nhất cho một vấn đề, nhưng phải áp dụng các kỹ thuật chính xác (đọc: tối ưu hóa) để có được giải pháp chính xác.
Tyler Crompton

3

Khi tôi viết câu trả lời này, tôi chỉ nhìn vào câu hỏi tiêu đề về <vs. <= nói chung, không phải là ví dụ cụ thể về hằng số a < 901so với a <= 900. Nhiều trình biên dịch luôn thu nhỏ độ lớn của các hằng số bằng cách chuyển đổi giữa <<=, ví dụ vì toán hạng tức thời x86 có mã hóa 1 byte ngắn hơn cho -128..127.

Đối với ARM và đặc biệt là AArch64, việc có thể mã hóa ngay lập tức phụ thuộc vào việc có thể xoay một trường hẹp vào bất kỳ vị trí nào trong một từ. Vì vậy, cmp w0, #0x00f000sẽ được mã hóa, trong khi cmp w0, #0x00effffcó thể không. Vì vậy, quy tắc làm cho nó nhỏ hơn để so sánh với hằng số thời gian biên dịch không phải lúc nào cũng áp dụng cho AArch64.


<vs. <= nói chung, bao gồm cả các điều kiện biến thời gian chạy

Trong ngôn ngữ lắp ráp trên hầu hết các máy, một so sánh <=có cùng chi phí so với so sánh <. Điều này áp dụng cho dù bạn đang phân nhánh trên nó, booleanize nó để tạo số nguyên 0/1 hoặc sử dụng nó làm vị ngữ cho một hoạt động chọn không phân nhánh (như x86 CMOV). Các câu trả lời khác chỉ giải quyết phần này của câu hỏi.

Nhưng câu hỏi này là về các toán tử C ++, đầu vào cho trình tối ưu hóa. Thông thường cả hai đều hiệu quả như nhau; lời khuyên từ cuốn sách nghe có vẻ hoàn toàn không có thật bởi vì trình biên dịch luôn có thể biến đổi sự so sánh mà chúng thực hiện trong asm. Nhưng có ít nhất một ngoại lệ khi sử dụng <=có thể vô tình tạo ra thứ gì đó trình biên dịch không thể tối ưu hóa.

Là một điều kiện vòng lặp, có những trường hợp <=chất lượng khác nhau từ <khi nó dừng lại trình biên dịch từ chứng minh rằng một vòng lặp không phải là vô hạn. Điều này có thể tạo ra một sự khác biệt lớn, vô hiệu hóa tự động vector hóa.

Tràn không được ký được xác định rõ là bao quanh cơ sở 2, không giống như tràn tràn đã ký (UB). Các bộ đếm vòng lặp đã ký thường an toàn với điều này với các trình biên dịch tối ưu hóa dựa trên UB tràn tràn đã ký không xảy ra: ++i <= sizecuối cùng sẽ luôn trở thành sai. ( Điều mà mọi lập trình viên C nên biết về hành vi không xác định )

void foo(unsigned size) {
    unsigned upper_bound = size - 1;  // or any calculation that could produce UINT_MAX
    for(unsigned i=0 ; i <= upper_bound ; i++)
        ...

Trình biên dịch chỉ có thể tối ưu hóa theo cách bảo tồn hành vi (được xác định và có thể quan sát được về mặt pháp lý) của nguồn C ++ cho tất cả các giá trị đầu vào có thể , ngoại trừ các giá trị dẫn đến hành vi không xác định.

(Một đơn giản i <= sizecũng sẽ tạo ra vấn đề, nhưng tôi nghĩ tính toán giới hạn trên là một ví dụ thực tế hơn về việc vô tình đưa ra khả năng của một vòng lặp vô hạn cho một đầu vào mà bạn không quan tâm nhưng trình biên dịch phải xem xét.)

Trong trường hợp này, size=0dẫn đến upper_bound=UINT_MAX, và i <= UINT_MAXluôn luôn đúng. Vì vậy, vòng lặp này là vô hạn size=0và trình biên dịch phải tôn trọng điều đó mặc dù bạn là lập trình viên có thể không bao giờ có ý định vượt qua size = 0. Nếu trình biên dịch có thể nội tuyến hàm này vào một người gọi trong đó nó có thể chứng minh rằng size = 0 là không thể, thì thật tuyệt, nó có thể tối ưu hóa như nó có thể i < size.

Asm like if(!size) skip the loop; do{...}while(--size);là một cách hiệu quả thông thường để tối ưu hóa for( i<size )vòng lặp, nếu giá trị thực tế ikhông cần thiết bên trong vòng lặp ( Tại sao các vòng lặp luôn được biên dịch thành kiểu "do ... while" (nhảy đuôi)? ).

Nhưng điều đó làm {} trong khi không thể là vô hạn: nếu được nhập cùng size==0, chúng ta sẽ nhận được 2 ^ n lần lặp. ( Lặp lại tất cả các số nguyên không dấu trong một vòng lặp for C cho phép thể hiện một vòng lặp trên tất cả các số nguyên không dấu bao gồm 0, nhưng không dễ dàng gì nếu không có cờ mang theo cách thực hiện.)

Với khả năng bao gồm bộ đếm vòng lặp là một khả năng, các trình biên dịch hiện đại thường chỉ "từ bỏ" và không tối ưu hóa gần như mạnh mẽ.

Ví dụ: tổng các số nguyên từ 1 đến n

Sử dụng unsign i <= nđánh bại nhận dạng thành ngữ của clang để tối ưu hóa sum(1 .. n)các vòng lặp với dạng đóng dựa trên n * (n+1) / 2công thức của Gauss .

unsigned sum_1_to_n_finite(unsigned n) {
    unsigned total = 0;
    for (unsigned i = 0 ; i < n+1 ; ++i)
        total += i;
    return total;
}

x86-64 asm từ clang7.0 và gcc8.2 trên trình thám hiểm trình biên dịch Godbolt

 # clang7.0 -O3 closed-form
    cmp     edi, -1       # n passed in EDI: x86-64 System V calling convention
    je      .LBB1_1       # if (n == UINT_MAX) return 0;  // C++ loop runs 0 times
          # else fall through into the closed-form calc
    mov     ecx, edi         # zero-extend n into RCX
    lea     eax, [rdi - 1]   # n-1
    imul    rax, rcx         # n * (n-1)             # 64-bit
    shr     rax              # n * (n-1) / 2
    add     eax, edi         # n + (stuff / 2) = n * (n+1) / 2   # truncated to 32-bit
    ret          # computed without possible overflow of the product before right shifting
.LBB1_1:
    xor     eax, eax
    ret

Nhưng đối với phiên bản ngây thơ, chúng ta chỉ nhận được một vòng lặp ngu ngốc từ tiếng kêu.

unsigned sum_1_to_n_naive(unsigned n) {
    unsigned total = 0;
    for (unsigned i = 0 ; i<=n ; ++i)
        total += i;
    return total;
}
# clang7.0 -O3
sum_1_to_n(unsigned int):
    xor     ecx, ecx           # i = 0
    xor     eax, eax           # retval = 0
.LBB0_1:                       # do {
    add     eax, ecx             # retval += i
    add     ecx, 1               # ++1
    cmp     ecx, edi
    jbe     .LBB0_1            # } while( i<n );
    ret

GCC không sử dụng dạng đóng theo bất kỳ cách nào, do đó, việc lựa chọn điều kiện vòng lặp không thực sự làm tổn thương nó ; nó tự động vectơ với phép cộng số nguyên SIMD, chạy isong song 4 giá trị trong các phần tử của thanh ghi XMM.

# "naive" inner loop
.L3:
    add     eax, 1       # do {
    paddd   xmm0, xmm1    # vect_total_4.6, vect_vec_iv_.5
    paddd   xmm1, xmm2    # vect_vec_iv_.5, tmp114
    cmp     edx, eax      # bnd.1, ivtmp.14     # bound and induction-variable tmp, I think.
    ja      .L3 #,       # }while( n > i )

 "finite" inner loop
  # before the loop:
  # xmm0 = 0 = totals
  # xmm1 = {0,1,2,3} = i
  # xmm2 = set1_epi32(4)
 .L13:                # do {
    add     eax, 1       # i++
    paddd   xmm0, xmm1    # total[0..3] += i[0..3]
    paddd   xmm1, xmm2    # i[0..3] += 4
    cmp     eax, edx
    jne     .L13      # }while( i != upper_limit );

     then horizontal sum xmm0
     and peeled cleanup for the last n%3 iterations, or something.

Nó cũng có một vòng vô hướng đơn giản mà tôi nghĩ rằng nó sử dụng cho rất nhỏ nvà / hoặc cho trường hợp vòng lặp vô hạn.

BTW, cả hai vòng lặp này đều lãng phí một lệnh (và một uop trên CPU gia đình Sandybridge) trên đầu vòng lặp. sub eax,1/ jnzthay vì add eax,1/ cmp / jcc sẽ hiệu quả hơn. 1 uop thay vì 2 (sau khi tổng hợp macro của sub / jcc hoặc cmp / jcc). Mã sau cả hai vòng ghi EAX vô điều kiện, do đó, nó không sử dụng giá trị cuối cùng của bộ đếm vòng lặp.


Đẹp ví dụ. Còn nhận xét khác của bạn về một hiệu ứng tiềm năng trong việc thực hiện đơn hàng do sử dụng EFLAGS thì sao? Đây hoàn toàn là lý thuyết hay thực sự có thể xảy ra rằng JB dẫn đến một đường ống tốt hơn JBE?
rustyx

@rustyx: Tôi đã nhận xét rằng ở đâu đó dưới một câu trả lời khác? Trình biên dịch sẽ không phát ra mã gây ra các gian hàng cờ một phần và chắc chắn không phải cho C <hoặc <=. Nhưng chắc chắn, test ecx,ecx/ bt eax, 3/ jbesẽ nhảy nếu ZF được đặt (ecx == 0) hoặc nếu CF được đặt (bit 3 của EAX == 1), khiến cờ bị treo một phần trên hầu hết các CPU vì các cờ mà nó đọc không phải là tất cả đến từ hướng dẫn cuối cùng để viết bất kỳ cờ. Trên gia đình Sandybridge, nó không thực sự bị đình trệ, chỉ cần chèn một uop hợp nhất. cmp/ testviết tất cả các cờ, nhưng btđể lại ZF không thay đổi. felixcloutier.com/x86/bt
Peter Cordes

2

Chỉ khi những người tạo ra máy tính xấu với logic boolean. Mà họ không nên.

Mọi so sánh ( >= <= > <) có thể được thực hiện trong cùng một tốc độ.

Mọi so sánh là gì, chỉ là một phép trừ (sự khác biệt) và xem nếu nó tích cực / tiêu cực.
(Nếu msbđược đặt, số âm)

Làm thế nào để kiểm tra a >= b? a-b >= 0Kiểm tra phụ nếu a-blà dương tính.
Làm thế nào để kiểm tra a <= b? 0 <= b-aKiểm tra phụ nếu b-alà dương tính.
Làm thế nào để kiểm tra a < b? a-b < 0Kiểm tra phụ nếu a-bâm tính.
Làm thế nào để kiểm tra a > b? 0 > b-aKiểm tra phụ nếu b-aâm tính.

Nói một cách đơn giản, máy tính chỉ có thể thực hiện việc này bên dưới mui xe cho op đã cho:

a >= b== msb(a-b)==0
a <= b== msb(b-a)==0
a > b== msb(b-a)==1
a < b== =msb(a-b)==1

và tất nhiên là máy tính sẽ không thực sự cần phải làm ==0hoặc ==1một trong hai.
cho ==0nó chỉ có thể đảo ngược msbtừ mạch.

Dù sao, họ chắc chắn sẽ không a >= bđược tính là a>b || a==blol

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.