ฉันจะสร้างลูปที่ไม่มีที่สิ้นสุดที่ไม่ได้รับการปรับให้เหมาะสมได้อย่างไร


131

มาตรฐาน C11 ดูเหมือนจะบ่งบอกว่างบการวนซ้ำที่มีการควบคุมการแสดงออกอย่างต่อเนื่องไม่ควรปรับให้เหมาะสม ฉันรับคำแนะนำจากคำตอบนี้ซึ่งอ้างอิงส่วน 6.8.5 จากร่างมาตรฐานโดยเฉพาะ:

คำสั่งวนซ้ำซึ่งนิพจน์การควบคุมไม่ใช่นิพจน์คงที่ ... อาจถูกสันนิษฐานโดยการนำไปใช้เพื่อยุติ

ในคำตอบนั้นระบุว่าการวนซ้ำเช่นwhile(1) ;นั้นไม่ควรมีการปรับให้เหมาะสม

ดังนั้น ... ทำไม Clang / LLVM เพิ่มประสิทธิภาพลูปด้านล่าง (รวบรวมด้วยcc -O2 -std=c11 test.c -o test)?

#include <stdio.h>

static void die() {
    while(1)
        ;
}

int main() {
    printf("begin\n");
    die();
    printf("unreachable\n");
}

บนเครื่องของฉันสิ่งนี้จะพิมพ์ออกมาbeginและเกิดปัญหากับคำสั่งที่ผิดกฎหมาย ( ud2กับดักวางไว้die()) เมื่อวันที่ godboltputsเราจะเห็นว่าไม่มีอะไรที่จะถูกสร้างขึ้นหลังจากการเรียกร้องให้

มันเป็นงานที่ยากอย่างน่าประหลาดใจที่ทำให้ Clang แสดงผลวนลูปไม่สิ้นสุดภายใต้-O2- ในขณะที่ฉันสามารถทดสอบvolatileตัวแปรซ้ำได้ซึ่งเกี่ยวข้องกับการอ่านหน่วยความจำที่ฉันไม่ต้องการ และถ้าฉันทำอะไรแบบนี้:

#include <stdio.h>

static void die() {
    while(1)
        ;
}

int main() {
    printf("begin\n");
    volatile int x = 1;
    if(x)
        die();
    printf("unreachable\n");
}

... เสียงดังกราวพิมพ์beginตามด้วยunreachableราวกับว่าวงวนไม่สิ้นสุด

คุณจะทำให้ Clang ส่งออกลูปไม่สิ้นสุดที่ไม่ต้องเข้าถึงหน่วยความจำได้อย่างเหมาะสมโดยเปิดการปรับให้เหมาะสมได้อย่างไร


3
ความคิดเห็นไม่ได้มีไว้สำหรับการอภิปรายเพิ่มเติม การสนทนานี้ได้รับการย้ายไปแชท
Bhargav Rao

2
ไม่มีโซลูชันแบบพกพาที่ไม่เกี่ยวข้องกับผลข้างเคียง หากคุณไม่ต้องการเข้าถึงหน่วยความจำความหวังที่ดีที่สุดของคุณคือการลงทะเบียนตัวละครที่ไม่ได้ลงนามแบบระเหย แต่การลงทะเบียนหายไปใน C ++ 17
Scott M

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

1
@ Cruncher: ผลกระทบของความพยายามในการเรียกใช้โปรแกรมใด ๆ อาจเป็นประโยชน์ไร้ประโยชน์อย่างยิ่งหรือเลวร้ายยิ่งกว่าไร้ประโยชน์ การประมวลผลที่ส่งผลให้โปรแกรมติดค้างอยู่ในลูปที่ไม่มีที่สิ้นสุดอาจไร้ประโยชน์ แต่ก็ยังดีกว่าพฤติกรรมอื่น ๆ ที่คอมไพเลอร์อาจแทนที่
supercat

6
@Cruncher: เนื่องจากรหัสอาจจะทำงานในบริบทอิสระที่มีแนวคิดไม่exit()และเพราะรหัสอาจได้ค้นพบสถานการณ์ที่มันไม่สามารถรับประกันได้ว่าผลกระทบของการดำเนินการอย่างต่อเนื่องจะไม่เลวร้ายยิ่งกว่าที่ไร้ประโยชน์ การกระโดดข้ามห่วงตนเองเป็นวิธีการจัดการกับสถานการณ์ที่น่ากลัว แต่มันอาจเป็นวิธีที่ดีที่สุดในการจัดการกับสถานการณ์ที่เลวร้าย
supercat

คำตอบ:


77

มาตรฐาน C11 บอกสิ่งนี้ 6.8.5 / 6:

คำสั่งการวนซ้ำซึ่งนิพจน์การควบคุมไม่ใช่นิพจน์คงที่156)ที่ไม่มีการดำเนินการอินพุต / เอาท์พุตไม่สามารถเข้าถึงวัตถุที่ระเหยได้และไม่ทำการซิงโครไนซ์หรือการดำเนินการอะตอมมิกในร่างกายการควบคุมนิพจน์หรือ (ในกรณี คำสั่ง) expression-3 อาจถูกสันนิษฐานโดยการนำไปใช้เพื่อยกเลิก 157)

บันทึกย่อแบบสองเท้าไม่ได้เป็นบรรทัดฐาน แต่ให้ข้อมูลที่เป็นประโยชน์:

156) นิพจน์ควบคุมที่ถูกละเว้นถูกแทนที่ด้วยค่าคงที่ที่ไม่ใช่ศูนย์ซึ่งเป็นนิพจน์ค่าคงที่

157) สิ่งนี้มีวัตถุประสงค์เพื่อให้การแปลงคอมไพเลอร์เช่นการลบลูปที่ว่างเปล่าแม้ว่าจะไม่สามารถพิสูจน์ได้ว่าการเลิกจ้าง

ในกรณีของคุณwhile(1)เป็นนิพจน์คงที่ใสดังนั้นอาจไม่สามารถสันนิษฐานโดยการนำไปใช้เพื่อยุติ การดำเนินการดังกล่าวจะถูกทำลายอย่างสิ้นหวังเนื่องจากลูป "ตลอดกาล" เป็นโครงสร้างโปรแกรมทั่วไป

จะเกิดอะไรขึ้นกับ "โค้ดที่ไม่สามารถเข้าถึงได้" หลังจากลูปเป็นสิ่งที่ฉันรู้ว่าไม่ได้กำหนดไว้อย่างดี อย่างไรก็ตามเสียงดังกราวทำหน้าที่แปลกมาก เปรียบเทียบรหัสเครื่องกับ gcc (x86):

gcc 9.2 -O3 -std=c11 -pedantic-errors

.LC0:
        .string "begin"
main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:.LC0
        call    puts
.L2:
        jmp     .L2

เสียงดังกราว 9.0.0 -O3 -std=c11 -pedantic-errors

main:                                   # @main
        push    rax
        mov     edi, offset .Lstr
        call    puts
.Lstr:
        .asciz  "begin"

