คอมไพเลอร์ C ++ ใดที่มีการปรับให้เหมาะสมแบบเรียกซ้ำ


150

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

คอมไพเลอร์ C ++ มีการเพิ่มประสิทธิภาพนี้หรือไม่? ทำไม? ทำไมจะไม่ล่ะ?

ฉันจะบอกให้คอมไพเลอร์ทำอย่างไร

  • สำหรับ MSVC: /O2หรือ/Ox
  • สำหรับ GCC: -O2หรือ-O3

วิธีการตรวจสอบว่าคอมไพเลอร์ได้ทำในกรณีที่แน่นอนหรือไม่

  • สำหรับ MSVC ให้เปิดใช้งานเอาต์พุต PDB เพื่อให้สามารถติดตามรหัสจากนั้นตรวจสอบรหัส
  • สำหรับ GCC ..

ฉันยังคงแนะนำวิธีการตรวจสอบว่าฟังก์ชั่นบางอย่างได้รับการปรับให้เหมาะสมเช่นนี้โดยคอมไพเลอร์ (แม้ว่าฉันจะพบว่ามันมั่นใจว่า Konrad บอกให้ฉันสมมติมัน)

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


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

หาก destructor ใด ๆ จำเป็นต้องรันหลังจาก tail-call การปรับ tail-call นั้นไม่สามารถทำได้

คำตอบ:


129

คอมไพเลอร์หลัก ๆ ในปัจจุบันทั้งหมดทำการเพิ่มประสิทธิภาพการโทรหางได้ค่อนข้างดี (และทำมานานกว่าทศวรรษ) แม้กระทั่งสำหรับการโทรแบบเรียกซ้ำด้วยกันเช่น:

int bar(int, int);

int foo(int n, int acc) {
    return (n == 0) ? acc : bar(n - 1, acc + 2);
}

int bar(int n, int acc) {
    return (n == 0) ? acc : foo(n - 1, acc + 1);
}

การให้คอมไพเลอร์ทำการปรับให้เหมาะสมตรงไปตรงมา: เพียงแค่เปิดการปรับให้เหมาะสมสำหรับความเร็ว:

  • สำหรับ MSVC ใช้หรือ/O2/Ox
  • สำหรับ GCC, Clang และ ICC ให้ใช้ -O3

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

ในฐานะที่เป็นบันทึกทางประวัติศาสตร์ที่น่าสนใจการเพิ่มประสิทธิภาพการโทรหางสำหรับ C ได้รับการเพิ่มใน GCC ในหลักสูตรการทำวิทยานิพนธ์โดย Mark Probst วิทยานิพนธ์อธิบายคำเตือนที่น่าสนใจบางประการในการประยุกต์ใช้ มันคุ้มค่าที่จะอ่าน


ICC จะทำเช่นนั้นฉันเชื่อว่า เพื่อความรู้ที่ดีที่สุดของฉัน ICC ผลิตรหัสที่เร็วที่สุดในตลาด
พอลนาธาน

35
@ พอลคำถามคือความเร็วของรหัส ICC เกิดจากการปรับอัลกอริธึมเช่นการปรับการเรียกหางและการเกิดการแคชและการเพิ่มประสิทธิภาพ microinstruction ที่ Intel เท่านั้นที่มีความรู้อย่างใกล้ชิดเกี่ยวกับโปรเซสเซอร์ของตนเองสามารถทำได้
Imagist

6
gccมีตัวเลือกที่แคบกว่า-foptimize-sibling-callsเพื่อ "เพิ่มประสิทธิภาพการโทรซ้ำแบบพี่น้องและแบบหาง" ตัวเลือกนี้ (ตามgcc(1)หน้าคู่มือสำหรับรุ่น 4.4, 4.7 และ 4.8 กำหนดเป้าหมายแพลตฟอร์มต่างๆ) ถูกเปิดใช้งานในระดับ-O2, ,-O3 -Os
FooF

นอกจากนี้การทำงานในโหมด DEBUG โดยไม่มีการร้องขอการปรับให้เหมาะสมอย่างชัดเจนจะไม่ทำการเพิ่มประสิทธิภาพใด ๆ เลย คุณสามารถเปิดใช้งาน PDB สำหรับโหมดการวางจำหน่ายจริง EXE และลองก้าวผ่านสิ่งนั้น แต่โปรดทราบว่าการดีบักในโหมดการวางตลาดมีความซับซ้อน - ตัวแปรที่มองไม่เห็น / ปล้น, ตัวแปรที่ผสาน, ตัวแปรที่อยู่นอกขอบเขตในขอบเขตที่ไม่รู้จัก / ไม่คาดคิด กำหนดขอบเขตและกลายเป็นค่าคงที่ที่แท้จริงพร้อมด้วยที่อยู่ระดับสแต็กและ - ดี - ผสานหรือไม่มีเฟรมสแต็ก โดยปกติกรอบสแต็กที่ผสานหมายความว่ามีการแทรกอินไลน์และเฟรมที่ขาดหายไป / แบ็กสำรองอาจเป็นหางเรียก
ПетърПетров

