ทำไมลูปนี้สร้าง“ คำเตือน: การวนซ้ำ 3u เรียกใช้พฤติกรรมที่ไม่ได้กำหนด” และส่งออกมากกว่า 4 บรรทัด


162

รวบรวมสิ่งนี้:

#include <iostream>

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

และgccสร้างคำเตือนต่อไปนี้:

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

ฉันเข้าใจว่ามีจำนวนเต็มล้นที่ลงนามแล้ว

สิ่งที่ฉันไม่สามารถทำได้คือทำไม iค่าจึงแตกโดยการดำเนินการมากเกินไป

ฉันได้อ่านคำตอบว่าทำไมจำนวนเต็มล้นใน x86 กับ GCC ทำให้เกิดการวนซ้ำไม่สิ้นสุด? แต่ฉันยังไม่ชัดเจนว่าทำไมสิ่งนี้เกิดขึ้น - ฉันได้รับ "undefined" หมายถึง "อะไรก็ได้ที่เกิดขึ้น" แต่สาเหตุพื้นฐานของพฤติกรรมที่เฉพาะเจาะจงนี้คืออะไร

ออนไลน์: http://ideone.com/dMrRKR

คอมไพเลอร์: gcc (4.8)


49
เครื่องหมายจำนวนเต็มล้น => พฤติกรรมที่ไม่ได้กำหนด => Nasal Daemons แต่ฉันต้องยอมรับว่าตัวอย่างนั้นค่อนข้างดี
dyp

1
ชุดประกอบออก: goo.gl/TtPmZn
ไบรอันเฉิน

1
เกิดขึ้นกับ GCC 4.8 ด้วยO2และO3ตั้งค่าสถานะ แต่ไม่ใช่O0หรือO1
Alex

3
@dyp เมื่อฉันอ่าน Nasal Daemons ฉันได้ทำ "imgur laugh" ซึ่งประกอบด้วยการหายใจออกทางจมูกเล็กน้อยเมื่อคุณเห็นบางสิ่งที่ตลก แล้วฉันก็รู้ว่า ... ฉันต้องถูกสาปโดย Nasal Daemon!
corsiKa

4
คั่นหน้านี้เพื่อให้ฉันสามารถเชื่อมโยงได้ในครั้งต่อไปที่มีคนตอบโต้ "เป็น UB ในทางเทคนิค แต่มันควรทำสิ่งต่าง ๆ " :)
MM

คำตอบ:


107

จำนวนเต็มล้นลงนาม (เช่นการพูดอย่างเคร่งครัดไม่มีสิ่งเช่น "ล้นจำนวนเต็มไม่ได้ลงนาม") หมายถึงพฤติกรรมที่ไม่ได้กำหนดพฤติกรรมที่ไม่ได้กำหนดและนี่หมายความว่าทุกสิ่งสามารถเกิดขึ้นได้และการอภิปรายว่าทำไมมันเกิดขึ้นภายใต้กฎของ C ++ นั้นไม่สมเหตุสมผล

C ++ 11 ฉบับร่าง N3337: §5.4: 1

หากในระหว่างการประเมินผลของนิพจน์ผลลัพธ์จะไม่ถูกคำนวณทางคณิตศาสตร์หรือไม่อยู่ในช่วงของค่าที่สามารถแทนได้สำหรับประเภทของมันพฤติกรรมนั้นจะไม่ได้รับการแก้ไข [หมายเหตุ: การใช้งานที่มีอยู่ส่วนใหญ่ของ C ++ ละเว้นจำนวนเต็มมากกว่า fl ows การรักษาโดยการหารศูนย์สร้างส่วนที่เหลือโดยใช้ตัวหารศูนย์และข้อยกเว้นจุดแตกต่างกันระหว่างเครื่องและมักจะปรับได้โดยฟังก์ชั่นห้องสมุด - บันทึกย่อ]

โค้ดของคุณถูกคอมไพล์ด้วยg++ -O3คำเตือนการส่งเสียง (แม้จะไม่มี-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)
  ^

วิธีเดียวที่เราสามารถวิเคราะห์สิ่งที่โปรแกรมกำลังทำคือการอ่านรหัสการประกอบที่สร้างขึ้น

นี่คือรายการประกอบเต็ม:

    .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