gcc สร้างลูปเสียงดังกราวเพียงวิ่งเข้าไปในป่าและออกด้วยข้อผิดพลาด 255

ฉันเอนตัวไปทางนี้เป็นพฤติกรรมที่ไม่สอดคล้องของเสียงดังกราว เพราะฉันพยายามที่จะขยายตัวอย่างของคุณเพิ่มเติมเช่นนี้:

#include <stdio.h>
#include <setjmp.h>

static _Noreturn void die() {
    while(1)
        ;
}

int main(void) {
    jmp_buf buf;
    _Bool first = !setjmp(buf);

    printf("begin\n");
    if(first)
    {
      die();
      longjmp(buf, 1);
    }
    printf("unreachable\n");
}

ฉันเพิ่ม C11 _Noreturnในความพยายามที่จะช่วยให้คอมไพเลอร์เพิ่มเติมพร้อม ควรชัดเจนว่าฟังก์ชั่นนี้จะวางสายจากคำหลักนั้นเพียงอย่างเดียว

setjmpจะคืนค่า 0 เมื่อดำเนินการครั้งแรกดังนั้นโปรแกรมนี้ควรชนไปwhile(1)และหยุดอยู่ที่นั่นเพียงพิมพ์ "เริ่มต้น" (สมมติว่า \ n ล้างข้อมูล stdout) สิ่งนี้เกิดขึ้นกับ gcc

หากการวนซ้ำถูกลบออกไปก็ควรพิมพ์ "เริ่มต้น" 2 ครั้งจากนั้นพิมพ์ "ไม่สามารถเข้าถึงได้" อย่างไรก็ตามในเสียงดังกราว ( godbolt ) มันพิมพ์ "เริ่มต้น" 1 ครั้งแล้ว "ไม่สามารถเข้าถึงได้" ก่อนที่จะส่งคืนรหัสทางออก 0 นั่นเป็นความผิดปกติธรรมดาไม่ว่าคุณจะใส่มันอย่างไร

ฉันไม่พบกรณีที่อ้างว่ามีพฤติกรรมที่ไม่ได้กำหนดไว้ที่นี่ดังนั้นสิ่งที่ฉันใช้คือว่านี่เป็นข้อผิดพลาดในเสียงดังกราว พฤติกรรมนี้ทำให้เสียงดังกราว 100% ไร้ประโยชน์สำหรับโปรแกรมเช่นระบบฝังตัวซึ่งคุณจะต้องพึ่งพาลูปนิรันดร์ที่แขวนโปรแกรมไว้ (ในขณะที่รอคอยสุนัขเฝ้าบ้าน ฯลฯ )


15
ผมไม่เห็นด้วยกับ"คริสตัลแสดงออกคงชัดเจนนี้จึงอาจไม่ได้รับการสันนิษฐานโดยการดำเนินการเพื่อยุติ" นี้จริงๆได้รับเป็นภาษาจู้จี้จุกจิก lawyering แต่6.8.5/6เป็นในรูปแบบของif (เหล่านี้) แล้วคุณอาจคิด (นี้) แต่นั่นไม่ได้หมายความว่าหากไม่ได้ (เหล่านี้) คุณอาจไม่คิด (นี้) มันเป็นข้อกำหนดเฉพาะเมื่อตรงตามเงื่อนไขไม่ใช่เมื่อไม่สงบซึ่งคุณสามารถทำสิ่งที่คุณต้องการภายในมาตรฐาน และถ้าไม่มีสิ่งที่น่าสังเกต ...
kabanus

7
@kabanus ส่วนที่ยกมาเป็นกรณีพิเศษ หากไม่ใช่ (กรณีพิเศษ) ให้ประเมินและจัดลำดับรหัสตามปกติ หากคุณอ่านบทเดิมต่อไปนิพจน์การควบคุมจะถูกประเมินตามที่ระบุไว้สำหรับแต่ละคำสั่งการวนซ้ำ ("ตามที่ระบุโดยความหมาย") ด้วยข้อยกเว้นของกรณีพิเศษที่ยกมา มันเป็นไปตามกฎเดียวกันกับการประเมินผลการคำนวณมูลค่าใด ๆ ซึ่งเป็นลำดับและกำหนดไว้อย่างดี
Lundin

2
ฉันเห็นด้วย แต่คุณจะไม่ประหลาดใจที่int z=3; int y=2; int x=1; printf("%d %d\n", x, z);ไม่มี2ในการชุมนุมดังนั้นในความรู้สึกไร้ประโยชน์ที่ว่างเปล่าxไม่ได้รับมอบหมายหลังจากyแต่หลังจากzเนื่องจากการเพิ่มประสิทธิภาพ ดังนั้นจากประโยคสุดท้ายของคุณเราปฏิบัติตามกฎปกติสมมติว่าขณะที่หยุด (เพราะเราไม่ได้ จำกัด อะไรให้ดีกว่านี้) และเหลือไว้ในขั้นตอนสุดท้าย ตอนนี้เราปรับให้เหมาะสมกับคำแถลงที่ไร้ประโยชน์ (เพราะเราไม่รู้อะไรเลย)
kabanus

2
@Malters หนึ่งในความคิดเห็นของฉันถูกลบ แต่ขอบคุณสำหรับการป้อนข้อมูล - และฉันเห็นด้วย สิ่งที่ความคิดเห็นของฉันพูดคือฉันคิดว่านี่เป็นหัวใจสำคัญของการอภิปราย - while(1);เหมือนกับ int y = 2;ข้อความในแง่ของสิ่งที่เราได้รับอนุญาตให้เพิ่มประสิทธิภาพความหมายแม้ว่าตรรกะของพวกเขายังคงอยู่ในแหล่งที่มา จาก n1528 ฉันอยู่ภายใต้ความประทับใจที่พวกเขาอาจจะเหมือนกัน แต่เนื่องจากผู้คนมีประสบการณ์มากกว่าฉันกำลังโต้เถียงในทางอื่นและมันเป็นข้อผิดพลาดอย่างเป็นทางการอย่างเห็นได้ชัดแล้วนอกเหนือจากการถกเถียงทางปรัชญาว่าถ้อยคำในมาตรฐานนั้นชัดเจนหรือไม่ อาร์กิวเมนต์มีการแสดงผลที่สงสัย
kabanus

2
“ การใช้งานดังกล่าวจะถูกทำลายอย่างสิ้นหวังเนื่องจากลูป 'ตลอดกาล' เป็นโครงสร้างโปรแกรมทั่วไป” - ฉันเข้าใจความรู้สึก แต่ข้อโต้แย้งนั้นมีข้อบกพร่องเพราะสามารถนำไปใช้กับ C ++ ได้เหมือนกัน แต่คอมไพเลอร์ C ++ ที่ปรับให้เหมาะกับการวนซ้ำนี้จะไม่แตก แต่สอดคล้อง
Konrad Rudolph

52

คุณต้องใส่นิพจน์ที่อาจทำให้เกิดผลข้างเคียง

ทางออกที่ง่ายที่สุด:

static void die() {
    while(1)
       __asm("");
}

ลิงค์ Godbolt


