การเพิ่มประสิทธิภาพการโทรหางคืออะไร?


817

ง่ายมากการเพิ่มประสิทธิภาพการโทรแบบหางคืออะไร

โดยเฉพาะอย่างยิ่งมีโค้ดขนาดเล็กอะไรบ้างที่สามารถนำไปใช้และไม่ได้มีคำอธิบายว่าทำไม


10
TCO เปลี่ยนการเรียกฟังก์ชั่นในตำแหน่งหางให้เป็นข้าม
Will Ness

8
คำถามนี้ถูกถามอย่างเต็มที่ 8 ปีก่อนหน้านั้น;)
majelbstoat

คำตอบ:


755

การเพิ่มประสิทธิภาพ Tail-call เป็นที่ที่คุณสามารถหลีกเลี่ยงการจัดสรรเฟรมสแต็กใหม่สำหรับฟังก์ชั่นได้เพราะฟังก์ชั่นการโทรจะส่งกลับค่าที่ได้รับจากฟังก์ชั่นที่เรียก การใช้งานทั่วไปคือ tail-recursion ซึ่งฟังก์ชั่น recursive ที่เขียนขึ้นเพื่อใช้ประโยชน์จาก tail-call optimization สามารถใช้พื้นที่สแต็คคงที่

Scheme เป็นหนึ่งในไม่กี่ภาษาการเขียนโปรแกรมที่รับประกันในข้อกำหนดที่การใช้งานใด ๆ จะต้องให้การเพิ่มประสิทธิภาพนี้(JavaScript ยังเริ่มต้นด้วย ES6)ดังนั้นนี่คือตัวอย่างของฟังก์ชันแฟกทอเรียลใน Scheme:

(define (fact x)
  (if (= x 0) 1
      (* x (fact (- x 1)))))

(define (fact x)
  (define (fact-tail x accum)
    (if (= x 0) accum
        (fact-tail (- x 1) (* x accum))))
  (fact-tail x 1))

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

(fact 3)
(* 3 (fact 2))
(* 3 (* 2 (fact 1)))
(* 3 (* 2 (* 1 (fact 0))))
(* 3 (* 2 (* 1 1)))
(* 3 (* 2 1))
(* 3 2)
6

ในทางตรงกันข้ามการติดตามสแต็กสำหรับแฟกทอเรียลแบบเรียกซ้ำมีลักษณะดังนี้:

(fact 3)
(fact-tail 3 1)
(fact-tail 2 3)
(fact-tail 1 6)
(fact-tail 0 6)
6

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


99
หากคุณต้องการเรียนรู้เพิ่มเติมเกี่ยวกับเรื่องนี้ฉันขอแนะนำให้อ่านบทแรกของโครงสร้างและการตีความโปรแกรมคอมพิวเตอร์
Kyle Cronin

3
คำตอบที่ดีอธิบายได้อย่างสมบูรณ์แบบ
โจนาห์

15
การพูดอย่างเคร่งครัดการเพิ่มประสิทธิภาพการโทรแบบหางไม่จำเป็นต้องแทนที่กรอบสแต็กของผู้โทรด้วยการใช้โทรศัพท์แทน แต่ให้ความมั่นใจว่าจำนวนการโทรที่ไม่ จำกัด ในตำแหน่งท้ายต้องใช้พื้นที่ จำกัด เท่านั้น ดูกระดาษของ Will Clinger "การเรียกซ้ำหางที่เหมาะสมและประสิทธิภาพพื้นที่": cesura17.net/~will/Professional/Research/Papers/tail.pdf
Jon Harrop

3
นี่เป็นเพียงวิธีการเขียนฟังก์ชั่นวนซ้ำในพื้นที่คงที่หรือไม่? เพราะคุณไม่สามารถบรรลุผลลัพธ์เดียวกันโดยใช้วิธีการวนซ้ำได้หรือไม่
dclowd9901

