Mengapa loop ini menghasilkan "peringatan: iterasi 3u memohon perilaku yang tidak terdefinisi" dan menghasilkan lebih dari 4 baris?

162

Kompilasi ini:

#include <iostream>

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

dan gccmenghasilkan peringatan berikut:

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

Saya mengerti ada bilangan bulat ditandatangani ditandatangani.

Yang tidak bisa saya dapatkan adalah mengapa inilai rusak oleh operasi luapan itu?

Saya telah membaca jawaban untuk Mengapa integer overflow pada x86 dengan GCC menyebabkan loop tak terbatas? , tapi saya masih belum jelas mengapa ini terjadi - saya mendapatkan "tidak terdefinisi" berarti "apa pun bisa terjadi", tetapi apa penyebab mendasar dari perilaku spesifik ini ?

Online: http://ideone.com/dMrRKR

Penyusun: gcc (4.8)

zerkms
sumber
49
Overflow integer yang ditandatangani => Perilaku Tidak Terdefinisi => Daemon Nasal. Tapi saya harus akui, contoh itu cukup bagus.
dyp
1
Output perakitan: goo.gl/TtPmZn
Bryan Chen
1
Terjadi pada GCC 4.8 dengan O2, dan O3bendera, tetapi tidak O0atauO1
Alex
3
@dyp ketika saya membaca Nasal Daemons saya melakukan "imgur laugh" yang terdiri dari sedikit mengembuskan hidung ketika melihat sesuatu yang lucu. Dan kemudian saya menyadari ... Saya harus dikutuk oleh Daemon Nasal!
corsiKa
4
Bookmarking ini sehingga saya dapat menautkannya di lain waktu seseorang membalas "itu secara teknis UB tetapi harus melakukan sesuatu " :)
MM

Jawaban:

107

Signed integer overflow (secara tegas, tidak ada yang namanya "unsigned integer overflow") berarti perilaku tidak terdefinisi . Dan ini berarti apa pun bisa terjadi, dan mendiskusikan mengapa itu terjadi di bawah aturan C ++ tidak masuk akal.

C ++ 11 draft N3337: §5.4: 1

Jika selama evaluasi suatu ekspresi, hasilnya tidak didefinisikan secara matematis atau tidak dalam kisaran nilai yang dapat diwakili untuk jenisnya, perilaku tersebut tidak ditentukan. [Catatan: sebagian besar implementasi C ++ mengabaikan aliran bilangan bulat. Perawatan pembagian dengan nol, membentuk sisa menggunakan pembagi nol, dan semua pengecualian titik mengambang bervariasi di antara mesin, dan biasanya dapat disesuaikan dengan fungsi perpustakaan. —Kirim catatan]

Kode Anda dikompilasi dengan g++ -O3peringatan emisi (bahkan tanpa -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)
  ^

Satu-satunya cara kita dapat menganalisis apa yang sedang dilakukan program, adalah dengan membaca kode assembly yang dihasilkan.

Berikut daftar perakitan lengkap:

    .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

Saya bahkan hampir tidak bisa membaca perakitan, tetapi bahkan saya bisa melihat addl $1000000000, %edigaris. Kode yang dihasilkan lebih mirip

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

Komentar @TC ini:

Saya menduga itu adalah sesuatu seperti: (1) karena setiap iterasi dengan inilai lebih besar dari 2 memiliki perilaku yang tidak terdefinisi -> (2) kita dapat mengasumsikan bahwa i <= 2untuk keperluan optimasi -> (3) kondisi loop selalu benar -> (4) ) itu dioptimalkan menjadi loop tak terbatas.

memberi saya ide untuk membandingkan kode rakitan dari kode OP ke kode rakitan dari kode berikut, tanpa perilaku yang tidak ditentukan.

#include <iostream>

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

Dan, pada kenyataannya, kode yang benar memiliki kondisi terminasi.

    ; ...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, itu sama sekali tidak jelas! Tidak adil! Saya menuntut pengadilan dengan api!