21
ไม่ได้อธิบายว่าทำไมเสียงดังกราวจึงแสดงออกมา
Lundin

4
เพียงแค่พูดว่า "มันเป็นข้อบกพร่องในเสียงดังกราว" ก็เพียงพอแล้ว ฉันต้องการลองบางสิ่งที่นี่ก่อนแม้ว่าฉันจะตะโกน "บั๊ก"
Lundin

3
@Lundin ฉันไม่ทราบว่ามันเป็นข้อผิดพลาด มาตรฐานไม่ถูกต้องทางเทคนิคในกรณีนี้
P__J__

4
โชคดีที่ GCC เป็นโอเพ่นซอร์สและฉันสามารถเขียนคอมไพเลอร์ที่ปรับตัวอย่างของคุณให้เหมาะสม และฉันสามารถทำได้สำหรับตัวอย่างใด ๆ ที่คุณคิดขึ้นมาในตอนนี้และในอนาคต
โทมัสเวลเลอร์

3
@ThomasWeller: GCC devs ไม่ยอมรับแพทช์ที่ปรับการวนซ้ำให้เหมาะสม มันจะละเมิดพฤติกรรมการรับประกัน = documented ดูความคิดเห็นก่อนหน้าฉัน: asm("")ปริยายasm volatile("");และทำให้คำสั่ง asm ต้องเรียกใช้หลาย ๆ ครั้งเช่นเดียวกับในเครื่องนามธรรมgcc.gnu.org/onlinedocs/gcc/Basic-Asm.html (โปรดทราบว่ามันไม่ปลอดภัยสำหรับผลข้างเคียงที่จะรวมหน่วยความจำหรือการลงทะเบียนใด ๆ คุณต้อง Extended asm ด้วย"memory"clobber ถ้าคุณต้องการอ่านหรือเขียนหน่วยความจำที่คุณเคยเข้าถึงจาก C. Basic asm นั้นปลอดภัยสำหรับสิ่งที่ชอบasm("mfence")หรือcliเท่านั้น)
Peter Cordes

50

คำตอบอื่น ๆ ที่กล่าวถึงวิธีที่ทำให้ Clang ปล่อยวงวนไม่สิ้นสุดด้วยภาษาแอสเซมบลีแบบอินไลน์หรือผลข้างเคียงอื่น ๆ ฉันแค่ต้องการยืนยันว่านี่เป็นข้อผิดพลาดคอมไพเลอร์ โดยเฉพาะมันเป็นข้อผิดพลาด LLVM ที่ยาวนานมานาน - ใช้แนวคิด C ++ ของ "ลูปทั้งหมดที่ไม่มีผลข้างเคียงต้องยุติ" กับภาษาที่ไม่ควรเช่น C

ตัวอย่างเช่นภาษาโปรแกรมมิง Rustยังอนุญาตให้วนซ้ำไม่สิ้นสุดและใช้ LLVM เป็นแบ็คเอนด์และมีปัญหาเดียวกันนี้

ในระยะสั้นปรากฏว่า LLVM จะยังคงสมมติว่า "ลูปทั้งหมดที่ไม่มีผลข้างเคียงต้องยุติ" สำหรับภาษาใด ๆ ที่อนุญาตให้llvm.sideeffectวนซ้ำไม่สิ้นสุด LLVM คาดว่าส่วนหน้าเพื่อแทรกopcode ลงในลูปดังกล่าว นี่คือสิ่งที่ Rust กำลังวางแผนที่จะทำดังนั้นเสียงดังกราว (เมื่อรวบรวมรหัส C) อาจจะต้องทำเช่นนั้นด้วย


5
ไม่มีอะไรที่เหมือนกับกลิ่นของบั๊กที่เก่ากว่าทศวรรษ ... ด้วยการแก้ไขและแพทช์ที่เสนอหลายรายการ ... แต่ยังไม่ได้รับการแก้ไข
เอียนเคมพ์

4
@IanKemp: สำหรับพวกเขาที่จะแก้ไขข้อผิดพลาดตอนนี้จะต้องยอมรับว่าพวกเขาใช้เวลาสิบปีในการแก้ไขข้อผิดพลาด ดีกว่าที่จะระงับความหวังว่ามาตรฐานจะเปลี่ยนเพื่อปรับพฤติกรรมของพวกเขา แน่นอนแม้ว่ามาตรฐานจะเปลี่ยนไป แต่ก็ยังคงไม่แสดงให้เห็นถึงพฤติกรรมของพวกเขายกเว้นในสายตาของคนที่จะถือว่าการเปลี่ยนแปลงมาตรฐานเป็นข้อบ่งชี้ว่าอาณัติพฤติกรรมก่อนหน้าของมาตรฐานนั้นเป็นข้อบกพร่องที่ควรได้รับการแก้ไขย้อนหลัง
supercat

4
มันได้รับการ "คงที่" ในแง่ที่ LLVM เพิ่มsideeffectop (ในปี 2017) และคาดว่า front-end จะแทรก op นั้นเข้าไปในลูปตามดุลยพินิจของพวกเขา LLVM ต้องเลือกบางส่วนเริ่มต้นสำหรับลูปและมันเกิดขึ้นที่จะเลือกหนึ่งที่สอดคล้องกับพฤติกรรม c ++ 's จงใจหรือมิฉะนั้น แน่นอนว่ายังมีงานเพิ่มประสิทธิภาพที่เหลืออยู่ที่ต้องทำเช่นการรวมsideeffectops ต่อเนื่องเข้าด้วยกัน (นี่คือสิ่งที่ปิดกั้น Front-end ของ Rust จากการใช้งาน) ดังนั้นบนพื้นฐานนั้นข้อผิดพลาดอยู่ใน front-end (เสียงดังกราว) ที่ไม่ได้แทรก op ในลูป
Arnavion

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

การสนทนานั้นน่าจะเป็นของ LLVM / clang mailing list FWIW the LLVM ยอมรับว่าการเพิ่ม op นั้นยังสอนการเพิ่มประสิทธิภาพหลายอย่างเกี่ยวกับมัน นอกจากนี้ Rust ยังทดลองด้วยการแทรกsideeffectops ไปยังจุดเริ่มต้นของทุกฟังก์ชั่นและไม่เห็นการถดถอยประสิทธิภาพรันไทม์ ปัญหาเดียวคือการรวบรวมเวลาถดถอยเห็นได้ชัดเนื่องจากขาดฟิวชั่นของ ops ติดต่อกันเหมือนที่ฉันกล่าวถึงในความคิดเห็นก่อนหน้าของฉัน
Arnavion

32

นี่คือข้อผิดพลาดของเสียงดังกราว

... เมื่ออินไลน์ฟังก์ชันที่มีการวนซ้ำไม่สิ้นสุด พฤติกรรมจะแตกต่างกันเมื่อwhile(1);ปรากฏในหลักโดยตรงซึ่งมีกลิ่นมากสำหรับฉัน

ดูคำตอบของสรุปและลิงก์ของ @ Arnavion ส่วนที่เหลือของคำตอบนี้ถูกเขียนขึ้นก่อนที่ฉันจะยืนยันว่ามันเป็นข้อผิดพลาด


