ความซับซ้อนของเวลาของคอมไพเลอร์


54

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

ฉันเดาว่าจะมีขั้นตอนการเพิ่มประสิทธิภาพบางอย่างที่อธิบายแทนได้ แต่มีผลกระทบเพียงเล็กน้อยต่อเวลาจริง เช่นเลขชี้กำลังเป็นเลขชี้กำลังเป็นอาร์กิวเมนต์ของฟังก์ชัน

จากด้านบนของหัวฉันจะบอกว่าการสร้างต้นไม้ AST จะเป็นเส้นตรง รุ่น IR จะต้องก้าวผ่านต้นไม้ในขณะที่มองค่าในตารางที่เคยเติบโตดังนั้นหรือlog) การสร้างรหัสและการเชื่อมโยงจะเป็นการดำเนินการที่คล้ายกัน ดังนั้นฉันเดาว่าจะเป็นถ้าเราลบเลขยกกำลังของตัวแปรที่ไม่เติบโตตามจริงO ( n log n ) O ( n 2 )O(n2)O(nlogn)O(n2)

ฉันอาจผิดอย่างสมบูรณ์แม้ว่า ไม่มีใครมีความคิดเกี่ยวกับมัน?


7
คุณจะต้องระมัดระวังเมื่อคุณอ้างว่าอะไรคือ "ชี้แจง", "เส้นตรง"หรือn) อย่างน้อยสำหรับฉันมันไม่ชัดเจนเลยว่าคุณจะวัดการป้อนข้อมูลของคุณอย่างไร (เลขชี้กำลังในอะไรอะไรถึงอะไร)O ( n บันทึกn ) nO(n2)O(nlogn)n
Juho

2
เมื่อคุณพูด LLVM คุณหมายถึง Clang หรือเปล่า LLVM เป็นโครงการขนาดใหญ่ที่มีโครงการย่อยคอมไพเลอร์ที่แตกต่างกันหลายโครงการดังนั้นจึงค่อนข้างคลุมเครือ
Nate CK

5
สำหรับ C # อย่างน้อยเป็นเลขชี้กำลังสำหรับปัญหากรณีที่เลวร้ายที่สุด (คุณสามารถเข้ารหัสปัญหา SAT NP ที่สมบูรณ์ใน C #) นี่ไม่ใช่เพียงการเพิ่มประสิทธิภาพ แต่จำเป็นสำหรับการเลือกฟังก์ชันที่เกินพิกัดที่ถูกต้อง สำหรับภาษาอย่าง C ++ มันจะไม่สามารถตัดสินใจได้เนื่องจากเทมเพลตจะสมบูรณ์
CodesInChaos

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

3
ความละเอียดเกินพิกัด C # นั้นค่อนข้างยุ่งยากเมื่อคุณรวมการโอเวอร์โหลดจำนวนมากเข้ากับการแสดงออกแลมบ์ดา คุณสามารถใช้มันเพื่อเข้ารหัสสูตรบูลีนในลักษณะที่กำหนดว่ามีการโอเวอร์โหลดที่เกี่ยวข้องหรือไม่จำเป็นต้องใช้ปัญหา 3SAT ของ NP-complete ในการรวบรวมปัญหาจริงผู้รวบรวมต้องค้นหาวิธีแก้ปัญหาสำหรับสูตรนั้นซึ่งอาจจะยากกว่านี้ Eric Lippert พูดถึงรายละเอียดในบล็อกโพสต์ของเขาในแลมบ์ดานิพจน์กับวิธีไม่ระบุตัวตน, ตอนที่ห้า
CodesInChaos

คำตอบ:


50

หนังสือที่ดีที่สุดที่จะตอบคำถามของคุณอาจเป็น: Cooper และ Torczon, "Engineering a Compiler," 2003. หากคุณมีสิทธิ์เข้าใช้ห้องสมุดมหาวิทยาลัยคุณควรยืมสำเนา

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