Hadapilah, Anda menulis kode buggy dan Anda akan merasa tidak enak. Tanggung konsekuensinya.

... atau, sebagai alternatif, gunakan diagnosa yang lebih baik dan alat debugging yang lebih baik - itulah gunanya:

  • aktifkan semua peringatan

    • -Walladalah opsi gcc yang memungkinkan semua peringatan bermanfaat tanpa positif palsu. Ini adalah jumlah minimum yang harus selalu Anda gunakan.
    • gcc memiliki banyak opsi peringatan lain , namun, mereka tidak diaktifkan -Wallkarena mereka dapat memperingatkan tentang kesalahan positif
    • Visual C ++ sayangnya tertinggal dengan kemampuan untuk memberikan peringatan yang bermanfaat. Setidaknya IDE memungkinkan beberapa secara default.
  • gunakan flag debug untuk debugging

    • untuk integer overflow -ftrapvmenjebak program pada overflow,
    • Compiler dentang sangat baik untuk ini: -fcatch-undefined-behaviormenangkap banyak contoh perilaku tidak terdefinisi (catatan: "a lot of" != "all of them")

Saya memiliki program spaghetti yang tidak ditulis oleh saya yang harus dikirim besok! BANTUAN !!!!!!

Gunakan gcc -fwrapv

Opsi ini menginstruksikan kompiler untuk mengasumsikan bahwa aritmatika yang ditandatangani dari penjumlahan, pengurangan dan multiplikasi membungkus menggunakan representasi dua-pelengkap.

1 - aturan ini tidak berlaku untuk "unsigned integer overflow", seperti §3.9.1.4 mengatakan itu

Bilangan bulat tak bertanda, dinyatakan tidak bertanda, harus mematuhi hukum modul hitung 2 n di mana n adalah jumlah bit dalam representasi nilai ukuran bilangan bulat tertentu.

dan misalnya hasil dari UINT_MAX + 1didefinisikan secara matematis - dengan aturan modulithith 2 n

milleniumbug
sumber
7
Saya masih tidak benar-benar mengerti apa yang terjadi di sini ... Mengapa itu isendiri terpengaruh? Secara umum perilaku tidak terdefinisi tidak memiliki efek samping yang aneh, setelah semua, i*100000000harus menjadi nilai
vsoftco
26
Saya menduga itu adalah sesuatu seperti: (1) karena setiap iterasi dengan inilai lebih besar dari 2 memiliki perilaku yang tidak terdefinisi -> (2) kita dapat mengasumsikan bahwa i <= 2untuk keperluan optimasi -> (3) kondisi loop selalu benar -> (4) ) itu dioptimalkan menjadi loop tak terbatas.
TC
28
@vsoftco: Apa yang terjadi adalah kasus pengurangan kekuatan , lebih khusus, penghapusan variabel induksi . Kompiler menghilangkan perkalian dengan memancarkan kode yang bukannya bertambah i1e9 setiap iterasi (dan mengubah kondisi loop sesuai). Ini adalah optimasi yang sangat valid di bawah aturan "seolah-olah" karena program ini tidak dapat mengamati perbedaannya jika itu berperilaku baik. Sayangnya, tidak, dan optimasi "bocor".
JohannesD
8
@ JohannesD memakukan alasan ini rusak. Namun, ini adalah optimasi yang buruk karena kondisi terminasi loop tidak melibatkan overflow. Penggunaan pengurangan kekuatan tidak apa-apa - Saya tidak tahu apa yang akan dilakukan oleh pengali dalam prosesor (4 * 100000000) yang akan berbeda dengan (100000000 + 100000000 + 100000000 + 100000000), dan jatuh kembali pada "itu tidak terdefinisi - siapa tahu "itu masuk akal. Tetapi mengganti apa yang seharusnya menjadi loop berperilaku baik, yang mengeksekusi 4 kali dan menghasilkan hasil yang tidak ditentukan, dengan sesuatu yang mengeksekusi lebih dari 4 kali "karena itu tidak terdefinisi!" adalah kebodohan.
Julie di Austin
14
@JulieinAustin Meskipun ini mungkin bodoh bagi Anda, itu sah-sah saja. Sisi positifnya, kompiler memperingatkan Anda tentang hal itu.
milleniumbug
68