ในการตอบคำถามชื่อ: ฉันจะทำให้วนที่ไม่มีที่สิ้นสุดที่จะไม่ได้รับการเพิ่มประสิทธิภาพได้อย่างไร ? -
สร้างdie()แมโครไม่ใช่ฟังก์ชันเพื่อแก้ไขข้อบกพร่องนี้ใน Clang 3.9 และใหม่กว่า (รุ่นก่อนหน้านี้อย่างใดอย่างหนึ่งดังกราวช่วยให้วงหรือส่งเสียงcallไปยังรุ่นที่ไม่ใช่แบบอินไลน์ของฟังก์ชั่นที่มีห่วงอนันต์.) ที่ปรากฏจะปลอดภัยแม้ว่าprint;while(1);print;inlines ฟังก์ชั่นเข้ามาของผู้โทร ( Godbolt ) -std=gnu11vs. -std=gnu99ไม่เปลี่ยนแปลงอะไรเลย

หากคุณสนใจ GNU C เพียงอย่างเดียวP__J ____asm__("");ในวงก็สามารถใช้ได้เช่นกันและไม่ควรทำให้โค้ดที่ล้อมรอบเหมาะสมสำหรับคอมไพเลอร์ใด ๆ ที่เข้าใจ คำสั่ง asm GNU C Basic นั้นมีความหมายโดยนัยvolatileดังนั้นสิ่งนี้จึงนับเป็นผลข้างเคียงที่มองเห็นได้ซึ่งต้อง "เรียกใช้" หลาย ๆ ครั้งตามที่มันเป็นในเครื่องนามธรรม C (และใช่เสียงดังกร่างใช้ภาษา GNU ของ C ตามที่จัดทำโดยคู่มือ GCC)


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

(นั่นจะเป็นไปตามมาตรฐานสำหรับ Clang ++ (แต่ยังไม่มีประโยชน์มาก); ลูปที่ไม่มีที่สิ้นสุดโดยไม่มีผลข้างเคียงใด ๆ คือ UB ใน C ++ แต่ไม่ใช่ C
คือขณะที่ (1) พฤติกรรมที่ไม่ได้กำหนดใน C? UB ทำให้คอมไพเลอร์ปล่อย สำหรับโค้ดบนเส้นทางของการดำเนินการที่จะพบ UB แน่นอนasmคำสั่งในลูปจะหลีกเลี่ยง UB นี้สำหรับ C ++ แต่ในทางปฏิบัติ Clang การคอมไพล์เป็น C ++ จะไม่ลบลูปที่ไม่มีที่สิ้นสุดของนิพจน์ยกเว้นเมื่ออินไลน์เหมือนกับ รวบรวมเป็น C. )


การแทรกแบบอินไลน์ด้วยตนเองwhile(1);เปลี่ยนวิธีการรวบรวมเสียงดังกังวาน: วงวนไม่สิ้นสุดแสดงเป็น asm นี่คือสิ่งที่เราคาดหวังจาก POV นักกฎหมาย

#include <stdio.h>
int main() {
    printf("begin\n");
    while(1);
    //infloop_nonconst(1);
    //infloop();
    printf("unreachable\n");
}

บนตัวรวบรวมคอมไพเลอร์ Godbolt Clang 9.0 -O3 คอมไพล์เป็น C ( -xc) สำหรับ x86-64:

main:                                   # @main
        push    rax                       # re-align the stack by 16
        mov     edi, offset .Lstr         # non-PIE executable can use 32-bit absolute addresses
        call    puts
.LBB3_1:                                # =>This Inner Loop Header: Depth=1
        jmp     .LBB3_1                   # infinite loop


.section .rodata
 ...
.Lstr:
        .asciz  "begin"

คอมไพเลอร์เดียวกันที่มีตัวเลือกเดียวกันจะคอมไพล์ a mainที่เรียกinfloop() { while(1); }ไปยังอันดับแรกputsแต่จากนั้นก็หยุดการเปล่งคำสั่งmainหลังจากนั้น ดังนั้นอย่างที่ฉันได้กล่าวไว้การประมวลผลจะตกจากจุดสิ้นสุดของฟังก์ชั่นไปยังฟังก์ชันใด ๆ ก็ตามที่อยู่ถัดไป (แต่ด้วยสแต็กที่ไม่ถูกต้องสำหรับรายการฟังก์ชันดังนั้นมันจึงไม่ใช่ tailcall ที่ใช้ได้)

ตัวเลือกที่ถูกต้องจะเป็น

  • ปล่อยlabel: jmp labelลูปไม่สิ้นสุด
  • หรือ (ถ้าเรายอมรับว่าสามารถลบลูปไม่สิ้นสุด) ปล่อยสายอื่นเพื่อพิมพ์สตริงที่ 2 และreturn 0จากmainนั้น

การหยุดทำงานหรือดำเนินการต่อโดยไม่พิมพ์ "ไม่สามารถเข้าถึง" นั้นไม่ชัดเจนสำหรับการใช้งาน C11 เว้นแต่ว่ามี UB ที่ฉันไม่ได้สังเกตเห็น


เชิงอรรถ 1:

สำหรับบันทึกฉันเห็นด้วยกับคำตอบของ @ Lundin ซึ่งอ้างอิงมาตรฐานสำหรับหลักฐานว่า C11 ไม่อนุญาตให้มีการยุติการวนซ้ำแบบไม่ จำกัด แม้ในขณะที่ว่างเปล่า (ไม่มี I / O, ระเหย, การซิงโครไนซ์หรืออื่น ๆ ผลข้างเคียงที่มองเห็นได้)

นี่เป็นชุดของเงื่อนไขที่จะอนุญาตให้รวบรวมลูปเป็นลูป asm ที่ว่างเปล่าสำหรับ CPU ปกติ (แม้ว่าเนื้อความจะไม่ว่างเปล่าในแหล่งที่มาการมอบหมายให้ตัวแปรไม่สามารถมองเห็นได้ในเธรดหรือตัวจัดการสัญญาณอื่นที่ไม่มี data-race UB ในขณะที่ลูปทำงานอยู่ดังนั้นการดำเนินการตามมาตรฐานจึงสามารถลบลูปเนื้อความดังกล่าวได้ถ้าต้องการ เป็นแล้วจากนั้นคำถามจะบอกว่าสามารถลบลูปเองได้หรือไม่ ISO C11 บอกว่าไม่ชัดเจน)