21

GCC 4.3.2 สมบูรณ์ inlines ฟังก์ชันนี้ (เส็งเคร็ง / เล็กน้อยatoi()การดำเนินงาน) main()ลง ระดับ Optimization -O1เป็น ผมสังเกตเห็นว่าถ้าผมเล่นรอบกับมัน (แม้จะเปลี่ยนจากstaticการexternเรียกซ้ำตัวเองหางออกไปอย่างรวดเร็วดังนั้นฉันจะไม่ได้ขึ้นอยู่กับมันถูกต้องโปรแกรม

#include <stdio.h>
static int atoi(const char *str, int n)
{
    if (str == 0 || *str == 0)
        return n;
    return atoi(str+1, n*10 + *str-'0');
}
int main(int argc, char **argv)
{
    for (int i = 1; i != argc; ++i)
        printf("%s -> %d\n", argv[i], atoi(argv[i], 0));
    return 0;
}

1
คุณสามารถเปิดใช้งานการเพิ่มประสิทธิภาพเวลาลิงค์ แต่และฉันเดาว่าแม้externวิธีอาจ inline
Konrad Rudolph

5
แปลก. ฉันเพียงแค่การทดสอบ GCC 4.2.3 (x86, Slackware 12.1) และ GCC 4.6.2 (AMD64 เดดังเสียงฮืด ๆ ) และมี-O1มีไม่มีอินไลน์และไม่มีหางเพิ่มประสิทธิภาพการเรียกซ้ำ คุณต้องใช้-O2สำหรับสิ่งนั้น (เช่นกันใน 4.2.x ซึ่งค่อนข้างเก่าแล้วในตอนนี้มันจะยังไม่ถูกแทรกอยู่) BTW นอกจากนี้ยังควรเพิ่ม gcc ที่สามารถเพิ่มประสิทธิภาพการเรียกซ้ำได้แม้จะไม่ใช่หางที่เข้มงวด (เช่น factorial w / o accumulator)
przemoc

16

เช่นเดียวกับที่เห็นได้ชัด (คอมไพเลอร์จะไม่ทำการเพิ่มประสิทธิภาพแบบนี้นอกจากคุณจะขอมัน) มันมีความซับซ้อนเกี่ยวกับการเพิ่มประสิทธิภาพการโทรหางใน C ++: destructors

รับบางสิ่งเช่น:

   int fn(int j, int i)
   {
      if (i <= 0) return j;
      Funky cls(j,i);
      return fn(j, i-1);
   }

คอมไพเลอร์ไม่สามารถ (โดยทั่วไป) การเรียกแบบ tail-optimization ปรับได้เพราะมันต้องเรียก destructor ของcls หลังจากที่การเรียกซ้ำเกิดขึ้น

บางครั้งคอมไพเลอร์สามารถเห็นว่า destructor ไม่มีผลข้างเคียงที่มองเห็นจากภายนอก (ดังนั้นจึงสามารถทำได้ แต่เช้า) แต่บ่อยครั้งที่มันไม่สามารถทำได้

รูปแบบทั่วไปของสิ่งนี้คือที่Funkyจริงstd::vectorหรือคล้ายกัน


ใช้งานไม่ได้สำหรับฉัน ระบบบอกฉันว่าการลงคะแนนของฉันถูกล็อคจนกว่าคำตอบจะได้รับการแก้ไข
hmuelner

เพิ่งแก้ไขคำตอบ (ลบ parantheses) และตอนนี้ฉันสามารถยกเลิก downvote ของฉันได้
hmuelner

11

คอมไพเลอร์ส่วนใหญ่จะไม่ทำการปรับแต่งใด ๆ ในโครงสร้างการดีบัก

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


2
ลอง gcc ทำไม -g -O3 และรับ opimizations ในบิลด์ debug xlC มีพฤติกรรมเดียวกัน
g24l

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

7

ในฐานะที่เกร็กกล่าวถึงคอมไพเลอร์จะไม่ทำในโหมดดีบัก มันก็โอเคสำหรับ debug builds ที่จะช้ากว่า prod build แต่มันไม่ควรจะพังบ่อยกว่า: และถ้าคุณขึ้นอยู่กับ tail optimization optimization พวกมันอาจทำอย่างนั้น ด้วยเหตุนี้จึงเป็นการดีที่สุดที่จะเขียน tail call เป็นวนซ้ำปกติ :-(

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