Tại sao vòng lặp này tạo ra cảnh báo trên mạng: lặp đi lặp lại 3u gọi hành vi không xác định, và xuất ra hơn 4 dòng?


162

Biên dịch này:

#include <iostream>

int main()
{
    for (int i = 0; i < 4; ++i)
        std::cout << i*1000000000 << std::endl;
}

và đưa gccra cảnh báo sau:

warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations]
   std::cout << i*1000000000 << std::endl;
                  ^

Tôi hiểu có một tràn số nguyên đã ký.

Những gì tôi không thể nhận được là tại sao igiá trị bị phá vỡ bởi hoạt động tràn đó?

Tôi đã đọc câu trả lời cho Tại sao số nguyên tràn trên x86 với GCC gây ra một vòng lặp vô hạn? , nhưng tôi vẫn không rõ tại sao điều này xảy ra - tôi hiểu rằng "không xác định" có nghĩa là "bất cứ điều gì có thể xảy ra", nhưng nguyên nhân cơ bản của hành vi cụ thể này là gì?

Trực tuyến: http://ideone.com/dMrRKR

Trình biên dịch: gcc (4.8)


49
Đã tràn số nguyên => Hành vi không xác định => Daemon mũi. Nhưng tôi phải thừa nhận, ví dụ đó khá hay.
dyp

1
Đầu ra hội: goo.gl/TtPmZn
Bryan Chen

1
Xảy ra trên GCC 4.8 với O2, và O3cờ, nhưng không O0hoặcO1
Alex

3
@dyp khi tôi đọc Da mũi, tôi đã làm "tiếng cười imgur" bao gồm hơi thở ra khi bạn thấy điều gì đó buồn cười. Và rồi tôi nhận ra ... Tôi phải bị một Daemon nguyền rủa!
corsiKa

4
Đánh dấu trang này để tôi có thể liên kết nó bắt bẻ, một người nào đó lần sau "đó là kỹ thuật UB nhưng nó phải làm điều " :)
MM

Câu trả lời:


107

Tràn số nguyên đã ký (như nói đúng ra, không có thứ gọi là "tràn số nguyên không dấu") có nghĩa là hành vi không xác định . Và điều này có nghĩa là bất cứ điều gì cũng có thể xảy ra và thảo luận về lý do tại sao nó xảy ra theo các quy tắc của C ++ không có ý nghĩa.

Dự thảo C ++ 11 N3337: §5.4: 1

Nếu trong quá trình đánh giá một biểu thức, kết quả không được định nghĩa về mặt toán học hoặc không nằm trong phạm vi của các giá trị đại diện cho loại của nó, thì hành vi đó không được xác định. [Lưu ý: hầu hết các triển khai C ++ hiện có đều bỏ qua số nguyên trên ows ows. Xử lý phép chia bằng 0, tạo thành phần dư bằng cách sử dụng ước số 0 và tất cả các ngoại lệ điểm khác nhau giữa các máy và thường được điều chỉnh bởi chức năng thư viện. Lưu ý

Mã của bạn được biên dịch với g++ -O3cảnh báo phát ra (thậm chí không có -Wall)

a.cpp: In function 'int main()':
a.cpp:11:18: warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations]
   std::cout << i*1000000000 << std::endl;
                  ^
a.cpp:9:2: note: containing loop
  for (int i = 0; i < 4; ++i)
  ^

Cách duy nhất chúng ta có thể phân tích những gì chương trình đang làm, là đọc mã lắp ráp được tạo.

Dưới đây là danh sách lắp ráp đầy đủ:

    .file   "a.cpp"
    .section    .text$_ZNKSt5ctypeIcE8do_widenEc,"x"
    .linkonce discard
    .align 2
LCOLDB0:
LHOTB0:
    .align 2
    .p2align 4,,15
    .globl  __ZNKSt5ctypeIcE8do_widenEc
    .def    __ZNKSt5ctypeIcE8do_widenEc;    .scl    2;  .type   32; .endef
__ZNKSt5ctypeIcE8do_widenEc:
LFB860:
    .cfi_startproc
    movzbl  4(%esp), %eax
    ret $4
    .cfi_endproc
LFE860:
LCOLDE0:
LHOTE0:
    .section    .text.unlikely,"x"
LCOLDB1:
    .text
LHOTB1:
    .p2align 4,,15
    .def    ___tcf_0;   .scl    3;  .type   32; .endef