lexer เป็นเครื่องสถานะ จำกัด ดังนั้นในขนาดของอินพุต (เป็นตัวอักษร) และสร้างกระแสของโทเค็นที่ส่งผ่านไปยังตัวแยกวิเคราะห์O ( n )O(n)O(n)

สำหรับคอมไพเลอร์หลายภาษาสำหรับหลาย ๆ ภาษาตัวแยกวิเคราะห์คือ LALR (1) และประมวลผลสตรีมโทเค็นในเวลาในจำนวนโทเค็นอินพุต ในระหว่างการแยกวิเคราะห์คุณมักจะต้องติดตามตารางสัญลักษณ์ แต่สำหรับหลายภาษาที่สามารถจัดการได้ด้วยตารางแฮช ("พจนานุกรม") การเข้าถึงพจนานุกรมแต่ละครั้งคือแต่บางครั้งคุณอาจต้องเดินผ่านสแต็กเพื่อค้นหาสัญลักษณ์ ความลึกของสแต็กคือโดยที่คือความลึกในการซ้อนของขอบเขต (ในภาษา C-like มีวงเล็บปีกกากี่ชั้นที่อยู่ข้างใน)O ( 1 ) O ( s ) sO(n)O(1)O(s)s

จากนั้นต้นไม้แยกวิเคราะห์จะ "แบน" ลงในกราฟควบคุมการไหล โหนดของกราฟการควบคุมการไหลอาจเป็นคำแนะนำ 3 ที่อยู่ (คล้ายกับภาษาแอสเซมบลี RISC) และขนาดของกราฟการควบคุมการไหลโดยทั่วไปจะเป็นแบบเชิงเส้นในขนาดของแผนภูมิการแยก

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

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

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

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


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

4
โอ้อีกอย่างหนึ่ง: ในปัจจุบันมีระบบการใช้งานบางประเภทที่ซับซ้อนมากในทางทฤษฎี การอนุมานของ Hindley-Milner เป็นที่ทราบกันดีว่า DEXPTIME-complete และภาษาที่คล้ายกับ ML ต้องดำเนินการอย่างถูกต้อง อย่างไรก็ตามเวลาทำงานเป็นเชิงเส้นในทางปฏิบัติเพราะก) กรณีทางพยาธิวิทยาไม่เคยเกิดขึ้นในโปรแกรมในโลกแห่งความเป็นจริงและข) โปรแกรมเมอร์ในโลกแห่งความเป็นจริงมักจะใส่คำอธิบายประกอบประเภทหากเพียงเพื่อให้ได้รับข้อความแสดงข้อผิดพลาดที่ดีขึ้น
นามแฝง

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

@ นามแฝงความจริงที่ว่าหลายครั้งคอมไพเลอร์จะต้องแก้ปัญหาการหยุดชะงัก (หรือปัญหาที่น่ารังเกียจ NP ยากมาก) เป็นหนึ่งในเหตุผลที่มาตรฐานให้นักเขียนคอมไพเลอร์ leeway ในสมมติว่าพฤติกรรมที่ไม่ได้กำหนดจะไม่เกิดขึ้น )
vonbrand

15

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


3
Haskell's (พร้อมส่วนขยาย) และระบบประเภทของ Scala ก็มีทัวริงสมบูรณ์ซึ่งหมายความว่าการตรวจสอบประเภทอาจใช้เวลาไม่ จำกัด Scala ตอนนี้มีมาโครทัวริงที่สมบูรณ์อยู่ด้านบน
Jörg W Mittag

5

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

งานการแก้ปัญหาการโอเวอร์โหลดทั่วไปใน C # นั้นเป็นที่รู้จักกันว่า NP-hard (และความซับซ้อนของการใช้งานจริงนั้นมีความซับซ้อนอย่างน้อยเป็นเลขชี้กำลัง)