5
@ dclowd9901, TCO ช่วยให้คุณชอบสไตล์การใช้งานมากกว่าการวนซ้ำ คุณสามารถชอบสไตล์ที่จำเป็น หลายภาษา (Java, Python) ไม่มี TCO ดังนั้นคุณต้องรู้ว่าหน่วยความจำที่ใช้งานได้มีค่าใช้จ่าย ... และควรเลือกรูปแบบที่จำเป็น
mcoolive

551

ลองมาดูตัวอย่างง่ายๆ: ฟังก์ชันแฟกทอเรียลที่ใช้ใน C

เราเริ่มต้นด้วยคำจำกัดความที่ชัดเจนซ้ำ

unsigned fac(unsigned n)
{
    if (n < 2) return 1;
    return n * fac(n - 1);
}

ฟังก์ชั่นจบลงด้วยการเรียกหางถ้าการดำเนินการครั้งสุดท้ายก่อนที่จะส่งกลับฟังก์ชั่นคือการเรียกใช้ฟังก์ชั่นอื่น ถ้าการเรียกนี้เรียกใช้ฟังก์ชันเดียวกันมันจะเรียกแบบวนซ้ำ

แม้ว่าจะfac()ดูซ้ำ ๆ ในตอนแรก แต่ก็ไม่ได้เป็นสิ่งที่เกิดขึ้นจริง

unsigned fac(unsigned n)
{
    if (n < 2) return 1;
    unsigned acc = fac(n - 1);
    return n * acc;
}

คือการดำเนินการสุดท้ายคือการคูณและไม่ใช่การเรียกใช้ฟังก์ชัน

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

unsigned fac(unsigned n)
{
    return fac_tailrec(1, n);
}

unsigned fac_tailrec(unsigned acc, unsigned n)
{
    if (n < 2) return acc;
    return fac_tailrec(n * acc, n - 1);
}

ทีนี้ทำไมมันถึงมีประโยชน์ เนื่องจากเรากลับมาทันทีหลังจากการเรียก tail เราสามารถทิ้ง stackframe ก่อนหน้านี้ก่อนที่จะเรียกใช้ฟังก์ชันในตำแหน่ง tail หรือในกรณีที่มีการเรียกใช้ฟังก์ชันแบบเรียกซ้ำ

การเพิ่มประสิทธิภาพหางเรียกแปลงรหัสซ้ำของเราเป็น

unsigned fac_tailrec(unsigned acc, unsigned n)
{
TOP:
    if (n < 2) return acc;
    acc = n * acc;
    n = n - 1;
    goto TOP;
}

สิ่งนี้สามารถสรุปได้fac()และเรามาถึงที่

unsigned fac(unsigned n)
{
    unsigned acc = 1;

TOP:
    if (n < 2) return acc;
    acc = n * acc;
    n = n - 1;
    goto TOP;
}

ซึ่งเทียบเท่ากับ

unsigned fac(unsigned n)
{
    unsigned acc = 1;

    for (; n > 1; --n)
        acc *= n;

    return acc;
}

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


คุณช่วยอธิบายว่า stackframe มีความหมายอย่างแม่นยำได้อย่างไร มีความแตกต่างระหว่าง call stack และ stackframe หรือไม่?
Shasak

10
@Kasahs: เฟรมสแต็กเป็นส่วนหนึ่งของ call stack ที่ 'เป็น' ในฟังก์ชั่นที่กำหนด (ใช้งาน); cf en.wikipedia.org/wiki/Call_stack#Structure
Christoph

1
ฉันเพิ่งมี epiphany ที่รุนแรงพอสมควรหลังจากอ่านโพสต์นี้หลังจากอ่าน2ality.com/2015/06/tail-call-optimization.html
agm1984

198

TCO (Tail Call Optimization) เป็นกระบวนการที่คอมไพเลอร์สมาร์ทสามารถโทรไปยังฟังก์ชั่นและไม่มีพื้นที่สแต็คเพิ่มเติม สถานการณ์เดียวที่เกิดเหตุการณ์นี้คือถ้าการเรียนการสอนที่ผ่านมาดำเนินการในฟังก์ชั่นFคือเรียกร้องให้ฟังก์ชัน g (หมายเหตุ: กรัมสามารถ ) กุญแจนี่คือfไม่ต้องการพื้นที่สแต็คอีกต่อไป - มันเรียกgแล้วส่งคืนสิ่งที่gจะกลับมา ในกรณีนี้การเพิ่มประสิทธิภาพสามารถทำให้ g เพียงแค่เรียกใช้และส่งคืนค่าใด ๆ ก็ตามที่มันจะต้องมีกับสิ่งที่เรียกว่า f