เนื่องจาก C11 แยกออกมาเป็นกรณีที่การใช้งานไม่สามารถถือว่าการวนซ้ำสิ้นสุดลง (และไม่ใช่ UB) ดูเหมือนว่าพวกเขาตั้งใจที่จะนำเสนอการวนรอบในเวลาทำงาน การนำไปใช้งานที่กำหนดเป้าหมาย CPUs ด้วยโมเดลการดำเนินการที่ไม่สามารถทำงานได้อย่างไม่มีขีด จำกัด ในเวลาที่ จำกัด นั้นไม่มีเหตุผลที่จะลบลูปอนันต์คงที่ว่างเปล่า หรือโดยทั่วไปแล้วถ้อยคำที่แน่นอนนั้นเกี่ยวกับว่าพวกเขาสามารถ "สันนิษฐานว่าจะยุติ" หรือไม่ หากลูปไม่สามารถยุติได้นั่นหมายความว่าโค้ดในภายหลังนั้นไม่สามารถเข้าถึงได้ไม่ว่าคุณจะโต้แย้งอะไรเกี่ยวกับคณิตศาสตร์และอินฟินิตี้และต้องใช้เวลานานเท่าใดในการทำงานจำนวนมหาศาลบนเครื่องจักรสมมุติ

นอกจากนั้นเสียงดังกังวานไม่เพียง แต่เป็น DeathStation 9000 ที่เป็นไปตามมาตรฐาน ISO C เท่านั้น แต่ตั้งใจให้เป็นประโยชน์สำหรับการเขียนโปรแกรมระบบระดับต่ำในโลกแห่งความเป็นจริงรวมถึงเมล็ดและสิ่งฝังตัว ดังนั้นไม่ว่าหรือไม่คุณยอมรับข้อโต้แย้งเกี่ยวกับ C11 ที่ช่วยให้การกำจัดของwhile(1);มันไม่ได้ทำให้รู้สึกว่าเสียงดังกราวอยากจะทำจริงว่า ถ้าคุณเขียนwhile(1);นั่นอาจไม่ใช่อุบัติเหตุ การกำจัดลูปที่สิ้นสุดโดยไม่ตั้งใจโดยอุบัติเหตุ (ด้วยนิพจน์ตัวแปรควบคุมรันไทม์) อาจเป็นประโยชน์และมันก็สมเหตุสมผลสำหรับคอมไพเลอร์ในการทำเช่นนั้น

เป็นเรื่องยากที่คุณจะต้องหมุนจนกว่าจะมีการขัดจังหวะครั้งต่อไป แต่ถ้าคุณเขียนลงใน C นั่นคือสิ่งที่คุณคาดหวัง (และจะเกิดอะไรขึ้นใน GCC และ Clang ยกเว้น Clang เมื่อวงวนไม่สิ้นสุดอยู่ในฟังก์ชัน wrapper)

ตัวอย่างเช่นในเคอร์เนลระบบปฏิบัติการดั้งเดิมเมื่อตัวกำหนดตารางเวลาไม่มีภารกิจในการรันมันอาจรันภารกิจว่าง while(1);การดำเนินแรกของการที่อาจจะมี

หรือสำหรับฮาร์ดแวร์ที่ไม่มีคุณสมบัติการประหยัดพลังงานซึ่งอาจเป็นการใช้งานเพียงอย่างเดียว (จนถึงต้นยุค 2000 นั่นคือฉันคิดว่าไม่หายากใน x86 แม้ว่าhltคำสั่งจะมีอยู่จริง IDK ถ้ามันช่วยประหยัดพลังงานได้อย่างมีความหมายจนกระทั่ง CPU เริ่มมีสถานะไม่ใช้พลังงานต่ำ)


1
ไม่มีใครอยากรู้จริงๆมีใครใช้เสียงดังกราวสำหรับระบบฝังตัวหรือไม่? ฉันไม่เคยเห็นมันมาก่อนและทำงานกับฝังตัว GCC เท่านั้น "เมื่อเร็ว ๆ นี้" (10 ปีก่อน) -ffreestanding -fno-strict-aliasingเข้ามาในตลาดฝังตัวและฉันจะใช้อย่างไม่เชื่อว่าหนึ่งควรมีการเพิ่มประสิทธิภาพต่ำและเสมอกับ มันใช้งานได้ดีกับ ARM และอาจใช้กับ AVR รุ่นเก่า
Lundin

1
@Lundin: IDK เกี่ยวกับการฝังตัว แต่ใช่คนสร้างเมล็ดด้วยเสียงดังกราวอย่างน้อยบางครั้ง Linux สันนิษฐานว่าดาร์วินสำหรับ MacOS
Peter Cordes

2
bugs.llvm.org/show_bug.cgi?id=965ข้อผิดพลาดนี้มีความเกี่ยวข้อง แต่ฉันไม่แน่ใจว่าเป็นสิ่งที่เราเห็นที่นี่
bracco23

1
@lundin - ฉันค่อนข้างมั่นใจว่าเราใช้ GCC (และชุดเครื่องมืออื่น ๆ อีกมากมาย) สำหรับงานฝังตลอด 90's ด้วย RTOS เช่น VxWorks และ PSOS ฉันไม่เข้าใจว่าทำไมคุณถึงบอกว่า GCC เพิ่งเข้าสู่ตลาดแบบฝังตัวเมื่อเร็ว ๆ นี้
Jeff Learman

1
@ JeffLearman กลายเป็นกระแสหลักเมื่อเร็ว ๆ นี้? อย่างไรก็ตามความล้มเหลวของนามแฝงที่เข้มงวดของ gcc เกิดขึ้นหลังจากการเปิดตัว C99 และรุ่นใหม่กว่าของมันดูเหมือนจะไม่ไปกล้วยอีกต่อไปเมื่อเผชิญหน้ากับการละเมิดนามแฝงที่เข้มงวดเช่นกัน แต่ถึงกระนั้นฉันยังคงสงสัยเมื่อใดก็ตามที่ฉันใช้มัน ในส่วนของเสียงดังกราวด์รุ่นล่าสุดนั้นแตกหักอย่างชัดเจนเมื่อพูดถึงลูปนิรันดร์ดังนั้นจึงไม่สามารถใช้กับระบบฝังตัวได้
Lundin

14

เพียงเพื่อบันทึกเสียงดังกราวก็ซุกซนกับgoto:

static void die() {
nasty:
    goto nasty;
}

int main() {
    int x; printf("begin\n");
    die();
    printf("unreachable\n");
}

มันก่อให้เกิดผลลัพธ์เช่นเดียวกับในคำถามคือ:

main: # @main
  push rax
  mov edi, offset .Lstr
  call puts
.Lstr:
  .asciz "begin"

ฉันเห็นไม่เห็นวิธีใด ๆ ในการอ่านตามที่ได้รับอนุญาตใน C11 ซึ่งพูดเพียง:

6.8.6.1 (2) gotoคำสั่งทำให้การข้ามแบบไม่มีเงื่อนไขไปยังคำสั่งนำหน้าด้วยเลเบลที่ระบุชื่อในฟังก์ชันล้อมรอบ

เช่น gotoไม่ได้เป็น "คำสั่งซ้ำ" (6.8.5 รายการwhile, doและfor) ไม่มีอะไรพิเศษเกี่ยวกับ "การเลิกจ้าง-สันนิษฐานว่า" หวานหูนำไปใช้ แต่คุณต้องการที่จะอ่านพวกเขา

