คอมไพเลอร์สามารถแปลงตรรกะแบบเรียกซ้ำเป็นตรรกะแบบไม่ซ้ำแบบเรียกซ้ำได้หรือไม่


15

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

สิ่งนี้ทำให้ฉันถามว่าคอมไพเลอร์สามารถแปลงฟังก์ชั่นแบบเรียกซ้ำเป็นแบบที่ไม่ใช่แบบเรียกซ้ำได้หรือไม่


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

@ ratchet freak: การเรียกซ้ำไม่ได้หมายความว่า "การคำนวณที่ใช้สแต็ก"
Giorgio

1
@Giorgio ฉันรู้ แต่กองเป็นวิธีที่ง่ายที่สุดในการแปลง recursion ให้เป็นวง
freet ratchet

คำตอบ:


21

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

int foo(n) {
  ...
  return bar(n);
}

ที่นี่ภาษาสามารถรับรู้ว่าผลลัพธ์ที่ส่งคืนมาเป็นผลลัพธ์จากฟังก์ชั่นอื่นและเปลี่ยนการเรียกใช้ฟังก์ชันด้วยกรอบสแต็กใหม่เป็นการกระโดด

ตระหนักดีว่าวิธีการแบบแฟกทอเรียล

int factorial(n) {
  if(n == 0) return 1;
  if(n == 1) return 1;
  return n * factorial(n - 1);
}

คือไม่ optimizatable โทรหางเพราะของการตรวจสอบที่จำเป็นในการกลับมา

ในการทำให้การเรียก tail นี้สามารถปรับได้

int _fact(int n, int acc) {
    if(n == 1) return acc;
    return _fact(n - 1, acc * n);
}

int factorial(int n) {
    if(n == 0) return 1;
    return _fact(n, 1);
}

การคอมไพล์โค้ดนี้ด้วยgcc -O2 -S fact.c(-O2 จำเป็นต้องเปิดใช้งานการปรับให้เหมาะสมในคอมไพเลอร์ แต่ด้วยการเพิ่มประสิทธิภาพที่ -O3 จะทำให้มนุษย์อ่านยาก ... )

_fact:
.LFB0:
        .cfi_startproc
        cmpl    $1, %edi
        movl    %esi, %eax
        je      .L2
        .p2align 4,,10
        .p2align 3
.L4:
        imull   %edi, %eax
        subl    $1, %edi
        cmpl    $1, %edi
        jne     .L4
.L2:
        rep
        ret
        .cfi_endproc

หนึ่งสามารถดูในส่วน.L4ที่jneมากกว่าcall(ซึ่งไม่ได้โทร subroutine กับกองกรอบใหม่)

โปรดทราบว่าสิ่งนี้ทำด้วยการเพิ่มประสิทธิภาพ C. tail call ใน java นั้นยากและขึ้นอยู่กับการใช้งาน JVM - tail-recursion + javaและtail-recursion + optimizationเป็นชุดแท็กที่ดีในการเรียกดู คุณอาจพบว่าภาษา JVM อื่น ๆ สามารถปรับปรุงการเรียกซ้ำแบบหางให้ดีขึ้นได้ (ลอง clojure (ซึ่งต้องใช้การเพิ่มประสิทธิภาพการโทรซ้ำแบบหาง) หรือสกาล่า)


1
ฉันไม่แน่ใจว่านี่คือสิ่งที่ OP ขอ เพียงเพราะรันไทม์ทำหรือไม่ใช้พื้นที่สแต็คในบางวิธีไม่ได้หมายความว่าฟังก์ชั่นจะไม่เรียกซ้ำ

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

9

เหยียบอย่างระมัดระวัง

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

ฉันชอบชื่อ "การเพิ่มประสิทธิภาพการโทรหาง" แต่มีคนอื่นและบางคนจะสับสนคำ

ที่กล่าวว่ามีสองสิ่งสำคัญที่ต้องตระหนัก:

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

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


1
คุณแน่ใจหรือไม่ว่านี่คือสิ่งที่ OP ขอ ขณะที่ฉันโพสต์ภายใต้คำตอบอื่น ๆ เพียงเพราะรันไทม์ทำหรือไม่ใช้พื้นที่สแต็คในบางวิธีไม่ได้หมายความว่าฟังก์ชั่นจะไม่เกิดซ้ำ

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