การเพิ่มประสิทธิภาพนี้สามารถทำการเรียกซ้ำแบบเรียกใช้พื้นที่สแต็คคงที่มากกว่าการระเบิด

ตัวอย่าง: ฟังก์ชันแฟกทอเรียลนี้ไม่ใช่ TCO ปรับขนาดได้:

def fact(n):
    if n == 0:
        return 1
    return n * fact(n-1)

ฟังก์ชันนี้ทำสิ่งต่าง ๆ นอกเหนือจากเรียกใช้ฟังก์ชันอื่นในคำสั่ง return

ฟังก์ชั่นด้านล่างนี้เป็น TCO ปรับขนาดได้:

def fact_h(n, acc):
    if n == 0:
        return acc
    return fact_h(n-1, acc*n)

def fact(n):
    return fact_h(n, 1)

เนื่องจากสิ่งสุดท้ายที่เกิดขึ้นในฟังก์ชันใด ๆ เหล่านี้คือการเรียกใช้ฟังก์ชันอื่น


3
ฟังก์ชั่น g ทั้งหมดอาจเป็น f 'ทำให้สับสนเล็กน้อย แต่ฉันเข้าใจในความหมายของคุณ ขอบคุณมาก!
majelbstoat

10
ตัวอย่างที่ยอดเยี่ยมที่แสดงแนวคิด เพียงพิจารณาว่าภาษาที่คุณเลือกต้องใช้การกำจัดการโทรแบบหางหรือการปรับการโทรแบบหางให้เหมาะสม ในตัวอย่างที่เขียนด้วย Python หากคุณป้อนค่า 1,000 คุณจะได้รับ "RuntimeError: ความลึกการเรียกซ้ำสูงสุดเกิน" เนื่องจากการใช้ Python เริ่มต้นไม่สนับสนุนการกำจัดแบบเรียกซ้ำแบบหาง ดูโพสต์จากกุยตัวเองอธิบายว่าทำไมที่: neopythonic.blogspot.pt/2009/04/tail-recursion-elimination.html
rmcc

" สถานการณ์เดียว " ค่อนข้างสมบูรณ์เกินไป นอกจากนี้ยังมีTRMCอย่างน้อยก็ในทางทฤษฎีซึ่งจะเพิ่มประสิทธิภาพ(cons a (foo b))หรือ(+ c (bar d))อยู่ในตำแหน่งหางในลักษณะเดียวกัน
Will Ness

ฉันชอบวิธี f และ g ของคุณดีกว่าคำตอบที่ยอมรับอาจเป็นเพราะฉันเป็นคนคณิตศาสตร์
Nithin

ฉันคิดว่าคุณหมายถึง TCO เพิ่มประสิทธิภาพ บอกว่าไม่ใช่ TCO ที่ปรับแต่งได้ infers ที่มันไม่สามารถปรับให้เหมาะสม (เมื่อมันสามารถ)
Jacques Mathieu

65

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

"ห่าคืออะไร: เรียกหาง"

โดย Dan Sugalski ในการเพิ่มประสิทธิภาพการโทรหางเขาเขียน:

ลองพิจารณาฟังก์ชั่นง่าย ๆ สักครู่:

sub foo (int a) {
  a += 15;
  return bar(a);
}

