เหตุใด. NET / C # จึงไม่ปรับให้เหมาะสมสำหรับการเรียกซ้ำหางโทร


111

ฉันพบคำถามนี้เกี่ยวกับภาษาที่เพิ่มประสิทธิภาพการเรียกซ้ำหาง เหตุใด C # จึงไม่เพิ่มประสิทธิภาพการเรียกซ้ำหางเมื่อใดก็ตามที่ทำได้

สำหรับกรณีที่เป็นรูปธรรมเหตุใดจึงไม่ปรับวิธีนี้ให้เหมาะสมกับการวนซ้ำ ( Visual Studio 2008 32 บิตถ้าเป็นเช่นนั้น):

private static void Foo(int i)
{
    if (i == 1000000)
        return;

    if (i % 100 == 0)
        Console.WriteLine(i);

    Foo(i+1);
}

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

5
บทสนทนาที่เป็นประโยชน์โดย Jon skeet และ Scott Hanselman ใน 2016 youtu.be/H2KkiRbDZyc?t=3302
Daniel B

@RBT: ฉันคิดว่ามันแตกต่างกัน หมายถึงจำนวนการโทรซ้ำ การโทรหางเป็นเรื่องเกี่ยวกับการโทรที่ปรากฏในตำแหน่งหางกล่าวคือสิ่งสุดท้ายที่ฟังก์ชันทำดังนั้นจึงส่งคืนผลลัพธ์จากคาลลีโดยตรง
JD

คำตอบ:


84

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

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

CLRตัวเองจะเพิ่มประสิทธิภาพของการสนับสนุนสายหาง แต่คอมไพเลอร์ภาษาเฉพาะจะต้องรู้วิธีการสร้างที่เกี่ยวข้องopcodeและ JIT จะต้องยินดีที่จะเคารพมัน fsc ของ F #จะสร้าง opcodes ที่เกี่ยวข้อง (แม้ว่าสำหรับการเรียกซ้ำแบบธรรมดามันอาจเพียงแค่แปลงสิ่งทั้งหมดให้เป็นwhileลูปโดยตรง) csc ของ C # ไม่ได้

ดูรายละเอียดบางอย่างในบล็อกโพสต์นี้ (อาจจะล้าสมัยไปแล้วเนื่องจากการเปลี่ยนแปลง JIT ล่าสุด) โปรดทราบว่า CLR การเปลี่ยนแปลงสำหรับ 4.0 x 86, x 64 และ IA64 จะเคารพมัน


2
ดูโพสต์นี้ด้วย: social.msdn.microsoft.com/Forums/en-US/netfxtoolsdev/thread/…ซึ่งฉันพบว่า tail ช้ากว่าการโทรปกติ EEP!
แท่น

77

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

ขอบคุณสำหรับคำแนะนำ เราได้พิจารณาการปล่อยคำสั่งการเรียกหางตามจุดต่างๆในการพัฒนาคอมไพเลอร์ C # อย่างไรก็ตามมีปัญหาเล็กน้อยที่ผลักดันให้เราหลีกเลี่ยงปัญหานี้: 1) มีค่าใช้จ่ายที่ไม่สำคัญในการใช้คำสั่ง. tail ใน CLR (ไม่ใช่แค่คำสั่งกระโดดเท่านั้นในที่สุดการเรียกหางจะกลายเป็น ในสภาพแวดล้อมที่เข้มงวดน้อยกว่าเช่นสภาพแวดล้อมรันไทม์ภาษาที่ใช้งานได้ซึ่งการเรียกหางถูกปรับให้เหมาะสมอย่างมาก) 2) มีวิธีการ C # จริงเพียงไม่กี่วิธีที่จะถูกกฎหมายในการส่งเสียงเรียกหาง (ภาษาอื่น ๆ สนับสนุนรูปแบบการเข้ารหัสที่มีการเรียกซ้ำหางมากขึ้น และอีกมากมายที่อาศัยการเพิ่มประสิทธิภาพการโทรหางอย่างมากจะทำการเขียนซ้ำทั่วโลก (เช่นการแปลงการผ่านต่อเนื่อง) เพื่อเพิ่มจำนวนการเรียกซ้ำหาง) 3) ส่วนหนึ่งเป็นเพราะ 2) กรณีที่เมธอด C # สแต็กล้นเนื่องจากการเรียกซ้ำแบบลึกที่ควรจะประสบความสำเร็จนั้นค่อนข้างหายาก