ฉันแทบจะไม่สามารถอ่านชุดประกอบได้ แต่ฉันเห็นaddl $1000000000, %ediบรรทัดได้ โค้ดผลลัพธ์ดูเหมือนมากกว่า

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

ความคิดเห็นของ @TC นี้:

ฉันสงสัยว่ามันคล้ายกับ: (1) เพราะการวนซ้ำทุกครั้งที่มีiค่ามากกว่า 2 มีพฤติกรรมที่ไม่ได้กำหนด -> (2) เราสามารถสันนิษฐานได้ว่าi <= 2เพื่อวัตถุประสงค์ในการปรับให้เหมาะสม -> (3) สภาพของห่วงนั้นเป็นจริงเสมอ -> (4 ) มันปรับให้เหมาะกับการวนซ้ำไม่สิ้นสุด

ให้แนวคิดในการเปรียบเทียบรหัสการประกอบของรหัส OP กับรหัสการประกอบของรหัสต่อไปนี้โดยไม่มีพฤติกรรมที่ไม่ได้กำหนด

#include <iostream>

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

และในความเป็นจริงรหัสที่ถูกต้องมีเงื่อนไขการเลิกจ้าง

    ; ...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 นั่นไม่ชัดเจนเลย! มันไม่ยุติธรรม! ฉันต้องการการทดลองด้วยไฟ!

จัดการกับมันคุณเขียนรหัสรถและคุณควรจะรู้สึกไม่ดี รับผลที่ตามมา

... หรือมิฉะนั้นให้ใช้การวินิจฉัยที่ดีขึ้นและใช้เครื่องมือการดีบักที่ดีขึ้นนั่นคือสิ่งที่มันมีไว้สำหรับ:

  • เปิดใช้งานคำเตือนทั้งหมด

    • -Wallเป็นตัวเลือก gcc ที่เปิดใช้งานการเตือนที่เป็นประโยชน์ทั้งหมดโดยไม่มีผลบวกปลอม นี่เป็นขั้นต่ำเปล่าที่คุณควรใช้
    • gcc มีตัวเลือกการเตือนอื่น ๆ อีกมากมายอย่างไรก็ตามไม่ได้เปิดใช้งานด้วย-Wallเนื่องจากพวกเขาอาจเตือนเกี่ยวกับการบวกเท็จ
    • Visual C ++ น่าเสียดายที่ล้าหลังด้วยความสามารถในการแจ้งเตือนที่เป็นประโยชน์ อย่างน้อย IDE จะเปิดใช้งานบางอย่างเป็นค่าเริ่มต้น
  • ใช้ดีบักแฟล็กสำหรับการดีบัก

    • สำหรับจำนวนเต็มล้น -ftrapvกับดักโปรแกรมบนล้น
    • เรียบเรียงเสียงดังกราวเป็นที่ยอดเยี่ยมสำหรับการนี้: -fcatch-undefined-behaviorจับจำนวนมากกรณีของพฤติกรรมที่ไม่ได้กำหนด (หมายเหตุ: "a lot of" != "all of them")

ฉันมีปาเก็ตตี้เป็นระเบียบของโปรแกรมที่ไม่ได้เขียนโดยฉันที่จะต้องจัดส่งในวันพรุ่งนี้! ช่วยเหลือ !!!!!! 111oneone

ใช้ของ gcc -fwrapv

ตัวเลือกนี้สั่งให้คอมไพเลอร์จะสันนิษฐานว่าโอเวอร์โฟลว์ทางคณิตศาสตร์ที่ลงนามแล้วของการบวกการลบและการคูณจะล้อมรอบโดยใช้การแทนด้วย twos-complement

1 - กฎนี้ใช้ไม่ได้กับ "ล้นจำนวนเต็มไม่ได้ลงนาม" ตามที่§3.9.1.4กล่าวว่า

เลขจำนวนเต็มที่ไม่ได้ลงนามซึ่งไม่ได้ลงนามจะต้องปฏิบัติตามกฎของเลขคณิตโมดูโล 2 nโดยที่ n คือจำนวนบิตในการแทนค่าของขนาดเฉพาะของจำนวนนั้น

และเช่นผลลัพธ์ของUINT_MAX + 1ถูกกำหนดทางคณิตศาสตร์ - ตามกฎของเลขคณิตโมดูโล 2 n