Jawaban singkat, gccsecara khusus telah mendokumentasikan masalah ini, kita dapat melihat bahwa dalam catatan rilis gcc 4.8 yang bertuliskan ( penekanan akan terus berlanjut ):

GCC sekarang menggunakan analisis yang lebih agresif untuk mendapatkan batas atas untuk jumlah iterasi loop menggunakan kendala yang dipaksakan oleh standar bahasa . Ini dapat menyebabkan program yang tidak sesuai tidak lagi berfungsi seperti yang diharapkan, seperti SPEC CPU 2006 464.h264ref dan 416.gamess. Opsi baru, -fno-agresif-loop-optimisasi, ditambahkan untuk menonaktifkan analisis agresif ini. Dalam beberapa loop yang telah mengetahui jumlah iterasi yang konstan, tetapi perilaku tidak terdefinisi diketahui terjadi dalam loop sebelum mencapai atau selama iterasi terakhir, GCC akan memperingatkan tentang perilaku yang tidak terdefinisi dalam loop alih-alih menurunkan batas atas bawah dari jumlah iterasi untuk loop. Peringatan itu dapat dinonaktifkan dengan -Wno-agresif-loop-optimasi.

dan memang jika kita menggunakan -fno-aggressive-loop-optimizationsperilaku loop tak terbatas harus berhenti dan tidak dalam semua kasus yang saya uji.

Jawaban panjang dimulai dengan mengetahui bahwa bilangan bulat bertanda tangan ditandatangani adalah perilaku tidak terdefinisi dengan melihat rancangan C ++ bagian standar 5 Ekspresi paragraf 4 yang mengatakan:

Jika selama evaluasi ekspresi, hasilnya tidak didefinisikan secara matematis atau tidak dalam kisaran nilai yang dapat diwakili untuk jenisnya, perilaku tidak terdefinisi . [Catatan: sebagian besar implementasi C ++ mengabaikan integer overflows. Perawatan pembagian dengan nol, membentuk sisa menggunakan pembagi nol, dan semua pengecualian floating point bervariasi di antara mesin, dan biasanya disesuaikan dengan fungsi perpustakaan. —Menghapus

Kita tahu bahwa standar mengatakan perilaku tidak terdefinisi tidak dapat diprediksi dari catatan yang datang dengan definisi yang mengatakan:

[Catatan: Perilaku yang tidak terdefinisi dapat diharapkan ketika Standar Internasional ini menghilangkan definisi perilaku yang eksplisit atau ketika suatu program menggunakan konstruksi yang salah atau data yang salah. Perilaku tak terdefinisi yang diizinkan berkisar dari mengabaikan situasi sepenuhnya dengan hasil yang tidak terduga , hingga berperilaku selama penerjemahan atau pelaksanaan program dengan cara yang terdokumentasi yang menjadi karakteristik lingkungan (dengan atau tanpa penerbitan pesan diagnostik), hingga penghentian terjemahan atau eksekusi (dengan penerbitan pesan diagnostik). Banyak konstruksi program yang salah tidak menimbulkan perilaku tidak terdefinisi; mereka harus didiagnosis. —Kirim catatan]

Tetapi apa yang bisa dilakukan oleh gccpengoptimal untuk mengubah ini menjadi loop tanpa batas? Kedengarannya sangat aneh. Tapi untungnya gccmemberi kita petunjuk untuk mencari tahu dalam peringatan:

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

Petunjuknya adalah Waggressive-loop-optimizations, apa artinya itu? Untungnya bagi kami ini bukan pertama kalinya optimasi ini melanggar kode dengan cara ini dan kami beruntung karena John Regehr telah mendokumentasikan sebuah kasus dalam artikel GCC pra-4.8 Breaks Broken SPEC 2006 Benchmark yang menunjukkan kode berikut:

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;
}

artikel itu mengatakan:

Perilaku yang tidak ditentukan mengakses d [16] sesaat sebelum keluar dari loop. Di C99 adalah sah untuk membuat pointer ke elemen satu posisi melewati akhir array, tetapi pointer itu tidak boleh ditereferensi.

dan kemudian mengatakan:

Secara rinci, inilah yang sedang terjadi. Kompiler AC, setelah melihat d [++ k], diizinkan untuk mengasumsikan bahwa nilai kenaikan k berada dalam batas array, karena perilaku yang tidak terdefinisi terjadi. Untuk kode di sini, GCC dapat menyimpulkan bahwa k berada dalam kisaran 0..15. Beberapa saat kemudian, ketika GCC melihat k <16, ia mengatakan pada dirinya sendiri: "Aha– ungkapan itu selalu benar, jadi kami memiliki loop tak terbatas."Situasi di sini, di mana kompiler menggunakan asumsi ketelitian untuk menyimpulkan fakta aliran data yang berguna,

Jadi apa yang harus dilakukan oleh kompiler dalam beberapa kasus adalah mengasumsikan karena integer yang ditandatangani ditandatangani adalah perilaku yang tidak didefinisikan maka iharus selalu kurang dari4 dan dengan demikian kita memiliki loop tak terbatas.

Dia menjelaskan ini sangat mirip dengan penghapusan null pointer kernel Linux terkenal di mana dalam melihat kode ini:

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

gccdisimpulkan bahwa karena stelah ditangguhkan s->f;dan karena dereferensi pointer nol adalah perilaku yang tidak terdefinisi maka stidak boleh nol dan karena itu mengoptimalkan if (!s)pemeriksaan di baris berikutnya.

Pelajaran di sini adalah bahwa pengoptimal modern sangat agresif dalam mengeksploitasi perilaku yang tidak terdefinisi dan kemungkinan besar hanya akan menjadi lebih agresif. Jelas hanya dengan beberapa contoh kita dapat melihat pengoptimal melakukan hal-hal yang tampaknya benar-benar tidak masuk akal bagi seorang programmer tetapi dalam retrospeksi dari perspektif pengoptimal masuk akal.

Shafik Yaghmour
sumber
7
Saya mengerti bahwa inilah yang dilakukan penulis kompiler (saya biasa menulis kompiler dan bahkan satu atau dua optimizer), tetapi ada perilaku yang "berguna" meskipun mereka "tidak terdefinisi", dan ini menuju ke optimasi yang lebih agresif. hanya kegilaan. Konstruk yang Anda kutip di atas salah, tetapi mengoptimalkan pemeriksaan kesalahan adalah permusuhan pengguna.
Julie di Austin
1
@JulieinAustin Saya setuju ini adalah perilaku yang cukup mengejutkan, mengatakan bahwa pengembang perlu menghindari perilaku tidak terdefinisi sebenarnya hanya setengah dari masalah. Jelas kompiler juga perlu memberikan umpan balik yang lebih baik kepada pengembang juga. Dalam hal ini peringatan dihasilkan meskipun tidak cukup informatif.
Shafik Yaghmour
3
Saya pikir ini hal yang baik, saya ingin kode yang lebih baik dan lebih cepat. UB seharusnya tidak pernah digunakan.
paulm
1
@paulm secara moral UB jelas buruk tetapi sulit untuk membantah dengan menyediakan alat yang lebih baik seperti analisa statis dentang untuk membantu pengembang menangkap UB dan masalah lain sebelum berdampak pada aplikasi produksi.
Shafik Yaghmour
1
@ShafikYaghmour Juga, jika pengembang Anda mengabaikan peringatan, apa kemungkinan mereka akan memperhatikan keluaran dentang? Masalah ini dapat dengan mudah ditangkap oleh kebijakan "tidak ada peringatan yang tidak adil" yang agresif. Dentang disarankan tetapi tidak diperlukan.
deworde
24