ทั้งหมดที่กล่าวมาเราจะดูสิ่งนี้ต่อไปและในอนาคตเราอาจพบรูปแบบบางอย่างที่เหมาะสมที่จะปล่อยคำแนะนำ. tail

อย่างไรก็ตามตามที่ได้ระบุไว้เป็นที่น่าสังเกตว่าการเรียกซ้ำของหางถูกปรับให้เหมาะสมกับ x64


3
คุณอาจพบว่าสิ่งนี้มีประโยชน์เช่นกัน: weblogs.asp.net/podwysocki/archive/2008/07/07/…
Noldorin

ไม่มีปัญหาดีใจที่คุณพบว่ามีประโยชน์
Noldorin

17
ขอบคุณสำหรับการอ้างอิงเพราะตอนนี้เป็น 404!
Roman Starkov

3
ตอนนี้ลิงก์ได้รับการแก้ไขแล้ว
luksan

15

C # ไม่ได้ปรับให้เหมาะสมสำหรับการเรียกซ้ำหางเพราะนั่นคือสิ่งที่ F # มีไว้สำหรับ!

สำหรับความลึกบางอย่างเกี่ยวกับเงื่อนไขที่ป้องกันไม่ให้เรียบเรียง C # จากการปฏิบัติเพิ่มประสิทธิภาพหางเรียกดูบทความนี้: JIT CLR เงื่อนไขหางโทร

ความสามารถในการทำงานร่วมกันระหว่าง C # และ F #

C # และ F # ทำงานร่วมกันได้เป็นอย่างดีและเนื่องจาก. NET Common Language Runtime (CLR) ได้รับการออกแบบโดยคำนึงถึงความสามารถในการทำงานร่วมกันนี้ภาษาแต่ละภาษาได้รับการออกแบบด้วยการเพิ่มประสิทธิภาพที่เฉพาะเจาะจงสำหรับเจตนาและวัตถุประสงค์ ตัวอย่างที่แสดงให้เห็นวิธีที่ง่ายก็คือการเรียก F # รหัสจากรหัส C #, ดูโทร F # รหัสจากรหัส C # ; ตัวอย่างของการเรียกฟังก์ชั่น C # จาก F # รหัสให้ดูโทรฟังก์ชัน C # จาก F #

สำหรับผู้ร่วมประชุมการทำงานร่วมกันดูบทความนี้: ผู้แทนการทำงานร่วมกันระหว่าง F #, C # และ Visual Basic

ความแตกต่างทางทฤษฎีและการปฏิบัติระหว่าง C # และ F #

นี่คือบทความที่ครอบคลุมความแตกต่างบางประการและอธิบายถึงความแตกต่างของการออกแบบของการเรียกซ้ำการโทรหางระหว่าง C # และ F #: การสร้าง Tail-Call Opcode ใน C # และ F # #

นี่คือบทความพร้อมตัวอย่างบางส่วนใน C #, F # และ C ++ \ CLI: Adventures in Tail Recursion ใน C #, F # และ C ++ \ CLI

ความแตกต่างทางทฤษฎีหลักคือ C # ได้รับการออกแบบด้วยลูปในขณะที่ F # ได้รับการออกแบบตามหลักการของแคลคูลัสแลมบ์ดา สำหรับหนังสือที่ดีมากในหลักการของแลมบ์ดาแคลคูลัสดูหนังสือเล่มนี้ฟรี: โครงสร้างและการแปลความหมายของโปรแกรมคอมพิวเตอร์โดย Abelson, Sussman และ Sussman

สำหรับบทความแนะนำที่ดีมากในการโทรหาง F # ดูบทความนี้: รายละเอียดเบื้องต้นเกี่ยวกับการโทรหางใน F # สุดท้ายนี่เป็นบทความที่ครอบคลุมความแตกต่างระหว่างการเรียกซ้ำไม่ใช่หาง recursion หางโทร (ใน F #): หาง recursion เทียบกับที่ไม่ใช่หาง recursion ใน F ที่คมชัด


8

เมื่อเร็ว ๆ นี้ฉันได้รับแจ้งว่าคอมไพเลอร์ C # สำหรับ 64 บิตปรับแต่งการเรียกซ้ำหางให้เหมาะสม