7
ฉันยังไม่เข้าใจจริงๆว่าเกิดอะไรขึ้นที่นี่ ... ทำไมiตัวเองถึงได้รับผลกระทบ? โดยทั่วไปพฤติกรรมที่ไม่ได้กำหนดไม่ได้มีผลข้างเคียงที่แปลกประหลาดเหล่านี้ แต่อย่างใดi*100000000ควรจะเป็นค่า
vsoftco

26
ฉันสงสัยว่ามันคล้ายกับ: (1) เพราะการวนซ้ำทุกครั้งที่มี iค่ามากกว่า 2 มีพฤติกรรมที่ไม่ได้กำหนด -> (2) เราสามารถสันนิษฐานได้ว่าi <= 2เพื่อวัตถุประสงค์ในการปรับให้เหมาะสม -> (3) สภาพของห่วงนั้นเป็นจริงเสมอ -> (4 ) มันปรับให้เหมาะกับการวนซ้ำไม่สิ้นสุด
TC

28
@vsoftco: สิ่งที่เกิดขึ้นเป็นกรณีของการลดความแข็งแรงมากขึ้นโดยเฉพาะตัวแปรเหนี่ยวนำการกำจัด คอมไพเลอร์กำจัดการคูณโดยการปล่อยรหัสซึ่งจะเพิ่มขึ้นทีละi1e9 แทนการวนซ้ำ (และการเปลี่ยนเงื่อนไขการวนซ้ำตามนั้น) นี่คือการเพิ่มประสิทธิภาพที่ถูกต้องสมบูรณ์แบบภายใต้กฎ "ราวกับว่า" เนื่องจากโปรแกรมนี้ไม่สามารถสังเกตเห็นความแตกต่างได้ว่ามันทำงานได้ดี อนิจจาไม่ใช่และการเพิ่มประสิทธิภาพ "รั่ว"
JohannesD

8
@ JohannesD ตอกย้ำสาเหตุที่ทำให้ช่วงนี้หยุดพัก อย่างไรก็ตามนี่เป็นการเพิ่มประสิทธิภาพที่ไม่ดีเนื่องจากเงื่อนไขการยกเลิกลูปไม่เกี่ยวข้องกับการโอเวอร์โฟลว์ การใช้กำลังลดลงก็โอเค - ฉันไม่รู้ว่าตัวคูณในโปรเซสเซอร์จะทำอะไรกับ (4 * 100000000) ที่จะแตกต่างกับ (100000000 + 100000000 + 100000000 + 100000000) และถอยกลับไปที่ "มันไม่ได้กำหนด - ใครจะรู้ "มีเหตุผล แต่แทนที่สิ่งที่ควรเป็นลูปที่มีความประพฤติดีซึ่งดำเนินการ 4 ครั้งและสร้างผลลัพธ์ที่ไม่ได้กำหนดด้วยบางสิ่งที่ดำเนินการมากกว่า 4 ครั้ง "เพราะไม่ได้กำหนด!" คือความงี่เง่า
Julie ใน Austin

14
@JulieinAustin ในขณะที่มันอาจจะงี่เง่ากับคุณมันถูกกฎหมายอย่างสมบูรณ์แบบ ในด้านบวกคอมไพเลอร์เตือนคุณเกี่ยวกับเรื่องนี้
milleniumbug

68

คำตอบสั้น ๆgccโดยเฉพาะมีการบันทึกปัญหานี้เราจะเห็นว่าในgcc 4.8 บันทึกย่อประจำรุ่นซึ่งกล่าวว่า ( เน้นไปข้างหน้า ):

GCC ตอนนี้ใช้มากขึ้นการวิเคราะห์เชิงรุกในการได้มาซึ่งผูกพันบนสำหรับจำนวนของการทำซ้ำของลูปโดยใช้ข้อ จำกัด ที่กำหนดโดยมาตรฐานภาษา สิ่งนี้อาจทำให้โปรแกรมที่ไม่เป็นไปตามข้อกำหนดไม่สามารถทำงานได้อย่างที่คาดไว้เช่น SPEC CPU 2006 464.h264ref และ 416.gamess มีการเพิ่มตัวเลือกใหม่ - -no-ก้าวร้าวลูป - การเพิ่มประสิทธิภาพเพื่อปิดการวิเคราะห์เชิงรุกนี้ ในบางลูปที่ทราบจำนวนการวนซ้ำคงที่ แต่พฤติกรรมที่ไม่ได้กำหนดเกิดขึ้นในลูปก่อนถึงหรือในระหว่างการทำซ้ำครั้งล่าสุด GCC จะเตือนเกี่ยวกับพฤติกรรมที่ไม่ได้กำหนดในลูปแทนการวนซ้ำบนล่างของจำนวนการวนซ้ำ สำหรับวง คำเตือนสามารถปิดการใช้งานด้วย -Wno-ก้าวร้าวลูปการเพิ่มประสิทธิภาพ