ต่อคอมไพเลอร์ลิงก์คำถามของ Godbolt คือ x86-64 Clang 9.0.0 และแฟล็กคือ -g -o output.s -mllvm --x86-asm-syntax=intel -S --gcc-toolchain=/opt/compiler-explorer/gcc-9.2.0 -fcolor-diagnostics -fno-crash-diagnostics -O2 -std=c11 example.c

กับคนอื่น ๆ เช่น x86-64 GCC 9.2 คุณจะได้รับความสมบูรณ์แบบ:

.LC0:
  .string "begin"
main:
  sub rsp, 8
  mov edi, OFFSET FLAT:.LC0
  call puts
.L2:
  jmp .L2

ธง: -g -o output.s -masm=intel -S -fdiagnostics-color=always -O2 -std=c11 example.c


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

2
@supercat ขอบคุณสำหรับความคิดเห็น ... ทำไมเกินขีด จำกัด การแปลทำอะไรนอกเหนือจากการล้มเหลวในขั้นตอนการแปลและปฏิเสธที่จะดำเนินการ? นอกจากนี้: " 5.1.1.3 การวินิจฉัยการใช้งานที่สอดคล้องจะทำให้ ... ข้อความการวินิจฉัย ... หากหน่วยการแปลหรือหน่วยการแปลที่ประมวลผลล่วงหน้ามีการละเมิดกฎไวยากรณ์หรือข้อ จำกัด ... " ฉันไม่เห็นว่าพฤติกรรมที่ผิดพลาดในขั้นตอนการปฏิบัติสามารถสอดคล้องกันได้อย่างไร
jonathanjo

มาตรฐานจะเป็นไปไม่ได้อย่างสมบูรณ์ในการนำไปใช้หากข้อ จำกัด ของการดำเนินการทั้งหมดได้รับการแก้ไขในเวลาที่สร้างเนื่องจากสามารถเขียนโปรแกรมการปรับแก้อย่างเคร่งครัดซึ่งจะต้องใช้จำนวนไบต์ของสแต็กมากกว่าอะตอมในจักรวาล มันไม่ชัดเจนว่าข้อ จำกัด รันไทม์ควรจะมี "ข้อ จำกัด การแปล" แต่สัมปทานดังกล่าวมีความจำเป็นอย่างชัดเจนและไม่มีหมวดหมู่อื่น ๆ ที่สามารถนำมาใส่ได้
supercat

1
ฉันตอบความคิดเห็นของคุณเกี่ยวกับ "ขีด จำกัด การแปล" แน่นอนว่ายังมีข้อ จำกัด ในการดำเนินการฉันยอมรับว่าฉันไม่เข้าใจว่าทำไมคุณถึงแนะนำว่าควรมีข้อ จำกัด ด้านการแปลหรือทำไมคุณถึงบอกว่าจำเป็น ฉันไม่เห็นเหตุผลว่าคำพูดnasty: goto nastyสามารถสอดคล้องและไม่หมุน CPU จนกว่าผู้ใช้หรือทรัพยากรหมดแรง
jonathanjo

1
มาตรฐานไม่ได้อ้างอิงถึง "ขีด จำกัด การดำเนินการ" ที่ฉันสามารถหาได้ สิ่งที่ต้องการฟังก์ชั่นการทำรังโทรมักจะมีการจัดการโดยกองจัดสรร แต่การดำเนินงานที่สอดคล้องว่าข้อ จำกัด การเรียกฟังก์ชันที่ความลึก 16 สามารถสร้าง 16 สำเนาของทุกฟังก์ชั่นและมีการโทรไปยังbar()ภายในfoo()ได้รับการประมวลผลเป็นสายจาก__1fooการ__2barจาก__2fooไป__3bar, ฯลฯ และจาก__16fooถึง__launch_nasal_demonsซึ่งจะอนุญาตให้วัตถุอัตโนมัติทั้งหมดได้รับการจัดสรรแบบคงที่และจะทำให้สิ่งที่มักจะจำกัด "เวลาทำงาน" ลงในขีด จำกัด การแปล
supercat

5

ฉันจะเล่นทนายของปีศาจและยืนยันว่ามาตรฐานไม่ได้ห้ามนักแปลอย่างชัดเจนจากการหาวงที่ไม่สิ้นสุด

คำสั่งวนซ้ำที่นิพจน์การควบคุมไม่ใช่นิพจน์คงที่ 156) ที่ไม่มีการดำเนินการอินพุต / เอาท์พุตไม่สามารถเข้าถึงวัตถุที่ระเหยได้และไม่ทำการซิงโครไนซ์หรือการดำเนินการอะตอมมิกในร่างกายการควบคุมนิพจน์หรือ (ในกรณีของ คำสั่ง) expression-3 ของมันอาจถูกสันนิษฐานโดยการนำไปใช้เพื่อยุติการใช้ 158)

ลองแยกวิเคราะห์กัน คำสั่งการทำซ้ำที่ตรงกับเกณฑ์บางอย่างอาจถูกสมมติให้ยุติ:

if (satisfiesCriteriaForTerminatingEh(a_loop)) 
    if (whatever_reason_or_just_because_you_feel_like_it)
         assumeTerminates(a_loop);

สิ่งนี้ไม่ได้พูดอะไรเกี่ยวกับสิ่งที่เกิดขึ้นหากเกณฑ์ไม่เป็นที่พอใจและสมมติว่าลูปอาจยุติแม้จะไม่ถูกห้ามอย่างชัดเจนตราบใดที่มีการปฏิบัติตามกฎอื่น ๆ ของมาตรฐาน

do { } while(0) หรือ while(0){}ตามหลังคำสั่งวนซ้ำทั้งหมด (ลูป) ที่ไม่เป็นไปตามเกณฑ์ที่อนุญาตให้คอมไพเลอร์เพิ่งสันนิษฐานในสิ่งที่พวกเขายุติและยังเห็นได้ชัดว่าพวกเขาจะยุติ

แต่คอมไพเลอร์สามารถเพิ่มประสิทธิภาพได้while(1){}หรือไม่

5.1.2.3p4พูดว่า:

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

สิ่งนี้กล่าวถึงการแสดงออกไม่ใช่ข้อความดังนั้นจึงไม่ใช่เรื่องน่าเชื่อ 100% แต่แน่นอนว่าสามารถเรียกได้เช่น:

void loop(void){ loop(); }

int main()
{
    loop();
}

ที่จะข้ามไป ที่น่าสนใจเสียงดังกราวไม่ข้ามมันและ GCC ไม่


"สิ่งนี้ไม่ได้พูดอะไรเกี่ยวกับสิ่งที่เกิดขึ้นหากเกณฑ์ไม่เป็นที่พอใจ" แต่มันก็เป็นเช่นนั้น 6.8.5.1 ประโยคคำสั่ง while: "การประเมินการควบคุมนิพจน์เกิดขึ้นก่อนการประมวลผลของลูปแต่ละครั้ง" แค่นั้นแหละ. นี่คือการคำนวณมูลค่า (ของนิพจน์คงที่) ซึ่งอยู่ภายใต้กฎของเครื่องนามธรรม 5.1.2.3 ที่กำหนดการประเมินผลคำว่า: "การประเมินการแสดงออกโดยทั่วไปรวมถึงการคำนวณมูลค่าและการเริ่มต้นของผลข้างเคียง" และในบทเดียวกันการประเมินทั้งหมดดังกล่าวจะถูกจัดลำดับและประเมินผลตามความหมายที่ระบุไว้
Lundin