___tcf_0:
LFB1091:
    .cfi_startproc
    movl    $__ZStL8__ioinit, %ecx
    jmp __ZNSt8ios_base4InitD1Ev
    .cfi_endproc
LFE1091:
    .section    .text.unlikely,"x"
LCOLDE1:
    .text
LHOTE1:
    .def    ___main;    .scl    2;  .type   32; .endef
    .section    .text.unlikely,"x"
LCOLDB2:
    .section    .text.startup,"x"
LHOTB2:
    .p2align 4,,15
    .globl  _main
    .def    _main;  .scl    2;  .type   32; .endef
_main:
LFB1084:
    .cfi_startproc
    leal    4(%esp), %ecx
    .cfi_def_cfa 1, 0
    andl    $-16, %esp
    pushl   -4(%ecx)
    pushl   %ebp
    .cfi_escape 0x10,0x5,0x2,0x75,0
    movl    %esp, %ebp
    pushl   %edi
    pushl   %esi
    pushl   %ebx
    pushl   %ecx
    .cfi_escape 0xf,0x3,0x75,0x70,0x6
    .cfi_escape 0x10,0x7,0x2,0x75,0x7c
    .cfi_escape 0x10,0x6,0x2,0x75,0x78
    .cfi_escape 0x10,0x3,0x2,0x75,0x74
    xorl    %edi, %edi
    subl    $24, %esp
    call    ___main
L4:
    movl    %edi, (%esp)
    movl    $__ZSt4cout, %ecx
    call    __ZNSolsEi
    movl    %eax, %esi
    movl    (%eax), %eax
    subl    $4, %esp
    movl    -12(%eax), %eax
    movl    124(%esi,%eax), %ebx
    testl   %ebx, %ebx
    je  L15
    cmpb    $0, 28(%ebx)
    je  L5
    movsbl  39(%ebx), %eax
L6:
    movl    %esi, %ecx
    movl    %eax, (%esp)
    addl    $1000000000, %edi
    call    __ZNSo3putEc
    subl    $4, %esp
    movl    %eax, %ecx
    call    __ZNSo5flushEv
    jmp L4
    .p2align 4,,10
L5:
    movl    %ebx, %ecx
    call    __ZNKSt5ctypeIcE13_M_widen_initEv
    movl    (%ebx), %eax
    movl    24(%eax), %edx
    movl    $10, %eax
    cmpl    $__ZNKSt5ctypeIcE8do_widenEc, %edx
    je  L6
    movl    $10, (%esp)
    movl    %ebx, %ecx
    call    *%edx
    movsbl  %al, %eax
    pushl   %edx
    jmp L6
L15:
    call    __ZSt16__throw_bad_castv
    .cfi_endproc
LFE1084:
    .section    .text.unlikely,"x"
LCOLDE2:
    .section    .text.startup,"x"
LHOTE2:
    .section    .text.unlikely,"x"
LCOLDB3:
    .section    .text.startup,"x"
LHOTB3:
    .p2align 4,,15
    .def    __GLOBAL__sub_I_main;   .scl    3;  .type   32; .endef
__GLOBAL__sub_I_main:
LFB1092:
    .cfi_startproc
    subl    $28, %esp
    .cfi_def_cfa_offset 32
    movl    $__ZStL8__ioinit, %ecx
    call    __ZNSt8ios_base4InitC1Ev
    movl    $___tcf_0, (%esp)
    call    _atexit
    addl    $28, %esp
    .cfi_def_cfa_offset 4
    ret
    .cfi_endproc
LFE1092:
    .section    .text.unlikely,"x"
LCOLDE3:
    .section    .text.startup,"x"
LHOTE3:
    .section    .ctors,"w"
    .align 4
    .long   __GLOBAL__sub_I_main
.lcomm __ZStL8__ioinit,1,1
    .ident  "GCC: (i686-posix-dwarf-rev1, Built by MinGW-W64 project) 4.9.0"
    .def    __ZNSt8ios_base4InitD1Ev;   .scl    2;  .type   32; .endef
    .def    __ZNSolsEi; .scl    2;  .type   32; .endef
    .def    __ZNSo3putEc;   .scl    2;  .type   32; .endef
    .def    __ZNSo5flushEv; .scl    2;  .type   32; .endef
    .def    __ZNKSt5ctypeIcE13_M_widen_initEv;  .scl    2;  .type   32; .endef
    .def    __ZSt16__throw_bad_castv;   .scl    2;  .type   32; .endef
    .def    __ZNSt8ios_base4InitC1Ev;   .scl    2;  .type   32; .endef
    .def    _atexit;    .scl    2;  .type   32; .endef