และแน่นอนถ้าเราใช้ -fno-aggressive-loop-optimizationsพฤติกรรมวนรอบไม่สิ้นสุดควรหยุดและมันก็ทำในทุกกรณีที่ฉันได้ทดสอบ

คำตอบที่ยาวเริ่มต้นด้วยการรู้ว่าจำนวนเต็มล้นลงนามคือพฤติกรรมที่ไม่ได้กำหนดโดยดูที่ร่างมาตรฐาน C ++ ส่วน5 นิพจน์วรรค4ซึ่งกล่าวว่า:

หากในระหว่างการประเมินผลของการแสดงออกที่ผลที่ได้คือไม่ได้กำหนดไว้ทางคณิตศาสตร์หรือไม่ได้อยู่ในช่วงของค่าที่แทนสำหรับประเภทของพฤติกรรมจะไม่ได้กำหนด [หมายเหตุ: การใช้งานที่มีอยู่ส่วนใหญ่ของ C ++ จะไม่สนใจการล้นของจำนวนเต็ม การรักษาโดยการหารศูนย์สร้างส่วนที่เหลือโดยใช้ตัวหารศูนย์และข้อยกเว้นจุดลอยตัวทั้งหมดแตกต่างกันไปในเครื่องและมักจะปรับได้โดยฟังก์ชั่นห้องสมุด - บันทึกย่อ

เรารู้ว่ามาตรฐานบอกว่าพฤติกรรมที่ไม่ได้กำหนดนั้นไม่สามารถคาดเดาได้จากบันทึกที่มาพร้อมกับคำจำกัดความซึ่งระบุว่า:

[หมายเหตุ: พฤติกรรมที่ไม่ได้กำหนดอาจถูกคาดหวังเมื่อมาตรฐานสากลนี้ละเว้นคำจำกัดความที่ชัดเจนของพฤติกรรมหรือเมื่อโปรแกรมใช้โครงสร้างที่ผิดพลาดหรือข้อมูลที่ผิดพลาด พฤติกรรมที่ไม่ได้กำหนดอนุญาตช่วงจากการละเว้นสถานการณ์อย่างสมบูรณ์กับผลลัพธ์ที่ไม่สามารถคาดการณ์ได้ถึงพฤติกรรมในระหว่างการแปลหรือการดำเนินการโปรแกรมในลักษณะเอกสารของสภาพแวดล้อม (มีหรือไม่มีการออกข้อความวินิจฉัย) เพื่อยกเลิกการแปลหรือการดำเนินการ ของข้อความวินิจฉัย) การสร้างโปรแกรมที่ผิดพลาดจำนวนมากไม่ก่อให้เกิดพฤติกรรมที่ไม่ได้กำหนด พวกเขาจะต้องได้รับการวินิจฉัย - บันทึกย่อ]

แต่gccเครื่องมือเพิ่มประสิทธิภาพสามารถทำอะไรเพื่อเปลี่ยนสิ่งนี้ให้กลายเป็นวงที่ไม่สิ้นสุด มันฟังดูแปลกประหลาดอย่างสมบูรณ์ แต่โชคดีที่gccทำให้เรามีเงื่อนงำในการหาคำเตือน:

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

เบาะแสคือWaggressive-loop-optimizationsสิ่งที่หมายความว่าอย่างไร โชคดีสำหรับเรานี่ไม่ใช่ครั้งแรกที่การเพิ่มประสิทธิภาพนี้มีรหัสที่ใช้ไม่ได้ในทางนี้และเราโชคดีเพราะJohn Regehrได้จัดทำเอกสารกรณีในบทความGCC pre-4.8 ตัวแบ่ง Broken SPEC 2006 เกณฑ์มาตรฐานซึ่งแสดงรหัสต่อไปนี้:

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

บทความพูดว่า:

พฤติกรรมที่ไม่ได้กำหนดกำลังเข้าถึง d [16] ก่อนออกจากลูป ใน C99 มันถูกกฎหมายในการสร้างตัวชี้ไปยังองค์ประกอบหนึ่งตำแหน่งผ่านจุดสิ้นสุดของอาร์เรย์ แต่ตัวชี้นั้นจะต้องไม่ถูกยกเลิกการลงทะเบียน

และในภายหลังพูดว่า:

รายละเอียดนี่คือสิ่งที่เกิดขึ้น คอมไพเลอร์ AC เมื่อเห็น d [++ k] ได้รับอนุญาตให้ถือว่าค่าที่เพิ่มขึ้นของ k อยู่ภายในขอบเขตของอาเรย์เนื่องจากพฤติกรรมที่ไม่ได้กำหนดเกิดขึ้นเป็นอย่างอื่น สำหรับรหัสที่นี่GCC สามารถอนุมานได้ว่า k อยู่ในช่วง 0..15 อีกไม่นานเมื่อ GCC เห็น k <16 มันพูดกับตัวเองว่า:“ Aha - การแสดงออกนั้นเป็นความจริงเสมอดังนั้นเราจึงมีวงวนไม่สิ้นสุด” สถานการณ์ที่นี่ซึ่งคอมไพเลอร์ใช้สมมติฐานของความชัดเจนเพื่อสรุปข้อมูลจริงที่เป็นประโยชน์

ดังนั้นสิ่งที่คอมไพเลอร์จะต้องทำในบางกรณีสมมติว่าเนื่องจากจำนวนเต็มล้นลงนามคือพฤติกรรมที่ไม่ได้กำหนดแล้วiจะต้องน้อยกว่า4และดังนั้นเราจึงมีวง จำกัด

เขาอธิบายว่านี่คล้ายกับการลบการตรวจสอบตัวชี้เคอร์เนล null ของลีนุกซ์ที่น่าอับอายมากซึ่งจะเห็นรหัสนี้:

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

gccอนุมานได้ว่าตั้งแต่sถูก s->f;เลื่อนออกไปและตั้งแต่ dereferencing ตัวชี้โมฆะเป็นพฤติกรรมที่ไม่ได้กำหนดแล้วsออกไปจะต้องไม่เป็นโมฆะดังนั้นจึงปรับการif (!s)ตรวจสอบในบรรทัดถัดไปให้เหมาะสม

บทเรียนที่นี่คือเครื่องมือเพิ่มประสิทธิภาพที่ทันสมัยมีความก้าวร้าวมากเกี่ยวกับการใช้ประโยชน์จากพฤติกรรมที่ไม่ได้กำหนดและส่วนใหญ่จะได้รับเพียงก้าวร้าว เห็นได้ชัดว่ามีเพียงไม่กี่ตัวอย่างเท่านั้นที่เราสามารถเห็นเครื่องมือเพิ่มประสิทธิภาพทำสิ่งที่ดูเหมือนว่าไม่มีเหตุผลอย่างสมบูรณ์สำหรับโปรแกรมเมอร์ แต่ในการมองย้อนกลับจากมุมมองของเครื่องมือเพิ่มประสิทธิภาพ


7
ฉันเข้าใจว่านี่คือสิ่งที่นักเขียนคอมไพเลอร์กำลังทำอยู่ (ฉันเคยเขียนคอมไพเลอร์และแม้กระทั่งเครื่องมือเพิ่มประสิทธิภาพหรือสอง) แต่มีพฤติกรรมที่ "มีประโยชน์" แม้ว่าพวกเขาจะ "ไม่ได้กำหนด" ก็ตาม เป็นเพียงความวิกลจริต โครงสร้างที่คุณอ้างถึงด้านบนไม่ถูกต้อง แต่การเพิ่มประสิทธิภาพการตรวจสอบข้อผิดพลาดนั้นเป็นเรื่องที่ไม่เป็นมิตรกับผู้ใช้
Julie ใน Austin

1
@JulieinAustin ฉันเห็นด้วยว่านี่เป็นพฤติกรรมที่น่าประหลาดใจมากโดยบอกว่านักพัฒนาจำเป็นต้องหลีกเลี่ยงพฤติกรรมที่ไม่ได้กำหนดเป็นเพียงครึ่งเดียวของปัญหา เห็นได้ชัดว่าคอมไพเลอร์ยังต้องการให้ข้อเสนอแนะที่ดีกว่าให้กับนักพัฒนาเช่นกัน ในกรณีนี้จะมีการเตือนแม้ว่าจะไม่ได้ให้ข้อมูลเพียงพอ
Shafik Yaghmour