1
@Lundin ดังนั้นwhile(1){}ลำดับที่ไม่มีที่สิ้นสุดของ1การประเมินที่เกี่ยวพันกับ{}การประเมิน แต่มันอยู่ที่ไหนในมาตรฐานที่บอกว่าการประเมินเหล่านั้นจำเป็นต้องใช้เวลาที่ไม่ใช่ศูนย์ ? พฤติกรรม gcc มีประโยชน์มากขึ้นฉันเดาว่าเพราะคุณไม่จำเป็นต้องมีลูกเล่นที่เกี่ยวข้องกับการเข้าถึงหน่วยความจำหรือลูกเล่นนอกภาษา แต่ฉันไม่เชื่อว่ามาตรฐานห้ามการเพิ่มประสิทธิภาพนี้ในเสียงดังกราว หากการทำให้ไม่สามารถwhile(1){}ปรับเปลี่ยนได้นั้นเป็นความตั้งใจมาตรฐานควรมีความชัดเจนเกี่ยวกับมันและการวนซ้ำไม่สิ้นสุดควรถูกระบุว่าเป็นผลข้างเคียงที่สังเกตได้ใน 5.1.2.3p2
PSkocik

1
ฉันคิดว่ามันมีการระบุไว้ถ้าคุณถือว่า1เงื่อนไขเป็นการคำนวณค่า เวลาในการดำเนินการไม่สำคัญ - สิ่งสำคัญคือสิ่งที่while(A){} B;อาจไม่ได้รับการปรับให้เหมาะสมทั้งหมดไม่ได้รับการปรับให้เหมาะสมB;และไม่ถูกจัดลำดับB; while(A){}ใหม่ เพื่ออ้างถึงเครื่องนามธรรม C11 ให้ความสำคัญกับฉัน: "การปรากฏตัวของจุดลำดับระหว่างการประเมินผลของนิพจน์ A และ B หมายความว่าทุกการคำนวณค่าและผลข้างเคียงที่เกี่ยวข้องกับ A นั้นถูกจัดลำดับก่อนทุกการคำนวณค่าและผลข้างเคียงที่เกี่ยวข้องกับ B " ค่าของAใช้ชัดเจน (โดยลูป)
Lundin

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

1
ใกล้"การเพิ่มประสิทธิภาพลูปไม่มีที่สิ้นสุด" : มันไม่ชัดเจนเลยว่า"มัน"หมายถึงมาตรฐานหรือคอมไพเลอร์ - อาจจะใช้ถ้อยคำใหม่? รับ"แม้ว่ามันอาจจะ"และไม่"แม้ว่ามันอาจจะไม่ควร"มันอาจเป็นมาตรฐานที่"มัน"หมายถึง
Peter Mortensen

2

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


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

พฤติกรรมนั้นแปลกมาก - มันลบการพิมพ์ขั้นสุดท้ายดังนั้น "เห็น" ลูปที่ไม่มีที่สิ้นสุด แต่จากนั้นก็กำจัดลูปด้วยเช่นกัน

มันแย่ยิ่งกว่าที่ฉันบอกได้ การลบอินไลน์ที่เราได้รับ:

die: # @die
.LBB0_1: # =>This Inner Loop Header: Depth=1
  jmp .LBB0_1
main: # @main
  push rax
  mov edi, offset .Lstr
  call puts
.Lstr:
  .asciz "begin"

ดังนั้นฟังก์ชั่นจึงถูกสร้างขึ้นและการโทรออกจะปรับให้เหมาะสม นี่คือความยืดหยุ่นที่มากกว่าที่คาดไว้:

#include <stdio.h>

void die(int x) {
    while(x);
}

int main() {
    printf("begin\n");
    die(1);
    printf("unreachable\n");
}

ส่งผลให้เกิดการชุมนุมที่ไม่เหมาะสมที่สุดสำหรับฟังก์ชั่น แต่การเรียกใช้ฟังก์ชั่นจะได้รับการปรับให้เหมาะสมอีกครั้ง! ยิ่งเลวร้ายลง:

void die(x) {
    while(x++);
}

int main() {
    printf("begin\n");
    die(1);
    printf("unreachable\n");
}

ฉันทำการทดสอบอื่น ๆ ด้วยการเพิ่มตัวแปรท้องถิ่นและเพิ่มขึ้นผ่านตัวชี้การใช้gotoฯลฯ ... ณ จุดนี้ฉันจะยอมแพ้ หากคุณต้องใช้เสียงดังกราว

static void die() {
    int volatile x = 1;
    while(x);
}

ทำงาน มันดูดที่เพิ่มประสิทธิภาพ (ชัด) printfและใบในรอบสุดท้ายที่ซ้ำซ้อน อย่างน้อยโปรแกรมก็ไม่หยุด อาจ GCC หลังจากทั้งหมดหรือไม่

ภาคผนวก

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

Heck n1528มีพฤติกรรมแบบไม่ได้กำหนดถ้าฉันอ่านถูกต้อง เฉพาะ

ปัญหาสำคัญของการทำเช่นนี้คือมันอนุญาตให้โค้ดเคลื่อนที่ข้ามลูปที่อาจไม่สิ้นสุด

จากที่นี่ฉันคิดว่ามันสามารถมีส่วนร่วมในการอภิปรายในสิ่งที่เราต้องการ (คาดหวัง?) มากกว่าสิ่งที่ได้รับอนุญาต


ความคิดเห็นไม่ได้มีไว้สำหรับการอภิปรายเพิ่มเติม การสนทนานี้ได้รับการย้ายไปแชท
Bhargav Rao

Re "plain all bug" : คุณหมายถึง" plain old bug"หรือไม่
Peter Mortensen

@PeterMortensen "ole" ก็โอเคกับฉันเช่นกัน
kabanus

2

ดูเหมือนว่านี่เป็นข้อผิดพลาดในคอมไพเลอร์เสียงดังกราว หากไม่มีการบังคับใด ๆ ในdie()ฟังก์ชั่นที่จะเป็นฟังก์ชั่นคงทำไปด้วยstaticและทำให้มันinline:

#include <stdio.h>

inline void die(void) {
    while(1)
        ;
}

int main(void) {
    printf("begin\n");
    die();
    printf("unreachable\n");
}

มันทำงานได้ตามที่คาดไว้เมื่อรวบรวมกับ Clang compiler และพกพาได้เช่นกัน

Compiler Explorer (godbolt.org) - เสียงดังกราว 9.0.0-O3 -std=c11 -pedantic-errors

main:                                   # @main
        push    rax
        mov     edi, offset .Lstr
        call    puts
.LBB0_1:                                # =>This Inner Loop Header: Depth=1
        jmp     .LBB0_1
.Lstr:
        .asciz  "begin"