Tôi hầu như không thể đọc lắp ráp, nhưng thậm chí tôi có thể nhìn thấy addl $1000000000, %edidòng. Mã kết quả trông giống như

for(int i = 0; /* nothing, that is - infinite loop */; i += 1000000000)
    std::cout << i << std::endl;

Nhận xét này của @TC:

Tôi nghi ngờ rằng đó là một cái gì đó như: (1) bởi vì mỗi lần lặp với ibất kỳ giá trị nào lớn hơn 2 đều có hành vi không xác định -> (2) chúng ta có thể giả sử rằng i <= 2vì mục đích tối ưu hóa -> (3) điều kiện vòng lặp luôn luôn đúng -> (4 ) nó được tối ưu hóa thành một vòng lặp vô hạn.

đã cho tôi ý tưởng để so sánh mã lắp ráp của mã OP với mã lắp ráp của mã sau đây, không có hành vi không xác định.

#include <iostream>

int main()
{
    // changed the termination condition
    for (int i = 0; i < 3; ++i)
        std::cout << i*1000000000 << std::endl;
}

Và, trên thực tế, mã chính xác có điều kiện chấm dứt.

    ; ...snip...
L6:
    mov ecx, edi
    mov DWORD PTR [esp], eax
    add esi, 1000000000
    call    __ZNSo3putEc
    sub esp, 4
    mov ecx, eax
    call    __ZNSo5flushEv
    cmp esi, -1294967296 // here it is
    jne L7
    lea esp, [ebp-16]
    xor eax, eax
    pop ecx
    ; ...snip...

OMG, điều đó hoàn toàn không rõ ràng! Thật không công bằng! Tôi yêu cầu thử nghiệm bằng lửa!

Đối phó với nó, bạn đã viết mã lỗi và bạn sẽ cảm thấy tồi tệ. Chịu hậu quả.

... Hoặc, thay vào đó, sử dụng hợp lý các chẩn đoán tốt hơn và các công cụ sửa lỗi tốt hơn - đó là những gì họ dành cho:

  • cho phép tất cả các cảnh báo

    • -Walllà tùy chọn gcc cho phép tất cả các cảnh báo hữu ích không có dương tính giả. Đây là mức tối thiểu mà bạn nên luôn luôn sử dụng.
    • gcc có nhiều tùy chọn cảnh báo khác , tuy nhiên, chúng không được bật -Wallvì chúng có thể cảnh báo về các kết quả dương tính giả
    • Thật không may, Visual C ++ bị tụt lại phía sau với khả năng đưa ra những cảnh báo hữu ích. Ít nhất IDE cho phép một số theo mặc định.
  • sử dụng cờ gỡ lỗi để gỡ lỗi

    • đối với tràn số nguyên -ftrapvbẫy chương trình trên tràn,
    • Trình biên dịch Clang là tuyệt vời cho việc này: -fcatch-undefined-behaviornắm bắt rất nhiều trường hợp hành vi không xác định (lưu ý "a lot of" != "all of them":)

Tôi có một mớ hỗn độn của một chương trình không phải do tôi viết mà cần phải chuyển đi vào ngày mai! GIÚP !!!!!!

Sử dụng gcc -fwrapv

Tùy chọn này hướng dẫn trình biên dịch giả định rằng tràn số học đã ký của phép cộng, phép trừ và phép nhân kết thúc bằng cách sử dụng biểu diễn bổ sung twos.

1 - quy tắc này không áp dụng cho "tràn số nguyên không dấu", như §3.9.1.4 nói rằng

Các số nguyên không dấu, được khai báo không dấu, sẽ tuân theo các định luật modulo số học 2 n trong đó n là số bit trong biểu diễn giá trị của kích thước cụ thể đó của số nguyên.

và ví dụ kết quả của UINT_MAX + 1được xác định theo toán học - theo quy tắc modulo số học 2 n


7
Tôi vẫn không thực sự hiểu những gì đang xảy ra ở đây ... Tại sao inó lại bị ảnh hưởng? Nói chung, hành vi không xác định là không có các loại tác dụng phụ kỳ lạ này, i*100000000
rốt cuộc