3
ฉันคิดว่ามันเป็นสิ่งที่ดีฉันต้องการโค้ดที่ดีกว่าและเร็วกว่า ไม่ควรใช้ UB
paulm

1
@paulm ทางศีลธรรม UB เห็นได้ชัดว่าไม่ดี แต่มันก็ยากที่จะเถียงกับการจัดหาเครื่องมือที่ดีกว่าเช่นclang static analyserเพื่อช่วยให้นักพัฒนาจับ UB และปัญหาอื่น ๆ ก่อนที่มันจะส่งผลกระทบต่อการใช้งานการผลิต
Shafik Yaghmour

1
@ShafikYaghmour นอกจากนี้หากนักพัฒนาของคุณไม่สนใจคำเตือนอะไรคือโอกาสที่พวกเขาจะให้ความสนใจกับการส่งเสียงดังกราว? ปัญหานี้สามารถถูกจับได้ง่ายโดยนโยบาย "ไม่เตือนไม่ยุติธรรม" ก้าวร้าว เสียงดังกราวแนะนำ แต่ไม่จำเป็น
deworde

24

TL; DRรหัสสร้างการทดสอบที่จำนวนเต็ม + จำนวนเต็มบวก == จำนวนเต็มเชิงลบ โดยทั่วไปเครื่องมือเพิ่มประสิทธิภาพจะไม่ปรับให้เหมาะสม แต่ในกรณีเฉพาะของstd::endlการใช้งานต่อไปคอมไพเลอร์จะปรับการทดสอบนี้ให้เหมาะสม ผมไม่ได้คิดว่ามีอะไรพิเศษเกี่ยวกับendlเลย


จากรหัสชุดประกอบที่ -O1 และระดับที่สูงกว่าเป็นที่ชัดเจนว่า gcc refactors วนซ้ำไปที่:

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

ค่าที่ใหญ่ที่สุดที่ทำงานได้อย่างถูกต้องคือ715827882เช่น floor ( INT_MAX/3) ตัวอย่างชุดประกอบที่-O1:

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

โปรดทราบว่า-1431655768เป็น4 * 715827882ส่วนเสริมใน 2

การกดปุ่ม-O2เพิ่มประสิทธิภาพนั้นเป็นสิ่งต่อไปนี้:

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

ดังนั้นการปรับให้เหมาะสมที่ได้ทำขึ้นมาก็เพียงว่าสิ่งaddlนั้นถูกย้ายให้สูงขึ้น

หากเราทำการคอมไพล์ใหม่715827883แทน -O1 จะเหมือนกันนอกเหนือจากหมายเลขที่เปลี่ยนแปลงและค่าการทดสอบ อย่างไรก็ตาม -O2 จะทำการเปลี่ยนแปลง:

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

ที่มีcmpl $-1431655764, %esiในสายที่ได้ถูกลบออก-O1 -O2เครื่องมือเพิ่มประสิทธิภาพต้องตัดสินใจว่าการเพิ่ม715827883ลงไป%esiจะไม่เท่ากัน-1431655764ไม่สามารถที่เท่าเทียมกัน

นี่ทำให้งงเลยทีเดียว การเพิ่มสิ่งนั้นลงในINT_MIN+1 นั้นจะสร้างผลลัพธ์ที่คาดหวังดังนั้นเครื่องมือเพิ่มประสิทธิภาพต้องตัดสินใจว่า%esiจะไม่มีทางเป็นไปได้INT_MIN+1และฉันไม่แน่ใจว่าทำไมจึงต้องตัดสินใจ

ในตัวอย่างการทำงานดูเหมือนว่ามันจะถูกต้องพอ ๆ กันที่จะสรุปว่าการเพิ่ม715827882หมายเลขไม่สามารถเท่ากับINT_MIN + 715827882 - 2! (สิ่งนี้เกิดขึ้นได้ก็ต่อเมื่อมีการเกิดเหตุการณ์ขึ้นจริง) แต่ก็ไม่ได้เพิ่มประสิทธิภาพของบรรทัดในตัวอย่างนั้น


รหัสที่ฉันใช้คือ:

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