C # ยังใช้สิ่งนี้ เหตุผลที่ไม่ได้ใช้เสมอคือกฎที่ใช้ในการเรียกใช้หางซ้ำนั้นเข้มงวดมาก


8
x64 กระวนกระวายใจทำสิ่งนี้ แต่คอมไพเลอร์ C # ไม่ทำ
Mark Sowul

ขอบคุณสำหรับข้อมูล. นี่คือสีขาวที่แตกต่างจากที่ฉันเคยคิดไว้ก่อนหน้านี้
Alexandre Brisebois

3
เพื่อชี้แจงความคิดเห็นทั้งสองนี้ C # ไม่เคยปล่อย opcode 'tail' ของ CIL และฉันเชื่อว่าสิ่งนี้ยังคงเป็นจริงในปี 2017 อย่างไรก็ตามสำหรับทุกภาษา opcode นั้นเป็นคำแนะนำเสมอในแง่ที่ว่าการกระวนกระวายใจตามลำดับ (x86, x64 ) จะเพิกเฉยต่อมันหากไม่เป็นไปตามเงื่อนไขต่าง ๆ (ดีไม่มีข้อผิดพลาดยกเว้นที่เป็นไปได้สแต็กล้น ) สิ่งนี้อธิบายว่าเหตุใดคุณจึงถูกบังคับให้ทำตาม "tail" ด้วย "ret" - สำหรับกรณีนี้ ในขณะเดียวกันผู้กระวนกระวายใจยังสามารถใช้การปรับให้เหมาะสมได้ฟรีเมื่อไม่มีคำนำหน้า 'หาง' ใน CIL อีกครั้งตามที่เห็นว่าเหมาะสมและไม่คำนึงถึงภาษา. NET
Glenn Slayden

3

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


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

1

ดังคำตอบอื่น ๆ ที่กล่าวถึง CLR สนับสนุนการเพิ่มประสิทธิภาพการโทรหางและดูเหมือนว่าจะอยู่ภายใต้การปรับปรุงที่ก้าวหน้าในอดีต แต่การสนับสนุนใน C # มีProposalปัญหาที่เปิดอยู่ในที่เก็บ git สำหรับการออกแบบการเขียนโปรแกรมภาษา C # Support tail recursion # 2544#

คุณสามารถดูรายละเอียดและข้อมูลที่เป็นประโยชน์ได้ที่นั่น เช่น @jaykrell กล่าวถึง

ให้ฉันให้สิ่งที่ฉันรู้

บางครั้ง tailcall เป็นผลงานที่ชนะ สามารถประหยัด CPU jmp ถูกกว่า call / ret สามารถบันทึก stack ได้ การสัมผัสกองน้อยทำให้พื้นที่ดีขึ้น

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

หากพารามิเตอร์ผู้โทรมีสแต็กมากกว่าพารามิเตอร์ callee โดยปกติจะเป็นการแปลง win-win ที่ค่อนข้างง่าย อาจมีปัจจัยต่างๆเช่นตำแหน่งพารามิเตอร์เปลี่ยนจากจัดการเป็นจำนวนเต็ม / ลอยและการสร้าง StackMaps ที่แม่นยำและอื่น ๆ

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

ให้ฉันพูดถึง (เป็นข้อมูลเพิ่มเติม) เมื่อเราสร้างแลมบ์ดาที่คอมไพล์โดยใช้คลาสนิพจน์ในSystem.Linq.Expressionsเนมสเปซมีอาร์กิวเมนต์ชื่อ 'tailCall' ซึ่งตามที่อธิบายไว้ในความคิดเห็นคือ

บูลที่ระบุว่าจะใช้การเพิ่มประสิทธิภาพการโทรหางหรือไม่เมื่อรวบรวมนิพจน์ที่สร้างขึ้น

ฉันยังไม่ได้ลองใช้และไม่แน่ใจว่าจะช่วยเกี่ยวกับคำถามของคุณได้อย่างไร แต่อาจมีคนลองใช้และอาจมีประโยชน์ในบางสถานการณ์:


var myFuncExpression = System.Linq.Expressions.Expression.Lambda<Func<  >>(body:  , tailCall: true, parameters:  );

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