การประมวลผลความคิดเห็นเอกสาร XML ในแหล่ง C # ยังต้องการการประเมินนิพจน์ XPath 1.0 โดยพลการ ณ เวลาคอมไพล์ซึ่งก็คือเลขชี้กำลัง AFAIK


อะไรทำให้ C # ไบนารีระเบิดอย่างนั้น? เสียงเหมือนข้อผิดพลาดภาษาฉัน ...
vonbrand

1
มันเป็นวิธีการเข้ารหัสประเภททั่วไปในเมตาดาต้า class X<A,B,C,D,E> { class Y : X<Y,Y,Y,Y,Y> { Y.Y.Y.Y.Y.Y.Y.Y.Y y; } }
Vladimir Reshetnikov

-2

วัดด้วยฐานรหัสที่สมจริงเช่นชุดโครงการโอเพนซอร์ส หากคุณพล็อตผลลัพธ์เป็น (codeSize, finishTime) คุณสามารถพล็อตกราฟเหล่านั้นได้ หากข้อมูลของคุณ f (x) = y เป็น O (n) ดังนั้นการวางแผน g = f (x) / x ควรให้คุณเป็นเส้นตรงหลังจากที่ข้อมูลเริ่มมีขนาดใหญ่ขึ้น

พล็อต f (x) / x, f (x) / lg (x), f (x) / (x * lg (x)), f (x) / (x * x) ฯลฯ กราฟจะดำดิ่ง ออกไปที่ศูนย์เพิ่มขึ้นโดยไม่ถูกผูกไว้หรือแผ่ออก แนวคิดนี้มีประโยชน์สำหรับสถานการณ์ต่างๆเช่นการวัดเวลาแทรกที่เริ่มต้นจากฐานข้อมูลเปล่า (เช่น: เพื่อค้นหา 'การรั่วไหลของประสิทธิภาพ' เป็นเวลานาน)


1
การวัดเชิงประจักษ์ของเวลาทำงานไม่ได้สร้างความซับซ้อนในการคำนวณ ข้อแรกความซับซ้อนในการคำนวณจะแสดงโดยทั่วไปในแง่ของเวลาทำงานที่เลวร้ายที่สุด ประการที่สองแม้ว่าคุณต้องการวัดตัวพิมพ์เล็กเฉลี่ยคุณจะต้องระบุว่าอินพุตของคุณเป็น "ปานกลาง" ในแง่นั้น
David Richerby

แน่นอนว่ามันเป็นเพียงการประมาณการ แต่การทดสอบเชิงประจักษ์อย่างง่าย ๆ ที่มีข้อมูลจริงจำนวนมาก (ทุกการคอมมิทสำหรับคอมไพล์ git) อาจดีกว่าแบบจำลองอย่างระมัดระวัง ไม่ว่าในกรณีใด ๆ ถ้าฟังก์ชั่นเป็น O (n ^ 3) และคุณวางแผน f (n) / (n n n) คุณควรได้เส้นที่มีเสียงดังซึ่งมีความชันเป็นศูนย์ประมาณ หากคุณวางแผน O (n ^ 3) / (n * n) เท่านั้นคุณจะเห็นว่ามันเพิ่มขึ้นเป็นเส้นตรง มันชัดเจนมากถ้าคุณประเมินค่าสูงเกินไปและดูเส้นพุ่งไปที่ศูนย์อย่างรวดเร็ว
Rob

1
ไม่ตัวอย่างเช่น quicksort ทำงานในเวลาในข้อมูลอินพุตส่วนใหญ่ แต่การใช้งานบางอย่างมีเวลาทำงานในกรณีที่เลวร้ายที่สุด แต่ถ้าคุณเพียงแค่พล็อตเวลาทำงานคุณมากขึ้นที่จะวิ่งเข้าไปในกรณีกว่าคน Θ ( n 2 ) Θ ( n log n ) Θ ( n 2 )Θ(nlogn)Θ(n2)Θ(nlogn)Θ(n2)
David Richerby

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

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