tl; dr Kode menghasilkan tes integer + bilangan bulat positif == bilangan bulat negatif . Biasanya optimizer tidak mengoptimalkan ini keluar, tetapi dalam kasus spesifik std::endlyang digunakan berikutnya, kompilator tidak mengoptimalkan tes ini. Saya belum tahu apa yang istimewa tentang endlbelum.


Dari kode rakitan di -O1 dan level yang lebih tinggi, jelas bahwa gcc mereformasi loop ke:

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

Nilai terbesar yang bekerja dengan benar adalah 715827882, yaitu floor ( INT_MAX/3). Cuplikan perakitan di -O1adalah:

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

Perhatikan, -1431655768ini4 * 715827882 di komplemen 2's.

Memukul -O2mengoptimalkan itu sebagai berikut:

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

Jadi optimasi yang telah dibuat hanyalah bahwa addl itu dipindahkan lebih tinggi.

Jika kita mengkompilasi ulang 715827883sebagai gantinya maka versi -O1 identik terlepas dari angka dan nilai pengujian yang diubah. Namun, -O2 kemudian membuat perubahan:

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

Di mana ada cmpl $-1431655764, %esidi -O1, garis itu telah dihapus untuk -O2. Optimizer harus telah memutuskan bahwa menambahkan 715827883untuk %esitidak pernah bisa sama -1431655764.

Ini cukup membingungkan. Menambahkan itu untuk INT_MIN+1 memang menghasilkan hasil yang diharapkan, sehingga pengoptimal harus memutuskan bahwa %esitidak pernah bisa INT_MIN+1dan saya tidak yakin mengapa itu akan memutuskan itu.

Dalam contoh kerja tampaknya sama validnya untuk menyimpulkan bahwa menambahkan 715827882ke nomor tidak bisa sama dengan INT_MIN + 715827882 - 2! (ini hanya mungkin jika sampul benar-benar terjadi), namun itu tidak mengoptimalkan garis keluar dalam contoh itu.


Kode yang saya gunakan adalah:

#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);
    }
}

Jika std::endl(std::cout)dihapus maka optimasi tidak lagi terjadi. Bahkan menggantinya dengan std::cout.put('\n'); std::flush(std::cout);juga menyebabkan pengoptimalan tidak terjadi, meskipun std::endlsudah sebaris.

The inlining dari std::endltampaknya mempengaruhi bagian awal dari struktur loop (yang saya tidak mengerti apa yang dilakukannya tetapi saya akan mempostingnya di sini kalau-kalau ada orang lain):

Dengan kode asli dan -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

Dengan inlining mymanual dari 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

Salah satu perbedaan antara keduanya adalah yang %esidigunakan dalam versi asli, dan %ebxdalam versi kedua; Apakah ada perbedaan dalam semantik yang didefinisikan antara %esidan %ebxsecara umum? (Saya tidak tahu banyak tentang perakitan x86).

MM
sumber
Akan lebih baik untuk mengetahui apa sebenarnya logika pengoptimal itu, tidak jelas bagi saya pada tahap ini mengapa beberapa kasus memiliki tes dioptimalkan dan beberapa tidak
MM
8

Contoh lain dari kesalahan ini dilaporkan dalam gcc adalah ketika Anda memiliki loop yang mengeksekusi untuk jumlah iterasi yang konstan, tetapi Anda menggunakan variabel counter sebagai indeks ke dalam array yang memiliki kurang dari jumlah item, seperti:

int a[50], x;

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

Compiler dapat menentukan bahwa loop ini akan mencoba mengakses memori di luar array 'a'. Kompilator mengeluh tentang hal ini dengan pesan yang agak samar:

iterasi xxu memanggil perilaku tidak terdefinisi [-Werror = optimisasi loop agresif]

Ed Tyler
sumber
Yang lebih samar lagi adalah bahwa pesan hanya memancarkan ketika optimisasi dihidupkan. Pesan M $ VB "Array out of bound" adalah untuk boneka?
Ravi Ganesh
6

Apa yang tidak bisa saya dapatkan adalah mengapa nilai saya rusak oleh operasi luapan itu?