ดังนั้นคุณสามารถหรือผู้แปลภาษาของคุณทำอะไรได้บ้าง สิ่งที่สามารถทำได้คือเปลี่ยนรหัสของแบบฟอร์มreturn somefunc();เป็นลำดับpop stack frame; goto somefunc();ต่ำ ในตัวอย่างของเรานั่นหมายความว่าก่อนที่เราจะเรียกbar, fooทำความสะอาดตัวเองขึ้นแล้วแทนที่จะเรียกbarว่าเป็นงานย่อยเราทำระดับต่ำการดำเนินการเพื่อการเริ่มต้นของgoto ทำความสะอาดตัวเองออกจากสแต็กแล้วดังนั้นเมื่อเริ่มต้นดูเหมือนว่าใครก็ตามที่โทรมาได้เรียกจริง ๆและเมื่อส่งกลับค่าของมันมันจะส่งกลับโดยตรงไปยังผู้ที่เรียกว่ามากกว่าที่จะกลับไปที่ผู้โทรbarFoobarfoobarbarfoofoo

และการเรียกซ้ำหาง:

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

ดังนั้นสิ่งนี้:

sub foo (int a, int b) {
  if (b == 1) {
    return a;
  } else {
    return foo(a*a + a, b - 1);
  }

กลายเป็นเงียบ ๆ :

sub foo (int a, int b) {
  label:
    if (b == 1) {
      return a;
    } else {
      a = a*a + a;
      b = b - 1;
      goto label;
   }

สิ่งที่ฉันชอบเกี่ยวกับคำอธิบายนี้คือความกระชับและง่ายต่อการเข้าใจสำหรับผู้ที่มาจากพื้นหลังภาษาที่จำเป็น (C, C ++, Java)


4
ข้อผิดพลาด 404 อย่างไรก็ตามมันยังคงมีอยู่ใน archive.org: web.archive.org/web/20111030134120/http://www.sidhe.org/~dan/…
Tommy

ฉันไม่ได้รับมันไม่ได้fooเรียกฟังก์ชั่นเริ่มต้นหางที่ดีที่สุด? มันแค่เรียกฟังก์ชั่นเป็นขั้นตอนสุดท้ายแล้วมันก็แค่คืนค่านั้นใช่ไหม?
SexyBeast

1
@TryinHard อาจไม่ใช่สิ่งที่คุณมีอยู่ในใจ แต่ฉันได้อัปเดตเพื่อให้ความสำคัญกับสิ่งที่เป็นอยู่ ขออภัยจะไม่ทำซ้ำบทความทั้งหมด!
btiernay

2
ขอขอบคุณนี่ง่ายและเข้าใจได้ดีกว่าตัวอย่างโครงร่างที่ได้รับการโหวตมากที่สุด (ไม่พูดถึง Scheme ไม่ใช่ภาษาทั่วไปที่นักพัฒนาส่วนใหญ่เข้าใจ)
Sevin7 7

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

15

หมายเหตุประการแรกที่ไม่ใช่ทุกภาษารองรับ

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

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


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

ไม่จำเป็นต้องเป็นจริงในภาษาตามภาษา - คอมไพเลอร์ 64 บิต C # อาจแทรกหาง opcodes ในขณะที่รุ่น 32 บิตจะไม่; และบิลด์ F # จะปล่อยออกมา แต่การดีบัก F # จะไม่เป็นค่าเริ่มต้น
Steve Gilham

3
"TCO นำไปใช้กับการเรียกซ้ำกรณีพิเศษ" ฉันกลัวว่าผิดอย่างสมบูรณ์ โทรหางนำไปใช้กับการโทรใด ๆ ในตำแหน่งท้าย พูดคุยกันโดยทั่วไปในบริบทของการเรียกซ้ำ แต่จริงๆแล้วไม่มีอะไรเกี่ยวข้องกับการเรียกซ้ำ
Jon Harrop

@Brian ให้ดูที่ลิงก์ @btiernay ที่ให้ไว้ข้างต้น fooวิธีการเริ่มต้นหางเรียกไม่ได้รับการปรับปรุงหรือไม่
SexyBeast

13

ตัวอย่างที่เรียกใช้งานได้น้อยที่สุดของ GCC พร้อมการวิเคราะห์การแยกชิ้นส่วน x86

มาดูกันว่า GCC สามารถปรับแต่งการโทรหางให้เราโดยอัตโนมัติได้อย่างไรโดยดูที่ชุดประกอบที่สร้างขึ้น

สิ่งนี้จะทำหน้าที่เป็นตัวอย่างที่ชัดเจนของสิ่งที่ถูกกล่าวถึงในคำตอบอื่น ๆ เช่นhttps://stackoverflow.com/a/9814654/895245ที่การเพิ่มประสิทธิภาพสามารถแปลงการเรียกฟังก์ชันแบบเรียกซ้ำให้เป็นการวนซ้ำ

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

ในฐานะอินพุตเราให้แฟคทอเรียลไร้เดียงสาแบบไม่ใช้สแต็ก GCC ที่ได้รับการปรับปรุง:

tail_call.c

#include <stdio.h>
#include <stdlib.h>

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

int main(int argc, char **argv) {
    int input;
    if (argc > 1) {
        input = strtoul(argv[1], NULL, 0);
    } else {
        input = 5;
    }
    printf("%u\n", factorial(input));
    return EXIT_SUCCESS;
}

GitHub ต้นน้ำ

รวบรวมและถอดแยกชิ้นส่วน:

gcc -O1 -foptimize-sibling-calls -ggdb3 -std=c99 -Wall -Wextra -Wpedantic \
  -o tail_call.out tail_call.c
objdump -d tail_call.out

ที่-foptimize-sibling-callsเป็นชื่อของลักษณะทั่วไปของการโทรหางตามman gcc:

   -foptimize-sibling-calls
       Optimize sibling and tail recursive calls.

       Enabled at levels -O2, -O3, -Os.

ตามที่กล่าวไว้ที่: ฉันจะตรวจสอบได้อย่างไรว่า gcc กำลังปรับปรุงการเรียกซ้ำแบบหางยาว

ฉันเลือก-O1เพราะ:

  • -O0การเพิ่มประสิทธิภาพไม่ได้ทำด้วย ฉันสงสัยว่านี่เป็นเพราะไม่มีการแปลงระหว่างกลางที่จำเป็น
  • -O3 สร้างโค้ดที่มีประสิทธิภาพแบบอธรรมที่จะไม่ให้ความรู้มากนัก

ถอดชิ้นส่วนด้วย-fno-optimize-sibling-calls:

0000000000001145 <factorial>:
    1145:       89 f8                   mov    %edi,%eax
    1147:       83 ff 01                cmp    $0x1,%edi
    114a:       74 10                   je     115c <factorial+0x17>
    114c:       53                      push   %rbx
    114d:       89 fb                   mov    %edi,%ebx
    114f:       8d 7f ff                lea    -0x1(%rdi),%edi
    1152:       e8 ee ff ff ff          callq  1145 <factorial>
    1157:       0f af c3                imul   %ebx,%eax
    115a:       5b                      pop    %rbx
    115b:       c3                      retq
    115c:       c3                      retq

ด้วย-foptimize-sibling-calls:

0000000000001145 <factorial>:
    1145:       b8 01 00 00 00          mov    $0x1,%eax
    114a:       83 ff 01                cmp    $0x1,%edi
    114d:       74 0e                   je     115d <factorial+0x18>
    114f:       8d 57 ff                lea    -0x1(%rdi),%edx
    1152:       0f af c7                imul   %edi,%eax
    1155:       89 d7                   mov    %edx,%edi
    1157:       83 fa 01                cmp    $0x1,%edx
    115a:       75 f3                   jne    114f <factorial+0xa>
    115c:       c3                      retq
    115d:       89 f8                   mov    %edi,%eax
    115f:       c3                      retq

ความแตกต่างที่สำคัญระหว่างสองคือ:

  • การ-fno-optimize-sibling-callsใช้callqงานซึ่งเป็นการเรียกฟังก์ชั่นทั่วไปที่ไม่ได้รับการปรับให้เหมาะสมที่สุด

    คำสั่งนี้ส่งที่อยู่ผู้ส่งกลับไปยังสแต็กดังนั้นการเพิ่มมัน

    นอกจากนี้รุ่นนี้ยังไม่push %rbxซึ่งผลักดันให้%rbxการสแต็ค

    GCC ทำอย่างนี้เพราะร้านค้าediซึ่งเป็นฟังก์ชั่นการโต้แย้งครั้งแรก ( n) ลงแล้วโทรออกebxfactorial

    GCC ต้องการที่จะทำเช่นนี้เพราะมันมีการเตรียมการสำหรับการโทรอีกครั้งเพื่อที่จะใช้ใหม่factorialedi == n-1

    มันเลือกebxเพราะการลงทะเบียนนี้เป็น callee บันทึก: สิ่งที่ลงทะเบียนจะถูกเก็บไว้ผ่านสายงานลินุกซ์ x86-64เพื่อ subcall เพื่อจะไม่เปลี่ยนมันและการสูญเสียfactorialn

  • -foptimize-sibling-callsไม่ได้ใช้คำแนะนำใด ๆ ที่จะผลักดันไปยังกอง: มันเพียง แต่gotoกระโดดภายในfactorialคำแนะนำและjejne

    ดังนั้นเวอร์ชันนี้เทียบเท่ากับ while loop โดยไม่มีการเรียกฟังก์ชันใด ๆ การใช้สแต็กคงที่

ทดสอบใน Ubuntu 18.10, GCC 8.2


6

ดูนี่:

http://tratt.net/laurie/tech_articles/articles/tail_call_optimization

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


3
  1. เราควรตรวจสอบให้แน่ใจว่าไม่มีคำสั่ง goto ในฟังก์ชั่นนั้น .. ได้รับการดูแลโดยการเรียกใช้ฟังก์ชั่นเป็นสิ่งสุดท้ายในฟังก์ชั่น callee

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

  3. TCO อาจทำให้เกิดการทำงานตลอดไป:

    void eternity()
    {
        eternity();
    }
    

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

"TCO ไม่ใช่สาเหตุของการใช้รูปแบบการเรียกซ้ำที่ไม่ถูกต้องสำหรับโครงสร้างข้อมูล" โปรดอธิบายอย่างละเอียดว่าสิ่งนี้เกี่ยวข้องกับกรณีที่ได้รับอย่างไร ตัวอย่างข้างต้นเป็นเพียงตัวอย่างของเฟรมที่ได้รับการจัดสรรใน call stack ที่มีและไม่มี TCO
grillSandwich

คุณเลือกที่จะใช้การสอบถามซ้ำที่ไม่มีมูลเพื่อสำรวจ () นั่นไม่เกี่ยวข้องกับ TCO นิรันดร์เกิดขึ้นเป็นตำแหน่งโทรหาง แต่ไม่จำเป็นต้องวางตำแหน่งโทรหาง: โมฆะนิรันดร์ () {นิรันดร์ (); ออกจาก (); }
nomen

ในขณะที่เราอยู่ที่นี่ "การเรียกซ้ำครั้งใหญ่" คืออะไร? ทำไมเราควรหลีกเลี่ยงการข้ามไปของฟังก์ชั่น? นี่ไม่จำเป็นหรือไม่เพียงพอที่จะอนุญาตให้ TCO และค่าใช้จ่ายการเรียนการสอนอะไร? จุดรวมของ TCO คือคอมไพเลอร์แทนที่การเรียกฟังก์ชันในตำแหน่งหางโดย goto
nomen

TCO เป็นเรื่องเกี่ยวกับการเพิ่มประสิทธิภาพของพื้นที่ที่ใช้กับ call stack ด้วยการเรียกซ้ำขนาดใหญ่ฉันหมายถึงขนาดของเฟรม ทุกครั้งที่มีการเรียกซ้ำเกิดขึ้นถ้าฉันต้องการจัดสรรเฟรมขนาดใหญ่บนสแต็กการโทรเหนือฟังก์ชัน callee TCO จะมีประโยชน์มากขึ้นและทำให้ฉันมีระดับการเรียกซ้ำได้มากขึ้น แต่ในกรณีที่ขนาดเฟรมของฉันน้อยกว่าฉันสามารถทำได้โดยไม่ต้อง TCO และยังคงรันโปรแกรมของฉันได้ดี หากคุณเหลือด้วย goto ในฟังก์ชั่นการโทร "tail" ไม่ใช่การโทร tail และ TCO ไม่สามารถใช้ได้
grillSandwich

3

ฟังก์ชั่นการเรียกซ้ำมีปัญหา มันสร้าง call stack ที่มีขนาด O (n) ซึ่งทำให้หน่วยความจำทั้งหมดของเรามีค่าใช้จ่าย O (n) สิ่งนี้ทำให้เสี่ยงต่อการเกิดข้อผิดพลาดของสแต็กโอเวอร์โฟลว์ที่สแต็กการโทรมีขนาดใหญ่เกินไปและไม่มีพื้นที่เหลือ

แบบแผน tail optimization optimization (TCO) ที่ซึ่งมันสามารถปรับฟังก์ชั่นการเรียกซ้ำเพื่อหลีกเลี่ยงการสร้าง call stack สูงและดังนั้นจึงประหยัดค่าใช้จ่ายหน่วยความจำ

มีหลายภาษาที่กำลังทำ TCO เช่น (JavaScript, Rubyและ C น้อย) ในขณะที่ Python และ Java ไม่ได้ทำ TCO

ภาษา JavaScript ได้รับการยืนยันโดยใช้ :) http://2ality.com/2015/06/tail-call-optimization.html


0

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

f x = g x

f 6 ลดลงเป็น g 6 ดังนั้นหากการนำไปปฏิบัติสามารถคืนค่า g 6 เป็นผลลัพธ์แล้วเรียกการแสดงออกนั้นมันจะบันทึกกรอบสแต็ก

ด้วย

f x = if c x then g x else h x.

ลด f ถึง 6 เป็น g 6 หรือ h 6 ดังนั้นหากการดำเนินการประเมิน c 6 และพบว่าเป็นจริงแล้วมันสามารถลดลงได้

if true then g x else h x ---> g x

f x ---> h x

ล่ามการเพิ่มประสิทธิภาพที่ไม่ใช่หางแบบง่าย ๆ อาจมีลักษณะเช่นนี้

class simple_expresion
{
    ...
public:
    virtual ximple_value *DoEvaluate() const = 0;
};

class simple_value
{
    ...
};

class simple_function : public simple_expresion
{
    ...
private:
    simple_expresion *m_Function;
    simple_expresion *m_Parameter;

public:
    virtual simple_value *DoEvaluate() const
    {
        vector<simple_expresion *> parameterList;
        parameterList->push_back(m_Parameter);
        return m_Function->Call(parameterList);
    }
};

class simple_if : public simple_function
{
private:
    simple_expresion *m_Condition;
    simple_expresion *m_Positive;
    simple_expresion *m_Negative;

public:
    simple_value *DoEvaluate() const
    {
        if (m_Condition.DoEvaluate()->IsTrue())
        {
            return m_Positive.DoEvaluate();
        }
        else
        {
            return m_Negative.DoEvaluate();
        }
    }
}

ล่ามการเพิ่มประสิทธิภาพการโทรหางอาจมีลักษณะเช่นนี้

class tco_expresion
{
    ...
public:
    virtual tco_expresion *DoEvaluate() const = 0;
    virtual bool IsValue()
    {
        return false;
    }
};

class tco_value
{
    ...
public:
    virtual bool IsValue()
    {
        return true;
    }
};

class tco_function : public tco_expresion
{
    ...
private:
    tco_expresion *m_Function;
    tco_expresion *m_Parameter;

public:
    virtual tco_expression *DoEvaluate() const
    {
        vector< tco_expression *> parameterList;
        tco_expression *function = const_cast<SNI_Function *>(this);
        while (!function->IsValue())
        {
            function = function->DoCall(parameterList);
        }
        return function;
    }

    tco_expresion *DoCall(vector<tco_expresion *> &p_ParameterList)
    {
        p_ParameterList.push_back(m_Parameter);
        return m_Function;
    }
};

class tco_if : public tco_function
{
private:
    tco_expresion *m_Condition;
    tco_expresion *m_Positive;
    tco_expresion *m_Negative;

    tco_expresion *DoEvaluate() const
    {
        if (m_Condition.DoEvaluate()->IsTrue())
        {
            return m_Positive;
        }
        else
        {
            return m_Negative;
        }
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.