หากstd::endl(std::cout)ถูกลบออกไปการปรับให้เหมาะสมจะไม่เกิดขึ้นอีก ในความเป็นจริงการแทนที่ด้วยstd::cout.put('\n'); std::flush(std::cout);ยังทำให้การเพิ่มประสิทธิภาพจะไม่เกิดขึ้นแม้ว่าstd::endlจะ inlined

std::endlดูเหมือนว่าการอินไลน์ของการส่งผลกระทบต่อส่วนก่อนหน้าของโครงสร้างลูป (ซึ่งฉันไม่ค่อยเข้าใจว่ามันกำลังทำอะไร แต่ฉันจะโพสต์ไว้ที่นี่ในกรณีที่คนอื่นทำ):

ด้วยรหัสเดิมและ-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

ด้วยอินไลน์ mymanual ของ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

ข้อแตกต่างระหว่างสองสิ่งนี้คือสิ่งที่%esiใช้ในต้นฉบับและ%ebxในรุ่นที่สอง มีความแตกต่างในความหมายที่กำหนดระหว่าง%esiและ%ebxโดยทั่วไป? (ฉันไม่รู้อะไรมากเกี่ยวกับชุดประกอบ x86)


มันเป็นการดีที่จะทราบว่าตรรกะของเครื่องมือเพิ่มประสิทธิภาพคืออะไรมันไม่ชัดเจนสำหรับฉันในขั้นตอนนี้ว่าทำไมบางกรณีมีการทดสอบที่ปรับให้เหมาะสมและบางอย่างไม่ได้
MM

8

อีกตัวอย่างของข้อผิดพลาดนี้ที่ถูกรายงานใน gcc คือเมื่อคุณมีลูปที่ดำเนินการสำหรับการวนซ้ำอย่างคงที่ แต่คุณกำลังใช้ตัวแปรตัวนับเป็นดัชนีในอาร์เรย์ที่มีจำนวนรายการน้อยกว่าเช่น:

int a[50], x;

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

คอมไพเลอร์สามารถระบุได้ว่าการวนซ้ำนี้จะพยายามเข้าถึงหน่วยความจำนอกอาร์เรย์ 'a' คอมไพเลอร์บ่นเกี่ยวกับสิ่งนี้ด้วยข้อความที่ค่อนข้างเป็นความลับ:

การทำซ้ำ xxu จะเรียกใช้พฤติกรรมที่ไม่ได้กำหนด [-Werror = ก้าวร้าว-loop-optimization]


ความลับยิ่งกว่าคือข้อความจะส่งเสียงเมื่อเปิดการปรับให้เหมาะสมที่สุดเท่านั้น ข้อความ M $ VB "Array out of bound" สำหรับหุ่น?
Ravi Ganesh

6

สิ่งที่ฉันไม่สามารถได้รับคือเหตุผลที่ค่า i ถูกทำลายโดยการดำเนินการโอเวอร์โฟลว์?

ดูเหมือนว่าการล้นจำนวนเต็มเกิดขึ้นในการวนซ้ำครั้งที่ 4 (สำหรับi = 3) signedจะเรียกจำนวนเต็มล้นพฤติกรรมไม่ได้กำหนด ในกรณีนี้ไม่มีอะไรสามารถทำนายได้ การวนซ้ำอาจวนซ้ำเพียง4ครั้งเดียวหรืออาจไปไม่สิ้นสุดหรืออย่างอื่น!
ผลลัพธ์อาจแตกต่างกันไปเรียบเรียงคอมไพเลอร์หรือแม้กระทั่งรุ่นที่แตกต่างกันของคอมไพเลอร์เดียวกัน

C11: 1.3.24 พฤติกรรมที่ไม่ได้กำหนด:

พฤติกรรมที่มาตรฐานสากลนี้ไม่มีข้อกำหนด
[หมายเหตุ: พฤติกรรมที่ไม่ได้กำหนดอาจเกิดขึ้นเมื่อมาตรฐานสากลนี้ละเว้นการกำหนดพฤติกรรมที่ชัดเจนหรือเมื่อโปรแกรมใช้โครงสร้างที่ผิดพลาดหรือข้อมูลที่ผิดพลาด พฤติกรรมที่ไม่ได้กำหนดอนุญาตช่วงจากการละเว้นสถานการณ์อย่างสมบูรณ์กับผลลัพธ์ที่ไม่สามารถคาดการณ์ได้ถึงพฤติกรรมในระหว่างการแปลหรือการดำเนินการโปรแกรมในลักษณะเอกสารของสภาพแวดล้อม (มีหรือไม่มีการออกข้อความวินิจฉัย) เพื่อยกเลิกการแปลหรือการดำเนินการ ของข้อความที่วินิจฉัย) การสร้างโปรแกรมที่ผิดพลาดจำนวนมากไม่ก่อให้เกิดพฤติกรรมที่ไม่ได้กำหนด พวกเขาจะต้องได้รับการวินิจฉัย - บันทึกย่อ]