Tampaknya integer overflow terjadi di iterasi ke-4 (untuk i = 3). signedinteger overflow memanggil perilaku tidak terdefinisi . Dalam hal ini tidak ada yang bisa diprediksi. Pengulangan hanya dapat dilakukan beberapa 4kali atau mungkin tidak terhingga atau apa pun!
Hasil dapat bervariasi dari kompiler ke kompiler atau bahkan untuk versi berbeda dari kompiler yang sama.

C11: 1.3.24 perilaku tidak terdefinisi:

perilaku yang tidak dituntut oleh Standar Internasional ini
[Catatan: Perilaku tidak terdefinisi dapat diharapkan ketika Standar Internasional ini menghilangkan definisi perilaku yang eksplisit atau ketika suatu program menggunakan konstruksi yang salah atau data yang salah. Perilaku tak terdefinisi yang diizinkan berkisar dari mengabaikan situasi sepenuhnya dengan hasil yang tidak terduga, hingga berperilaku selama penerjemahan atau pelaksanaan program dengan cara yang terdokumentasi yang menjadi karakteristik lingkungan (dengan atau tanpa penerbitan pesan diagnostik), hingga penghentian terjemahan atau eksekusi (dengan penerbitan pesan diagnostik) . Banyak konstruksi program yang salah tidak menimbulkan perilaku tidak terdefinisi; mereka harus didiagnosis. —Kirim catatan]

haccks
sumber
@bits_international; Iya.
pukul
4
Anda benar, itu adil untuk menjelaskan mengapa saya kalah. Informasi dalam jawaban ini benar, tetapi tidak mendidik dan benar-benar mengabaikan gajah di ruangan: kerusakan tampaknya terjadi di tempat yang berbeda (kondisi berhenti) daripada operasi yang menyebabkan luapan. Mekanisme tentang bagaimana barang-barang rusak dalam kasus khusus ini tidak dijelaskan, meskipun ini adalah inti dari pertanyaan ini. Ini adalah situasi guru yang buruk yang khas di mana jawaban guru tidak hanya tidak membahas inti masalah, tetapi juga mencegah pertanyaan lebih lanjut. Hampir terdengar seperti ...
Szabolcs
5
"Saya melihat ini adalah perilaku yang tidak terdefinisi, dan mulai saat ini saya tidak peduli bagaimana atau mengapa itu rusak. Standar ini memungkinkannya untuk dilanggar. Tidak ada pertanyaan lebih lanjut." Anda mungkin tidak bermaksud seperti itu, tetapi sepertinya begitu. Saya berharap untuk melihat kurang dari ini (sangat disayangkan) sikap pada SO. Ini praktis tidak berguna. Jika Anda mendapatkan input pengguna, tidak masuk akal untuk memeriksa overflow setelah setiap operasi integer yang ditandatangani , bahkan jika standar mengatakan bahwa ada bagian lain dari program yang mungkin meledak karena itu. Memahami bagaimana itu pecah tidak membantu menghindari masalah seperti ini dalam praktiknya.
Szabolcs
2
@Szabolcs: Mungkin lebih baik untuk menganggap C sebagai dua bahasa, salah satunya dirancang untuk memungkinkan kompiler sederhana untuk mencapai kode yang dapat dieksekusi yang cukup efisien dengan bantuan programmer yang mengeksploitasi konstruksi yang dapat diandalkan pada platform target yang dituju tetapi tidak yang lain, dan akibatnya diabaikan oleh komite Standar, dan bahasa kedua yang mengecualikan semua konstruksi seperti itu yang Standar tidak mengamanatkan dukungan, untuk tujuan memungkinkan kompiler untuk menerapkan optimasi tambahan yang mungkin atau mungkin tidak melebihi yang harus dimiliki oleh programmer. menyerah.
supercat
1
@Szabolcs " Jika Anda mendapatkan input pengguna, tidak masuk akal untuk memeriksa overflow setelah setiap operasi integer yang ditandatangani " - benar karena pada saat itu sudah terlambat. Anda harus memeriksa luapan sebelum setiap operasi integer yang ditandatangani.
melpomene