Một bước nhảy đắt giá với GCC 5.4.0


171

Tôi có một chức năng trông như thế này (chỉ hiển thị phần quan trọng):

double CompareShifted(const std::vector<uint16_t>& l, const std::vector<uint16_t> &curr, int shift, int shiftY)  {
...
  for(std::size_t i=std::max(0,-shift);i<max;i++) {
     if ((curr[i] < 479) && (l[i + shift] < 479)) {
       nontopOverlap++;
     }
     ...
  }
...
}

Viết như thế này, chức năng mất ~ 34ms trên máy của tôi. Sau khi thay đổi điều kiện thành phép nhân bool (làm cho mã trông như thế này):

double CompareShifted(const std::vector<uint16_t>& l, const std::vector<uint16_t> &curr, int shift, int shiftY)  {
...
  for(std::size_t i=std::max(0,-shift);i<max;i++) {
     if ((curr[i] < 479) * (l[i + shift] < 479)) {
       nontopOverlap++;
     }
     ...
  }
...
}

thời gian thực hiện giảm xuống ~ 19ms.

Trình biên dịch được sử dụng là GCC 5.4.0 với -O3 và sau khi kiểm tra mã asm được tạo bằng godbolt.org tôi phát hiện ra rằng ví dụ đầu tiên tạo ra một bước nhảy, trong khi ví dụ thứ hai thì không. Tôi đã quyết định thử GCC 6.2.0, nó cũng tạo ra một lệnh nhảy khi sử dụng ví dụ đầu tiên, nhưng GCC 7 dường như không tạo ra một cái nào nữa.

Tìm ra cách này để tăng tốc mã khá khủng khiếp và mất khá nhiều thời gian. Tại sao trình biên dịch hành xử theo cách này? Có dự định và nó là một cái gì đó các lập trình viên nên tìm ra? Có bất cứ điều gì tương tự như thế này?

EDIT: liên kết đến godbolt https://godbolt.org/g/5lKPF3


17
Tại sao trình biên dịch hành xử theo cách này? Trình biên dịch có thể làm theo ý muốn, miễn là mã được tạo là chính xác. Một số trình biên dịch đơn giản là tốt hơn ở tối ưu hóa so với những người khác.
Jabberwocky

26
Tôi đoán là đánh giá ngắn mạch &&nguyên nhân này.
Jens

9
Lưu ý rằng đây là lý do tại sao chúng ta cũng có &.
rubenvb

7
@Jakub sắp xếp nó rất có thể sẽ tăng tốc độ thực hiện, xem câu hỏi này .
rubenvb

8
@rubenvb "không được đánh giá" thực sự không có ý nghĩa gì đối với một biểu thức không có tác dụng phụ. Tôi nghi ngờ rằng vectơ không kiểm tra giới hạn và GCC không thể chứng minh rằng nó sẽ không nằm ngoài giới hạn. EDIT: Trên thực tế, tôi không nghĩ rằng bạn đang làm bất cứ điều gì để ngăn i + shift khỏi giới hạn.
Random832

Câu trả lời:


263

Toán tử AND logic (&& ) sử dụng đánh giá ngắn mạch, có nghĩa là thử nghiệm thứ hai chỉ được thực hiện nếu so sánh đầu tiên đánh giá là đúng. Điều này thường chính xác là ngữ nghĩa mà bạn yêu cầu. Ví dụ, hãy xem xét mã sau đây:

if ((p != nullptr) && (p->first > 0))

Bạn phải đảm bảo rằng con trỏ không rỗng trước khi bạn hủy đăng ký nó. Nếu điều này không phải là một đánh giá ngắn mạch, bạn sẽ có hành vi không xác định vì bạn sẽ hủy bỏ một con trỏ rỗng.

Cũng có thể đánh giá ngắn mạch mang lại hiệu suất đạt được trong trường hợp đánh giá các điều kiện là một quá trình tốn kém. Ví dụ:

if ((DoLengthyCheck1(p) && (DoLengthyCheck2(p))

Nếu DoLengthyCheck1 thất bại, không có điểm nào trong cuộc gọiDoLengthyCheck2 .

Tuy nhiên, trong nhị phân kết quả, một hoạt động ngắn mạch thường dẫn đến hai nhánh, vì đây là cách dễ nhất để trình biên dịch bảo tồn các ngữ nghĩa này. (Đó là lý do tại sao, ở phía bên kia của đồng xu, việc đánh giá ngắn mạch đôi khi có thể ức chế tiềm năng tối ưu hóa.) Bạn có thể thấy điều này bằng cách xem phần mã có liên quan được tạo cho ifcâu lệnh của bạn bằng GCC 5.4:

    movzx   r13d, WORD PTR [rbp+rcx*2]
    movzx   eax,  WORD PTR [rbx+rcx*2]

    cmp     r13w, 478         ; (curr[i] < 479)
    ja      .L5

    cmp     ax, 478           ; (l[i + shift] < 479)
    ja      .L5

    add     r8d, 1            ; nontopOverlap++

Bạn thấy ở đây hai so sánh ( cmphướng dẫn) ở đây, mỗi lần theo sau là một bước nhảy / nhánh có điều kiện riêng ( jahoặc nhảy nếu ở trên).

Đó là một quy tắc chung của ngón tay cái rằng các nhánh chậm và do đó phải tránh trong các vòng lặp chặt chẽ. Điều này đúng với hầu hết tất cả các bộ xử lý x86, từ 8088 khiêm tốn (có thời gian tìm nạp chậm và hàng đợi tìm nạp cực nhỏ [có thể so sánh với bộ đệm hướng dẫn], kết hợp với việc thiếu dự đoán nhánh, có nghĩa là các nhánh bị mất yêu cầu phải xóa bộ đệm. ) cho đến các triển khai hiện đại (có đường ống dài làm cho các nhánh bị dự đoán sai tương tự đắt tiền). Lưu ý cảnh báo nhỏ mà tôi trượt trong đó. Bộ xử lý hiện đại kể từ Pentium Pro có các công cụ dự đoán chi nhánh tiên tiến được thiết kế để giảm thiểu chi phí cho các chi nhánh. Nếu hướng của chi nhánh có thể được dự đoán đúng, chi phí là tối thiểu. Hầu hết thời gian, điều này hoạt động tốt, nhưng nếu bạn gặp phải các trường hợp bệnh lý trong đó công cụ dự đoán nhánh không đứng về phía bạn,mã của bạn có thể rất chậm . Đây có lẽ là nơi bạn đang ở đây, vì bạn nói rằng mảng của bạn chưa được sắp xếp.

Bạn nói rằng các điểm chuẩn đã xác nhận rằng việc thay thế &&bằng một *làm cho mã nhanh hơn đáng kể. Lý do cho điều này là hiển nhiên khi chúng ta so sánh phần có liên quan của mã đối tượng:

    movzx   r13d, WORD PTR [rbp+rcx*2]
    movzx   eax,  WORD PTR [rbx+rcx*2]

    xor     r15d, r15d        ; (curr[i] < 479)
    cmp     r13w, 478
    setbe   r15b

    xor     r14d, r14d        ; (l[i + shift] < 479)
    cmp     ax, 478
    setbe   r14b

    imul    r14d, r15d        ; meld results of the two comparisons

    cmp     r14d, 1           ; nontopOverlap++
    sbb     r8d, -1

Có một chút phản trực giác rằng điều này có thể nhanh hơn, vì có nhiều hướng dẫn hơn ở đây, nhưng đó là cách tối ưu hóa đôi khi hoạt động. Bạn thấy các phép so sánh tương tự ( cmp) đang được thực hiện ở đây, nhưng bây giờ, mỗi cái được đi trước bởi một xorvà theo sau là a setbe. XOR chỉ là một mẹo tiêu chuẩn để xóa sổ đăng ký. Lệnh setbex86 đặt một bit dựa trên giá trị của cờ và thường được sử dụng để triển khai mã không phân nhánh. Ở đây, setbelà nghịch đảo của ja. Nó đặt thanh ghi đích của nó thành 1 nếu so sánh là dưới hoặc bằng (vì thanh ghi được đặt trước 0, nó sẽ là 0 nếu không), trong khi japhân nhánh nếu so sánh ở trên. Một khi hai giá trị này đã đạt được trongr15br14bđăng ký, chúng được nhân với nhau bằng cách sử dụng imul. Truyền thống là một hoạt động tương đối chậm, nhưng nó rất nhanh trên các bộ xử lý hiện đại và điều này sẽ đặc biệt nhanh, bởi vì nó chỉ nhân hai giá trị kích thước byte.

Bạn có thể dễ dàng thay thế phép nhân bằng toán tử bitwise AND ( &), không thực hiện đánh giá ngắn mạch. Điều này làm cho mã rõ ràng hơn nhiều và là một mẫu mà trình biên dịch thường nhận ra. Nhưng khi bạn làm điều này với mã của mình và biên dịch nó với GCC 5.4, nó sẽ tiếp tục phát ra nhánh đầu tiên:

    movzx   r13d, WORD PTR [rbp+rcx*2]
    movzx   eax,  WORD PTR [rbx+rcx*2]

    cmp     r13w, 478         ; (curr[i] < 479)
    ja      .L4

    cmp     ax, 478           ; (l[i + shift] < 479)
    setbe   r14b

    cmp     r14d, 1           ; nontopOverlap++
    sbb     r8d, -1

Không có lý do kỹ thuật nào mà nó phải phát mã theo cách này, nhưng vì một số lý do, các heuristic bên trong của nó đang nói với nó rằng điều này nhanh hơn. Nó sẽ có thể được nhanh hơn nếu các yếu tố dự báo chi nhánh là về phía bạn, nhưng nó có khả năng sẽ chậm hơn nếu dự đoán rẽ nhánh không thường xuyên hơn nó thành công.

Các thế hệ trình biên dịch mới hơn (và các trình biên dịch khác, như Clang) biết quy tắc này, và đôi khi sẽ sử dụng nó để tạo cùng một mã mà bạn đã tìm kiếm bằng cách tối ưu hóa bằng tay. Tôi thường xuyên thấy Clang dịch các &&biểu thức sang cùng một mã sẽ được phát ra nếu tôi đã sử dụng &. Sau đây là đầu ra có liên quan từ GCC 6.2 với mã của bạn bằng &&toán tử thông thường :

    movzx   r13d, WORD PTR [rbp+rcx*2]
    movzx   eax,  WORD PTR [rbx+rcx*2]

    cmp     r13d, 478         ; (curr[i] < 479)
    jg      .L7

    xor     r14d, r14d        ; (l[i + shift] < 479)
    cmp     eax, 478
    setle   r14b

    add     esi, r14d         ; nontopOverlap++

Lưu ý làm thế nào thông minh này là! Nó đang sử dụng các điều kiện đã ký ( jgsetle) trái ngược với các điều kiện không dấu ( jasetbe), nhưng điều này không quan trọng. Bạn có thể thấy rằng nó vẫn thực hiện so sánh và phân nhánh cho điều kiện đầu tiên như phiên bản cũ hơn và sử dụng cùng một setCChướng dẫn để tạo mã không phân nhánh cho điều kiện thứ hai, nhưng nó đã hiệu quả hơn rất nhiều trong cách tăng . Thay vì thực hiện so sánh thứ hai, dự phòng để đặt cờ cho một sbbthao tác, nó sử dụng kiến ​​thức r14dsẽ là 1 hoặc 0 để thêm giá trị này vô điều kiện vào nontopOverlap. Nếu r14dlà 0, thì phép cộng là không có op; mặt khác, nó thêm 1, chính xác như nó được cho là phải làm.

GCC 6.2 thực sự tạo ra mã hiệu quả hơn khi bạn sử dụng &&toán tử ngắn mạch so với toán &tử bitwise :

    movzx   r13d, WORD PTR [rbp+rcx*2]
    movzx   eax,  WORD PTR [rbx+rcx*2]

    cmp     r13d, 478         ; (curr[i] < 479)
    jg      .L6

    cmp     eax, 478          ; (l[i + shift] < 479)
    setle   r14b

    cmp     r14b, 1           ; nontopOverlap++
    sbb     esi, -1

Chi nhánh và tập hợp điều kiện vẫn còn đó, nhưng bây giờ nó trở lại cách tăng ít thông minh hơn nontopOverlap. Đây là một bài học quan trọng tại sao bạn nên cẩn thận khi cố gắng vượt qua trình biên dịch của mình!

Nhưng nếu bạn có thể chứng minh với điểm chuẩn rằng mã phân nhánh thực sự chậm hơn, thì có thể phải trả tiền để thử và vượt qua trình biên dịch của bạn. Bạn chỉ cần làm như vậy với việc kiểm tra cẩn thận bộ tháo gỡ và chuẩn bị để đánh giá lại các quyết định của bạn khi bạn nâng cấp lên phiên bản mới hơn của trình biên dịch. Ví dụ: mã bạn có thể được viết lại thành:

nontopOverlap += ((curr[i] < 479) & (l[i + shift] < 479));

Không có iftuyên bố nào ở đây cả, và đại đa số các trình biên dịch sẽ không bao giờ nghĩ về việc phát ra mã phân nhánh cho việc này. GCC cũng không ngoại lệ; tất cả các phiên bản tạo ra một cái gì đó giống như sau:

    movzx   r14d, WORD PTR [rbp+rcx*2]
    movzx   eax,  WORD PTR [rbx+rcx*2]

    cmp     r14d, 478         ; (curr[i] < 479)
    setle   r15b

    xor     r13d, r13d        ; (l[i + shift] < 479)
    cmp     eax, 478
    setle   r13b

    and     r13d, r15d        ; meld results of the two comparisons
    add     esi, r13d         ; nontopOverlap++

Nếu bạn đã theo dõi cùng với các ví dụ trước, điều này sẽ rất quen thuộc với bạn. Cả hai so sánh được thực hiện theo cách không phân nhánh, các kết quả trung gian được kết andhợp với nhau và sau đó kết quả này (sẽ là 0 hoặc 1) được chỉnh addsửa nontopOverlap. Nếu bạn muốn mã không phân nhánh, điều này hầu như sẽ đảm bảo rằng bạn nhận được mã.

GCC 7 đã trở nên thông minh hơn. Bây giờ nó tạo mã gần như giống hệt nhau (ngoại trừ một số sắp xếp lại các hướng dẫn) cho thủ thuật trên như mã gốc. Vì vậy, câu trả lời cho câu hỏi của bạn, "Tại sao trình biên dịch lại hành xử theo cách này?" , có lẽ là vì chúng không hoàn hảo! Họ cố gắng sử dụng phương pháp phỏng đoán để tạo mã tối ưu nhất có thể, nhưng họ không luôn đưa ra quyết định tốt nhất. Nhưng ít nhất họ có thể trở nên thông minh hơn theo thời gian!

Một cách để xem xét tình huống này là mã phân nhánh có hiệu suất trường hợp tốt nhất . Nếu dự đoán chi nhánh thành công, bỏ qua các hoạt động không cần thiết sẽ dẫn đến thời gian chạy nhanh hơn một chút. Tuy nhiên, mã không phân nhánh có hiệu suất trường hợp xấu nhất tốt hơn . Nếu dự đoán chi nhánh thất bại, thực hiện một vài hướng dẫn bổ sung khi cần thiết để tránh một chi nhánh chắc chắn sẽ nhanh hơn một chi nhánh dự đoán sai. Ngay cả những trình biên dịch thông minh và thông minh nhất cũng sẽ gặp khó khăn khi đưa ra lựa chọn này.

Và đối với câu hỏi của bạn về việc liệu đây có phải là thứ mà các lập trình viên cần chú ý hay không, câu trả lời gần như chắc chắn là không, ngoại trừ trong các vòng lặp nóng nhất định mà bạn đang cố gắng tăng tốc thông qua tối ưu hóa vi mô. Sau đó, bạn ngồi xuống với việc tháo gỡ và tìm cách tinh chỉnh nó. Và, như tôi đã nói trước đây, hãy chuẩn bị xem xét lại các quyết định đó khi bạn cập nhật lên phiên bản mới hơn của trình biên dịch, bởi vì nó có thể làm điều gì đó ngu ngốc với mã khó hiểu của bạn, hoặc nó có thể thay đổi heuristic tối ưu hóa đủ để bạn có thể quay lại để sử dụng mã gốc của bạn. Nhận xét kỹ lưỡng!


3
Vâng, không có một "tốt hơn" phổ quát. Tất cả phụ thuộc vào tình huống của bạn, đó là lý do tại sao bạn hoàn toàn phải điểm chuẩn khi bạn thực hiện loại tối ưu hóa hiệu suất cấp thấp này. Như tôi đã giải thích trong câu trả lời, nếu bạn đang giảm kích thước dự đoán chi nhánh, các nhánh bị dự đoán sai sẽ làm chậm mã của bạn xuống rất nhiều . Đoạn mã cuối cùng không sử dụng bất kỳ nhánh nào (lưu ý không có j*hướng dẫn), vì vậy nó sẽ nhanh hơn trong trường hợp đó. [tiếp tục]
Cody Grey


2
@ 8bit Bob là đúng. Tôi đã đề cập đến hàng đợi prefetch. Tôi có lẽ không nên gọi nó là bộ đệm, nhưng không quá lo lắng về việc phrasing và đã không mất nhiều thời gian để cố nhớ lại các chi tiết cụ thể, vì tôi không nghĩ ai quan tâm nhiều ngoại trừ sự tò mò lịch sử. Nếu bạn muốn biết chi tiết, Zen of Association Language của Michael Abrash là vô giá. Toàn bộ cuốn sách có sẵn nhiều nơi trực tuyến; đây là phần áp dụng cho việc phân nhánh , nhưng bạn cũng nên đọc và hiểu các phần về tìm nạp trước.
Cody Grey

6
@Hurkyl Tôi cảm thấy như toàn bộ câu trả lời nói lên câu hỏi đó. Bạn nói đúng rằng tôi không thực sự gọi nó một cách rõ ràng, nhưng có vẻ như nó đã đủ dài rồi. :-) Bất cứ ai dành thời gian để đọc toàn bộ nội dung nên hiểu đủ về điểm đó. Nhưng nếu bạn nghĩ thiếu một cái gì đó, hoặc cần làm rõ hơn, xin đừng vội vàng trong việc chỉnh sửa câu trả lời để đưa nó vào. Một số người không thích điều này, nhưng tôi hoàn toàn không phiền. Tôi đã thêm một nhận xét ngắn gọn về điều này, cùng với việc sửa đổi từ ngữ của tôi theo đề xuất của 8bittree.
Cody Grey

2
Hah, cảm ơn vì sự bổ sung, @green. Tôi không có bất cứ điều gì cụ thể để đề nghị. Như với tất cả mọi thứ, bạn trở thành một chuyên gia bằng cách làm, nhìn và trải nghiệm. Tôi đã đọc tất cả mọi thứ mà tôi có thể có được khi nói đến kiến ​​trúc x86, tối ưu hóa, nội bộ trình biên dịch và các công cụ cấp thấp khác, và tôi vẫn chỉ biết một phần nhỏ của mọi thứ cần biết. Cách tốt nhất để học là lấy tay của bạn đào bới xung quanh. Nhưng trước khi bạn thậm chí có thể hy vọng bắt đầu, bạn sẽ cần nắm vững C (hoặc C ++), con trỏ, ngôn ngữ lắp ráp và tất cả các nguyên tắc cơ bản cấp thấp khác.
Cody Grey

23

Một điều quan trọng cần lưu ý là

(curr[i] < 479) && (l[i + shift] < 479)

(curr[i] < 479) * (l[i + shift] < 479)

không tương đương về mặt ngữ nghĩa! Đặc biệt, nếu bạn từng có tình huống:

  • 0 <= ii < curr.size()đều đúng
  • curr[i] < 479 là sai
  • i + shift < 0hoặc i + shift >= l.size()là sự thật

sau đó biểu thức (curr[i] < 479) && (l[i + shift] < 479)được đảm bảo là một giá trị boolean được xác định rõ. Ví dụ, nó không gây ra lỗi phân đoạn.

Tuy nhiên, trong những trường hợp này, biểu hiện (curr[i] < 479) * (l[i + shift] < 479)hành vi không xác định ; nó được phép gây ra một lỗi phân khúc.

Điều này có nghĩa là đối với đoạn mã gốc, ví dụ, trình biên dịch không thể chỉ viết một vòng lặp thực hiện cả so sánh và thực hiện một andthao tác, trừ khi trình biên dịch cũng có thể chứng minh rằng l[i + shift]sẽ không bao giờ gây ra một segfault trong tình huống mà nó không yêu cầu.

Nói tóm lại, đoạn mã gốc cung cấp ít cơ hội tối ưu hóa hơn đoạn mã sau. (tất nhiên, việc trình biên dịch có nhận ra cơ hội hay không là một câu hỏi hoàn toàn khác)

Thay vào đó, bạn có thể sửa phiên bản gốc

bool t1 = (curr[i] < 479);
bool t2 = (l[i + shift] < 479);
if (t1 && t2) {
    // ...

Điều này! Tùy thuộc vào giá trị của shift(và max) có UB ở đây ...
Matthieu M.

18

Các &&nhà điều hành thực hiện đánh giá ngắn mạch. Điều này có nghĩa là toán hạng thứ hai chỉ được ước tính nếu toán hạng thứ nhất ước tính true. Điều này chắc chắn dẫn đến một bước nhảy trong trường hợp đó.

Bạn có thể tạo một ví dụ nhỏ để hiển thị điều này:

#include <iostream>

bool f(int);
bool g(int);

void test(int x, int y)
{
  if ( f(x) && g(x)  )
  {
    std::cout << "ok";
  }
}

Đầu ra của trình biên dịch có thể được tìm thấy ở đây .

Bạn có thể thấy mã được tạo trước khi gọi f(x), sau đó kiểm tra đầu ra và chuyển sang đánh giá thời g(x)điểm này true. Nếu không thì nó rời khỏi chức năng.

Thay vào đó, sử dụng phép nhân "boolean" buộc phải đánh giá cả hai toán hạng và do đó không cần phải nhảy.

Tùy thuộc vào dữ liệu, bước nhảy có thể gây chậm lại vì nó làm xáo trộn đường ống của CPU và những thứ khác như thực thi đầu cơ. Thông thường dự đoán chi nhánh sẽ giúp, nhưng nếu dữ liệu của bạn là ngẫu nhiên thì không có nhiều dự đoán.


1
Tại sao bạn nói rằng phép nhân buộc phải đánh giá cả hai toán hạng mỗi lần? 0 * x = x * 0 = 0 bất kể giá trị của x. Khi tối ưu hóa, trình biên dịch cũng có thể "rút ngắn" phép nhân. Xem stackoverflow.com/questions/8145894/ , ví dụ. Hơn nữa, không giống như &&toán tử, phép nhân có thể được đánh giá lười biếng với đối số thứ nhất hoặc với đối số thứ hai, cho phép tự do hơn để tối ưu hóa.
Một số tên người dùng 6/12/2016

@Jens - "Thông thường dự đoán chi nhánh có ích, nhưng nếu dữ liệu của bạn là ngẫu nhiên thì không có nhiều dự đoán." - làm cho câu trả lời tốt.
SChepurin

1
@SomeWittyUsername Ok, trình biên dịch tất nhiên là miễn phí để thực hiện bất kỳ tối ưu hóa nào giữ cho hành vi có thể quan sát được. Điều này có thể hoặc không thể biến đổi nó và bỏ qua các tính toán. nếu bạn tính toán 0 * f()fcó hành vi quan sát được thì trình biên dịch phải gọi nó. Sự khác biệt là đánh giá ngắn mạch là bắt buộc &&nhưng được phép nếu nó có thể chỉ ra rằng nó tương đương với *.
Jens

@SomeWittyUsername chỉ trong trường hợp giá trị 0 có thể được dự đoán từ một biến hoặc hằng. Tôi đoán những trường hợp này là rất rất ít. Chắc chắn việc tối ưu hóa không thể được thực hiện trong trường hợp của OP, vì có liên quan đến truy cập mảng.
Diego Sevilla

3
@Jens: Đánh giá ngắn mạch là không bắt buộc. Mã chỉ được yêu cầu để hành xử như thể nó ngắn mạch; trình biên dịch được phép sử dụng bất kỳ phương tiện nào nó muốn để đạt được kết quả.

-2

Điều này có thể là do khi bạn đang sử dụng toán tử logic &&, trình biên dịch phải kiểm tra hai điều kiện để câu lệnh if thành công. Tuy nhiên, trong trường hợp thứ hai do bạn đang ngầm chuyển đổi một giá trị int thành bool, trình biên dịch đưa ra một số giả định dựa trên các loại và giá trị được truyền vào, cùng với (có thể) một điều kiện nhảy đơn. Cũng có thể trình biên dịch tối ưu hóa hoàn toàn jmps với dịch chuyển bit.


8
Bước nhảy xuất phát từ thực tế là điều kiện thứ hai được đánh giá khi và chỉ khi điều kiện thứ nhất là đúng. Mã không được đánh giá nó theo cách khác, do đó trình biên dịch không thể tối ưu hóa điều này tốt hơn và vẫn đúng (trừ khi nó có thể suy ra câu lệnh đầu tiên sẽ luôn luôn đúng).
rubenvb
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.