@bits_international; ใช่.
haccks

4
คุณพูดถูกต้องมันยุติธรรมที่จะอธิบายว่าทำไมฉันจึงลงคะแนน ข้อมูลในคำตอบนี้ถูกต้อง แต่ไม่เป็นความรู้และไม่สนใจช้างอย่างสมบูรณ์ในห้อง: การแตกจะเกิดขึ้นในสถานที่ที่แตกต่าง (เงื่อนไขการหยุด) กว่าการปฏิบัติการที่ทำให้เกิดการล้น กลไกของการแตกหักของสิ่งต่าง ๆ ในกรณีนี้ไม่ได้อธิบายถึงแม้ว่านี่จะเป็นแกนหลักของคำถามนี้ มันเป็นสถานการณ์ของครูที่ไม่ดีโดยทั่วไปซึ่งคำตอบของครูไม่เพียง แต่ไม่ได้แก้ปัญหาหลักของปัญหาเท่านั้น ดูเหมือนว่า ...
Szabolcs

5
"ฉันเห็นว่านี่เป็นพฤติกรรมที่ไม่ได้กำหนดและจากจุดนี้ฉันไม่สนใจว่ามันจะแตกหรือไม่ทำไมมาตรฐานจึงอนุญาตให้แตกได้ไม่มีคำถามเพิ่มเติม" คุณอาจไม่ได้หมายความว่าอย่างนั้น แต่ดูเหมือนว่า ฉันหวังว่าจะเห็นทัศนคติเช่นนี้น้อยลง สิ่งนี้ไม่มีประโยชน์ในทางปฏิบัติ หากคุณได้รับการป้อนข้อมูลจากผู้ใช้ก็ไม่สมควรที่จะตรวจสอบการล้นหลังจากการดำเนินการจำนวนเต็มทุกครั้งที่ลงนามแม้ว่ามาตรฐานจะบอกว่าส่วนอื่น ๆของโปรแกรมอาจระเบิดขึ้นเพราะมัน การทำความเข้าใจว่าการหยุดพักจะช่วยหลีกเลี่ยงปัญหาเช่นนี้ได้อย่างไรในทางปฏิบัติ
Szabolcs

2
@Sababcs: มันเป็นการดีที่สุดที่จะคิดว่า C เป็นสองภาษาซึ่งหนึ่งในนั้นถูกออกแบบมาเพื่อให้คอมไพเลอร์ง่าย ๆ เพื่อให้ได้รหัสที่มีประสิทธิภาพที่สมเหตุสมผลด้วยความช่วยเหลือของโปรแกรมเมอร์ที่ใช้ประโยชน์จากโครงสร้างที่น่าเชื่อถือบนแพลตฟอร์มเป้าหมาย อื่น ๆ และถูกเพิกเฉยต่อโดยคณะกรรมการมาตรฐานและภาษาที่สองซึ่งไม่รวมโครงสร้างทั้งหมดที่มาตรฐานไม่ได้ให้การสนับสนุนเพื่อจุดประสงค์ในการอนุญาตให้คอมไพเลอร์ใช้การเพิ่มประสิทธิภาพเพิ่มเติมซึ่งอาจหรืออาจไม่เกินค่าที่คนเขียนโปรแกรมต้อง ยอมแพ้.
supercat

1
@Szabolcs " หากคุณได้รับข้อมูลเข้าจากผู้ใช้มันไม่สมควรที่จะตรวจสอบการล้นหลังจากการดำเนินการจำนวนเต็มทุกครั้งที่ลงนาม " - ถูกต้องเพราะตอนนั้นมันสายเกินไป คุณต้องตรวจสอบโอเวอร์โฟลว์ก่อนการดำเนินการจำนวนเต็มแบบลงชื่อทุกครั้ง
melpomene
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.