เกี่ยวกับstatic inlineอะไร
SS Anne

1

ดูเหมือนว่าต่อไปนี้จะทำงานสำหรับฉัน:

#include <stdio.h>

__attribute__ ((optnone))
static void die(void) {
    while (1) ;
}

int main(void) {
    printf("begin\n");
    die();
    printf("unreachable\n");
}

ที่godbolt

บอก Clang อย่างชัดแจ้งว่าไม่ควรปรับให้เหมาะสมว่าฟังก์ชั่นหนึ่งทำให้เกิดการวนซ้ำไม่สิ้นสุดตามที่คาดไว้ หวังว่าจะมีวิธีในการปิดใช้งานการเพิ่มประสิทธิภาพบางอย่างโดยเลือกแทนที่จะปิดพวกเขาทั้งหมดแบบนั้น เสียงดังกังวานยังคงปฏิเสธที่จะปล่อยรหัสที่สองprintfแม้ว่า ในการบังคับให้ทำเช่นนั้นฉันต้องแก้ไขโค้ดภายในmainเป็น:

volatile int x = 0;
if (x == 0)
    die();

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


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

เอกสาร GCC __attribute__ ((optimize(1)))แต่ละเว้นเสียงดังกราวว่ามันเป็นไม่สนับสนุน: godbolt.org/z/4ba2HM gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html
Peter Cordes

0

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

ฉันคิดว่าความตั้งใจของมาตรฐานค่อนข้างชัดเจนว่าผู้รวบรวมไม่ควรคิดว่าการwhile(1) {}วนซ้ำที่ไม่มีผลข้างเคียงหรือbreakข้อความจะสิ้นสุดลง ตรงกันข้ามกับสิ่งที่บางคนอาจคิดว่าผู้เขียนมาตรฐานไม่ได้เชิญนักเขียนคอมไพเลอร์ให้โง่หรือป้าน การใช้งานที่สอดคล้องกันอาจมีประโยชน์ในการตัดสินใจที่จะยุติโปรแกรมใด ๆ ซึ่งหากไม่ขัดจังหวะดำเนินการคำสั่งฟรีผลข้างเคียงมากกว่าที่มีอะตอมในจักรวาล แต่การดำเนินการที่มีคุณภาพไม่ควรดำเนินการดังกล่าวบนพื้นฐานของสมมติฐาน การเลิกจ้าง แต่อยู่บนพื้นฐานที่การทำเช่นนั้นอาจมีประโยชน์และจะไม่เลวร้ายไปกว่าไร้ประโยชน์


-2

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

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

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


ความคิดเห็นไม่ได้มีไว้สำหรับการอภิปรายเพิ่มเติม การสนทนานี้ได้รับการย้ายไปแชท
Samuel Liew

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

@pipe: ฉันคิดว่าผู้ดูแลของ clang และ gcc หวังว่าเวอร์ชันมาตรฐานในอนาคตจะทำให้พฤติกรรมของคอมไพเลอร์ของพวกเขาได้รับอนุญาตและผู้ดูแลของคอมไพเลอร์เหล่านั้นจะสามารถแสร้งว่าการเปลี่ยนแปลงนั้นเป็นเพียงการแก้ไขข้อบกพร่องที่ยาวนาน ในมาตรฐาน นั่นเป็นวิธีที่พวกเขาปฏิบัติต่อการรับประกันการเริ่มต้นสามัญของ C89 เช่น
supercat

@SSAnne: อืม ... ฉันคิดว่ามันไม่เพียงพอที่จะปิดกั้นการอ้างถึง gcc และเสียงดังกังวานที่ไม่ถูกต้องจากผลลัพธ์ของการเปรียบเทียบความเท่าเทียมกันของตัวชี้
supercat

@supercat มี <s> อื่น ๆ </s> ตันอยู่
SS Anne

-2

ฉันขอโทษถ้านี่ไม่ใช่กรณีที่ไร้สาระฉันก็สะดุดกับโพสต์นี้และฉันรู้เพราะปีของฉันใช้ Gentoo Linux distro ว่าถ้าคุณต้องการให้คอมไพเลอร์ไม่ปรับรหัสของคุณคุณควรใช้ -O0 (ศูนย์) ฉันอยากรู้เกี่ยวกับมันและรวบรวมและเรียกใช้รหัสข้างต้นและวงจะไปอย่างไม่มีกำหนด รวบรวมโดยใช้เสียงดังกราว -9:

cc -O0 -std=c11 test.c -o test

1
จุดคือการทำให้วนรอบไม่สิ้นสุดด้วยการเปิดใช้งานการเพิ่มประสิทธิภาพ
SS Anne

-4

การwhileวนซ้ำที่ว่างเปล่าไม่มีผลข้างเคียงใด ๆ ต่อระบบ

ดังนั้นเสียงดังกราวจึงลบออก มีวิธีที่ "ดีกว่า" เพื่อให้บรรลุพฤติกรรมตามที่ตั้งใจซึ่งบังคับให้คุณต้องชัดเจนยิ่งขึ้นจากความตั้งใจของคุณ

while(1); คือ baaadd


6
ในโครงสร้างที่ฝังอยู่จำนวนมากมีแนวคิดของการไม่มีหรือabort() exit()หากสถานการณ์เกิดขึ้นที่ฟังก์ชั่นกำหนดว่า (อาจจะเป็นผลมาจากการทุจริตหน่วยความจำ) while(1);ยังคงดำเนินการจะเลวร้ายยิ่งกว่าอันตรายพฤติกรรมเริ่มต้นที่พบบ่อยสำหรับห้องสมุดฝังคือการเรียกใช้ฟังก์ชั่นที่จะดำเนินการ มันอาจจะมีประโยชน์สำหรับคอมไพเลอร์ที่มีตัวเลือกเพื่อทดแทนพฤติกรรมที่มีประโยชน์มากขึ้นแต่ผู้เขียนคอมไพเลอร์ใด ๆ ที่ไม่สามารถคิดออกว่าจะรักษาโครงสร้างที่เรียบง่ายเช่นเป็นอุปสรรคในการดำเนินการโปรแกรมต่อไป
supercat

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

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

... "รอ N + 1 วินาทีแล้วจึงดำเนินการอื่น" ดังนั้นความจริงที่ว่าชุดของการกระทำที่ยอมรับได้นอกเหนือจากการรอนั้นว่างเปล่าจะไม่สามารถสังเกตได้ ในทางกลับกันหากชิ้นส่วนของรหัสลบการกระทำบางอย่างที่ทนไม่ได้ออกจากชุดของการกระทำที่เป็นไปได้และหนึ่งในการกระทำเหล่านั้นได้รับการดำเนินการต่อไปนั้นควรได้รับการพิจารณาที่สังเกตได้ น่าเสียดายที่กฎภาษา C และ C ++ ใช้คำว่า "สมมติ" ในลักษณะแปลก ๆ ซึ่งแตกต่างจากสาขาอื่นของตรรกะหรือความพยายามของมนุษย์ที่ฉันสามารถระบุได้
supercat

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