26
Tôi nghi ngờ rằng đó là một cái gì đó như: (1) bởi vì mỗi lần lặp với ibất kỳ giá trị nào lớn hơn 2 đều có hành vi không xác định -> (2) chúng ta có thể giả sử rằng i <= 2vì mục đích tối ưu hóa -> (3) điều kiện vòng lặp luôn luôn đúng -> (4 ) nó được tối ưu hóa thành một vòng lặp vô hạn.
TC

28
@vsoftco: Điều gì đang xảy ra là một trường hợp giảm sức mạnh , cụ thể hơn là loại bỏ biến cảm ứng . Trình biên dịch loại bỏ phép nhân bằng cách phát ra mã thay vào đó tăng thêm i1e9 mỗi lần lặp (và thay đổi điều kiện vòng lặp cho phù hợp). Đây là một tối ưu hóa hoàn toàn hợp lệ theo quy tắc "như thể" vì chương trình này không thể quan sát được sự khác biệt là nó hoạt động tốt. Than ôi, không phải, và tối ưu hóa "rò rỉ".
JohannesD

8
@JohannesD đóng đinh lý do này phá vỡ. Tuy nhiên, đây là một tối ưu hóa xấu vì điều kiện chấm dứt vòng lặp không liên quan đến tràn. Việc sử dụng giảm sức mạnh là ổn - Tôi không biết hệ số nhân trong bộ xử lý sẽ làm gì với (4 * 100000000) sẽ khác với (100000000 + 100000000 + 100000000 + 100000000) và không quay lại "không xác định được - ai biết "là hợp lý. Nhưng thay thế những gì nên là một vòng lặp hoạt động tốt, thực hiện 4 lần và tạo ra kết quả không xác định, bằng một cái gì đó thực thi hơn 4 lần "bởi vì nó không được xác định!" là ngu ngốc.
Julie ở Austin

14
@JulieinAustin Mặc dù nó có thể là ngu ngốc đối với bạn, nhưng nó hoàn toàn hợp pháp. Về mặt tích cực, trình biên dịch cảnh báo bạn về nó.
milleniumorms

68

Câu trả lời ngắn, gcccụ thể đã ghi lại vấn đề này, chúng ta có thể thấy rằng trong ghi chú phát hành gcc 4.8 có ghi ( nhấn mạnh tôi sẽ tiếp tục ):

GCC hiện sử dụng phân tích tích cực hơn để rút ra giới hạn trên cho số lần lặp của các vòng lặp sử dụng các ràng buộc áp đặt theo tiêu chuẩn ngôn ngữ . Điều này có thể khiến các chương trình không tuân thủ không còn hoạt động như mong đợi, chẳng hạn như CPU ​​CPU 2006 464.h264ref và 416.gamess. Một tùy chọn mới, tối ưu hóa -fno-hung hăng-vòng lặp, đã được thêm vào để vô hiệu hóa phân tích tích cực này. Trong một số vòng lặp có số lần lặp không đổi đã biết, nhưng hành vi không xác định được biết là xảy ra trong vòng lặp trước khi đến hoặc trong lần lặp cuối cùng, GCC sẽ cảnh báo về hành vi không xác định trong vòng lặp thay vì xuất phát giới hạn dưới của số lần lặp cho vòng lặp. Cảnh báo có thể bị vô hiệu hóa với tối ưu hóa -Wno-hung hăng-vòng lặp.

và thực sự nếu chúng ta sử dụng -fno-aggressive-loop-optimizationshành vi vòng lặp vô hạn sẽ chấm dứt và nó thực hiện trong tất cả các trường hợp tôi đã thử nghiệm.

Câu trả lời dài bắt đầu bằng việc biết rằng tràn số nguyên đã ký là hành vi không xác định bằng cách xem dự thảo phần tiêu chuẩn C ++ 5 Biểu thức đoạn 4 có nội dung:

Nếu trong quá trình đánh giá biểu thức, kết quả không được xác định theo toán học hoặc không nằm trong phạm vi giá trị đại diện cho loại của nó, hành vi không được xác định . [Lưu ý: hầu hết các triển khai C ++ hiện có đều bỏ qua tràn số nguyên. Xử lý phép chia bằng 0, tạo thành phần dư bằng cách sử dụng ước số 0 và tất cả các ngoại lệ dấu phẩy động khác nhau giữa các máy và thường được điều chỉnh bởi chức năng thư viện. Lưu ý

Chúng tôi biết rằng tiêu chuẩn nói rằng hành vi không xác định là không thể đoán trước được từ ghi chú đi kèm với định nghĩa có nội dung:

[Lưu ý: Hành vi không xác định có thể được mong đợi khi Tiêu chuẩn quốc tế này bỏ qua mọi định nghĩa rõ ràng về hành vi hoặc khi chương trình sử dụng cấu trúc sai hoặc dữ liệu sai. Hành vi không xác định cho phép bao gồm từ bỏ qua hoàn toàn tình huống với kết quả không thể đoán trước , đến hành vi trong quá trình dịch thuật hoặc thực hiện chương trình theo đặc tính của môi trường (có hoặc không có thông báo chẩn đoán), để chấm dứt dịch hoặc thực thi (với việc ban hành của một thông điệp chẩn đoán). Nhiều cấu trúc chương trình sai lầm không gây ra hành vi không xác định; họ được yêu cầu phải được chẩn đoán. Lưu ý

Nhưng những gì trên thế giới gcctối ưu hóa có thể làm để biến điều này thành một vòng lặp vô hạn? Nghe có vẻ hoàn toàn lập dị. Nhưng rất may gcccho chúng tôi một manh mối để tìm ra nó trong cảnh báo:

warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations]
   std::cout << i*1000000000 << std::endl;
                  ^

Manh mối là Waggressive-loop-optimizations, điều đó có nghĩa là gì? May mắn cho chúng tôi, đây không phải là lần đầu tiên tối ưu hóa này bị hỏng mã theo cách này và chúng tôi rất may mắn vì John Regehr đã ghi lại một trường hợp trong bài viết GCC pre-4.8 Breaks Broken Spec 2006 Điểm chuẩn hiển thị mã sau:

int d[16];

int SATD (void)
{
  int satd = 0, dd, k;
  for (dd=d[k=0]; k<16; dd=d[++k]) {
    satd += (dd < 0 ? -dd : dd);
  }
  return satd;
}

bài báo nói:

Hành vi không xác định đang truy cập d [16] ngay trước khi thoát khỏi vòng lặp. Trong C99, việc tạo một con trỏ tới một phần tử một vị trí qua cuối mảng là hợp pháp, nhưng con trỏ đó không được hủy đăng ký.

và sau này nói:

Cụ thể, đây là những gì đang xảy ra. Trình biên dịch AC, khi thấy d [++ k], được phép giả định rằng giá trị tăng của k nằm trong giới hạn mảng, vì nếu không có hành vi không xác định xảy ra. Đối với mã ở đây, GCC có thể suy ra k nằm trong phạm vi 0..15. Một lát sau, khi GCC thấy k <16, nó tự nói với chính mình: thì Aha, biểu hiện đó luôn luôn đúng, vì vậy chúng ta có một vòng lặp vô hạn. Tình huống ở đây, nơi trình biên dịch sử dụng giả định về tính xác định rõ để suy ra một thực tế dữ liệu hữu ích,

Vì vậy, những gì trình biên dịch phải làm trong một số trường hợp là giả sử vì tràn số nguyên đã ký là hành vi không xác định thì iphải luôn nhỏ hơn 4và do đó chúng ta có một vòng lặp vô hạn.

Ông giải thích điều này rất giống với việc loại bỏ kiểm tra con trỏ null nhân Linux khét tiếng khi nhìn thấy mã này:

struct foo *s = ...;
int x = s->f;
if (!s) return ERROR;

gccsuy ra rằng vì sđã được hoãn lại s->f;và kể từ khi hủy bỏ một con trỏ null là hành vi không xác định thì skhông được rỗng và do đó tối ưu hóa việc if (!s)kiểm tra trên dòng tiếp theo.

Bài học ở đây là các trình tối ưu hóa hiện đại rất tích cực trong việc khai thác hành vi không xác định và rất có thể sẽ chỉ trở nên hung hăng hơn. Rõ ràng chỉ với một vài ví dụ, chúng ta có thể thấy trình tối ưu hóa thực hiện những điều dường như hoàn toàn không hợp lý với một lập trình viên nhưng nhìn lại từ quan điểm tối ưu hóa có ý nghĩa.


7
Tôi hiểu rằng đây là những gì người viết trình biên dịch đang làm (tôi đã từng viết trình biên dịch và thậm chí là trình tối ưu hóa hoặc hai), nhưng có những hành vi "hữu ích" mặc dù chúng "không xác định", và điều này tiến tới tối ưu hóa mạnh mẽ hơn bao giờ hết chỉ là sự điên rồ. Cấu trúc bạn trích dẫn ở trên là sai, nhưng tối ưu hóa kiểm tra lỗi là do người dùng thù địch.
Julie ở Austin

1
@JulieinAustin Tôi đồng ý đây là hành vi khá đáng ngạc nhiên, nói rằng các nhà phát triển cần tránh hành vi không xác định thực sự chỉ là một nửa vấn đề. Rõ ràng trình biên dịch cũng cần cung cấp phản hồi tốt hơn cho nhà phát triển. Trong trường hợp này, một cảnh báo được đưa ra mặc dù nó không thực sự đủ thông tin.
Shafik Yaghmour

3
Tôi nghĩ đó là một điều tốt, tôi muốn mã tốt hơn, nhanh hơn. Không bao giờ nên sử dụng UB.
paulm

1
@paulm về mặt đạo đức UB rõ ràng là xấu nhưng thật khó để tranh luận với việc cung cấp các công cụ tốt hơn như phân tích tĩnh clang để giúp các nhà phát triển nắm bắt được UB và các vấn đề khác trước khi nó ảnh hưởng đến các ứng dụng sản xuất.
Shafik Yaghmour

1
@ShafikYaghmour Ngoài ra, nếu nhà phát triển của bạn bỏ qua các cảnh báo, cơ hội họ sẽ chú ý đến đầu ra clang là gì? Vấn đề này có thể dễ dàng bị bắt bởi một chính sách "không cảnh báo bất công" tích cực. Clang khuyên nhưng không bắt buộc.
deworde

24

tl; dr Mã tạo ra một bài kiểm tra rằng số nguyên + số nguyên dương == số nguyên âm . Thông thường trình tối ưu hóa không tối ưu hóa điều này, nhưng trong trường hợp cụ thể std::endlđược sử dụng tiếp theo, trình biên dịch sẽ tối ưu hóa thử nghiệm này. Tôi chưa tìm ra điều gì đặc biệt endl.


Từ mã lắp ráp ở mức -O1 trở lên, rõ ràng gcc sẽ tái cấu trúc vòng lặp thành:

i = 0;
do {
    cout << i << endl;
    i += NUMBER;
} 
while (i != NUMBER * 4)

Giá trị lớn nhất hoạt động chính xác là 715827882, ví dụ sàn ( INT_MAX/3). Đoạn mã lắp ráp tại -O1là:

L4:
movsbl  %al, %eax
movl    %eax, 4(%esp)
movl    $__ZSt4cout, (%esp)
call    __ZNSo3putEc
movl    %eax, (%esp)
call    __ZNSo5flushEv
addl    $715827882, %esi
cmpl    $-1431655768, %esi
jne L6
    // fallthrough to "return" code

Lưu ý, -14316557684 * 715827882trong bổ sung của 2.

Đánh -O2tối ưu hóa điều đó như sau:

L4:
movsbl  %al, %eax
addl    $715827882, %esi
movl    %eax, 4(%esp)
movl    $__ZSt4cout, (%esp)
call    __ZNSo3putEc
movl    %eax, (%esp)
call    __ZNSo5flushEv
cmpl    $-1431655768, %esi
jne L6
leal    -8(%ebp), %esp
jne L6 
   // fallthrough to "return" code

Vì vậy, việc tối ưu hóa đã được thực hiện chỉ đơn thuần là việc addlđược chuyển lên cao hơn.

Nếu chúng tôi biên dịch lại 715827883thay vào đó thì phiên bản -O1 giống hệt với số lượng và giá trị thử nghiệm đã thay đổi. Tuy nhiên, -O2 sau đó thực hiện thay đổi:

L4:
movsbl  %al, %eax
addl    $715827883, %esi
movl    %eax, 4(%esp)
movl    $__ZSt4cout, (%esp)
call    __ZNSo3putEc
movl    %eax, (%esp)
call    __ZNSo5flushEv
jmp L2

Trong trường hợp có là cmpl $-1431655764, %esitại -O1, dòng đó đã bị xóa do -O2. Trình tối ưu hóa phải quyết định rằng việc thêm 715827883vào %esikhông bao giờ có thể bằng -1431655764.

Điều này là khá khó hiểu. Thêm vào đó để INT_MIN+1 không tạo ra các kết quả mong đợi, vì vậy tôi ưu hoa phải đã quyết định rằng %esikhông bao giờ có thể INT_MIN+1và tôi không chắc chắn lý do tại sao nó sẽ quyết định đó.

Trong ví dụ hoạt động, có vẻ như nó có giá trị như nhau để kết luận rằng việc thêm 715827882vào một số không thể bằng INT_MIN + 715827882 - 2! (điều này chỉ có thể xảy ra nếu tình trạng quay vòng thực sự xảy ra), nhưng nó không tối ưu hóa dòng trong ví dụ đó.


Mã tôi đang sử dụng là:

#include <iostream>
#include <cstdio>

int main()
{
    for (int i = 0; i < 4; ++i)
    {
        //volatile int j = i*715827883;
        volatile int j = i*715827882;
        printf("%d\n", j);

        std::endl(std::cout);
    }
}

Nếu std::endl(std::cout)được loại bỏ thì tối ưu hóa không còn xảy ra. Trong thực tế, việc thay thế nó std::cout.put('\n'); std::flush(std::cout);cũng khiến việc tối ưu hóa không xảy ra, mặc dù đã std::endlđược nội tuyến.

Nội tuyến std::endldường như ảnh hưởng đến phần trước của cấu trúc vòng lặp (điều mà tôi không hiểu lắm về những gì nó đang làm nhưng tôi sẽ đăng nó ở đây trong trường hợp người khác làm):

Với mã gốc và -O2:

L2:
movl    %esi, 28(%esp)
movl    28(%esp), %eax
movl    $LC0, (%esp)
movl    %eax, 4(%esp)
call    _printf
movl    __ZSt4cout, %eax
movl    -12(%eax), %eax
movl    __ZSt4cout+124(%eax), %ebx
testl   %ebx, %ebx
je  L10
cmpb    $0, 28(%ebx)
je  L3
movzbl  39(%ebx), %eax
L4:
movsbl  %al, %eax
addl    $715827883, %esi
movl    %eax, 4(%esp)
movl    $__ZSt4cout, (%esp)
call    __ZNSo3putEc
movl    %eax, (%esp)
call    __ZNSo5flushEv
jmp L2                  // no test

Với nội tuyến của tôi std::endl, -O2:

L3:
movl    %ebx, 28(%esp)
movl    28(%esp), %eax
addl    $715827883, %ebx
movl    $LC0, (%esp)
movl    %eax, 4(%esp)
call    _printf
movl    $10, 4(%esp)
movl    $__ZSt4cout, (%esp)
call    __ZNSo3putEc
movl    $__ZSt4cout, (%esp)
call    __ZNSo5flushEv
cmpl    $-1431655764, %ebx
jne L3
xorl    %eax, %eax

Một sự khác biệt giữa hai cái %esinày là được sử dụng trong bản gốc và %ebxtrong phiên bản thứ hai; Có sự khác biệt nào về ngữ nghĩa được định nghĩa giữa %esi%ebxnói chung không? (Tôi không biết nhiều về lắp ráp x86).


Thật tốt khi tìm hiểu chính xác logic của trình tối ưu hóa là gì, tôi không rõ ở giai đoạn này tại sao một số trường hợp thử nghiệm được tối ưu hóa và một số thì không
MM

8

Một ví dụ khác về lỗi này được báo cáo trong gcc là khi bạn có một vòng lặp thực thi với số lần lặp không đổi, nhưng bạn đang sử dụng biến đếm như một chỉ mục thành một mảng có ít hơn số mục đó, chẳng hạn như:

int a[50], x;

for( i=0; i < 1000; i++) x = a[i];

Trình biên dịch có thể xác định rằng vòng lặp này sẽ cố gắng truy cập bộ nhớ bên ngoài mảng 'a'. Trình biên dịch phàn nàn về điều này với thông điệp khá khó hiểu này:

lặp xxu gọi hành vi không xác định [-Werror = hung hăng-loop-tối ưu hóa]


Thậm chí khó hiểu hơn là thông điệp chỉ phát ra khi tối ưu hóa được bật. Thông điệp M $ VB "Mảng ngoài giới hạn" là dành cho người giả?
Ravi Ganesh

6

Những gì tôi không thể nhận được là tại sao giá trị tôi bị phá vỡ bởi hoạt động tràn đó?

Có vẻ như tràn số nguyên xảy ra trong lần lặp thứ 4 (for i = 3). signedtràn số nguyên gọi hành vi không xác định . Trong trường hợp này không có gì có thể dự đoán. Vòng lặp có thể lặp lại chỉ một 4lần hoặc nó có thể đi đến vô tận hoặc bất cứ điều gì khác!
Kết quả có thể thay đổi trình biên dịch sang trình biên dịch hoặc thậm chí cho các phiên bản khác nhau của cùng một trình biên dịch.

C11: 1.3.24 hành vi không xác định:

hành vi mà Tiêu chuẩn quốc tế này áp đặt không có yêu cầu
[Lưu ý: Hành vi không xác định có thể được mong đợi khi Tiêu chuẩn quốc tế này bỏ qua bất kỳ định nghĩa rõ ràng nào về hành vi hoặc khi chương trình sử dụng cấu trúc sai hoặc dữ liệu sai. Hành vi không xác định cho phép bao gồm từ bỏ qua hoàn toàn tình huống với kết quả không thể đoán trước, đến hành vi trong quá trình dịch thuật hoặc thực hiện chương trình theo đặc tính tài liệu của môi trường (có hoặc không có thông báo chẩn đoán), để chấm dứt dịch thuật hoặc thực thi (với việc ban hành của một thông điệp chẩn đoán) . Nhiều cấu trúc chương trình sai lầm không gây ra hành vi không xác định; họ được yêu cầu phải được chẩn đoán. Lưu ý


@bits_i quốc tế; Đúng.
haccks

4
Bạn nói đúng, thật công bằng khi giải thích lý do tại sao tôi đánh giá thấp. Thông tin trong câu trả lời này là chính xác, nhưng nó không mang tính giáo dục và nó hoàn toàn bỏ qua con voi trong phòng: sự cố vỡ rõ ràng xảy ra ở một nơi khác (điều kiện dừng) so với thao tác gây ra tràn. Các cơ chế về cách mọi thứ bị phá vỡ trong trường hợp cụ thể này không được giải thích, mặc dù đây là cốt lõi của câu hỏi này. Đó là một tình huống giáo viên tồi điển hình trong đó câu trả lời của giáo viên không chỉ không giải quyết được cốt lõi của vấn đề, nó không khuyến khích các câu hỏi tiếp theo. Nó gần giống như ...
Szabolcs

5
"Tôi thấy đây là hành vi không xác định và từ thời điểm này tôi không quan tâm làm thế nào hoặc tại sao nó phá vỡ. Tiêu chuẩn cho phép nó phá vỡ. Không có câu hỏi nào nữa." Bạn có thể không có ý như vậy, nhưng có vẻ như vậy. Tôi hy vọng sẽ thấy ít hơn về thái độ này (không may phổ biến) trên SO. Điều này thực tế không hữu ích. Nếu bạn nhận được đầu vào của người dùng, sẽ không hợp lý khi kiểm tra tràn sau mỗi hoạt động số nguyên đã ký , ngay cả khi tiêu chuẩn nói rằng bất kỳ phần nào khác của chương trình có thể nổ tung vì nó. Hiểu cách nó phá vỡ sẽ giúp tránh các vấn đề như thế này trong thực tế.
Szabolcs

2
@Szabolcs: Có thể tốt nhất khi nghĩ về C là hai ngôn ngữ, một trong số đó được thiết kế để cho phép các trình biên dịch đơn giản đạt được mã thực thi hợp lý với sự trợ giúp của các lập trình viên khai thác các cấu trúc có thể tin cậy trên nền tảng đích của họ nhưng không các ngôn ngữ khác và do đó bị ủy ban Tiêu chuẩn bỏ qua và một ngôn ngữ thứ hai loại trừ tất cả các cấu trúc như vậy mà Tiêu chuẩn không bắt buộc hỗ trợ, với mục đích cho phép các trình biên dịch áp dụng các tối ưu hóa bổ sung có thể hoặc không thể vượt trội hơn các lập trình viên phải bỏ cuộc.
supercat

1
@Szabolcs " Nếu bạn nhận được đầu vào của người dùng, sẽ không hợp lý khi kiểm tra tràn sau mỗi hoạt động số nguyên đã ký " - chính xác vì tại thời điểm đó đã quá muộn. Bạn phải kiểm tra tràn trước mỗi thao tác số nguyên đã ký.
melpomene
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.