ข้อมูลทั่วไปถูกนำไปใช้ในคอมไพเลอร์สมัยใหม่อย่างไร


15

สิ่งที่ฉันหมายถึงนี่คือเราจะเปลี่ยนจากเทมเพลตบางส่วนT add(T a, T b) ...เป็นรหัสที่สร้างได้อย่างไร ฉันคิดถึงวิธีที่จะบรรลุผลสองสามอย่างนี้เราเก็บฟังก์ชันทั่วไปไว้ใน AST Function_Nodeแล้วทุกครั้งที่เราใช้เราเก็บไว้ในโหนดฟังก์ชั่นดั้งเดิมสำเนาของตัวเองด้วยประเภททั้งหมดที่ถูกTแทนที่ด้วยประเภทที่ กำลังใช้. ยกตัวอย่างเช่นadd<int>(5, 6)จะเก็บสำเนาของฟังก์ชั่นทั่วไปหาaddและทดแทนทุกประเภทT ในการคัดลอกintด้วย

ดังนั้นมันจะมีลักษณะดังนี้:

struct Function_Node {
    std::string name; // etc.
    Type return_type;
    std::vector<std::pair<Type, std::string>> arguments;
    std::vector<Function_Node> copies;
};

จากนั้นคุณสามารถสร้างรหัสสำหรับสิ่งเหล่านี้และเมื่อคุณเยี่ยมชมFunction_Nodeรายการของสำเนาที่copies.size() > 0คุณเรียกใช้visitFunctionในสำเนาทั้งหมด

visitFunction(Function_Node& node) {
    if (node.copies.size() > 0) {
        for (auto& node : nodes.copies) {
            visitFunction(node);
        }
        // it's a generic function so we don't want
        // to emit code for this.
        return;
    }
}

มันจะออกมาดีหรือไม่ คอมไพเลอร์สมัยใหม่เข้าใกล้ปัญหานี้อย่างไร ฉันคิดว่าบางทีอีกวิธีหนึ่งในการทำเช่นนี้ก็คือคุณสามารถฉีดสำเนาลงใน AST เพื่อให้มันวิ่งผ่านช่วงความหมายทั้งหมด ฉันยังคิดว่าบางทีคุณสามารถสร้างพวกเขาในรูปแบบทันทีเช่น MIR ของ Rust หรือ Swifts SIL ตัวอย่างเช่น

รหัสของฉันเขียนด้วยภาษา Java ตัวอย่างที่นี่คือ C ++ เพราะมันเป็นตัวอย่างที่ละเอียดน้อยกว่า - แต่หลักการก็คือสิ่งเดียวกัน แม้ว่าอาจจะมีข้อผิดพลาดเล็กน้อยเพราะมันเขียนด้วยมือในกล่องคำถาม

โปรดทราบว่าฉันหมายถึงคอมไพเลอร์ที่ทันสมัยเช่นเดียวกับวิธีที่ดีที่สุดในการแก้ไขปัญหานี้ และเมื่อฉันพูดว่า generics ฉันไม่ได้หมายความว่าเหมือน generics Java ที่พวกเขาใช้การลบประเภท


ใน C ++ (ภาษาการเขียนโปรแกรมอื่น ๆ มี generics แต่พวกเขาแต่ละคนใช้มันแตกต่างกัน) โดยทั่วไปมันเป็นระบบแมโครยักษ์รวบรวมเวลา รหัสจริงถูกสร้างขึ้นโดยใช้ชนิดที่ถูกแทนที่
Robert Harvey

ทำไมไม่พิมพ์ลบ? โปรดทราบว่าไม่ใช่แค่ Java เท่านั้นที่ทำได้และไม่ใช่เทคนิคที่ไม่ดี (ขึ้นอยู่กับความต้องการของคุณ)
Andres F.

@AndresF ฉันคิดว่าถ้าวิธีการทำงานของภาษาของฉันมันไม่ได้ผล
Jon Flow

2
ฉันคิดว่าคุณควรชี้แจงว่าคุณพูดถึงเรื่องอะไรกันบ้าง ตัวอย่างเช่นเทมเพลต C ++, C # generics และ Java generics ต่างกันมาก คุณบอกว่าคุณไม่ได้หมายถึง Java generics แต่คุณไม่ได้พูดในสิ่งที่คุณหมายถึง
svick

2
สิ่งนี้จำเป็นต้องมุ่งเน้นไปที่ระบบภาษาหนึ่งเพื่อหลีกเลี่ยงการกว้างเกินไป
Daenyth

คำตอบ:


14

ข้อมูลทั่วไปถูกนำไปใช้ในคอมไพเลอร์สมัยใหม่อย่างไร

ฉันขอเชิญคุณอ่านซอร์สโค้ดของคอมไพเลอร์สมัยใหม่หากคุณต้องการทราบว่าคอมไพเลอร์สมัยใหม่ทำงานอย่างไร ฉันจะเริ่มต้นด้วยโครงการ Roslyn ซึ่งใช้คอมไพเลอร์ C # และ Visual Basic

โดยเฉพาะฉันดึงความสนใจของคุณไปยังรหัสในคอมไพเลอร์ C # ซึ่งใช้สัญลักษณ์ประเภท:

https://github.com/dotnet/roslyn/tree/master/src/Compilers/CSharp/Portable/Symbols

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

https://github.com/dotnet/roslyn/tree/master/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions

ฉันพยายามอย่างหนักเพื่อทำให้หลังอ่านง่าย

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

คุณจะอธิบายแม่แบบไม่generics C # และ Visual Basic มี generics จริงในระบบชนิดของพวกเขา

สั้น ๆ พวกเขาทำงานเช่นนี้

  • เราเริ่มต้นด้วยการสร้างกฎสำหรับสิ่งที่เป็นรูปแบบที่รวบรวมอย่างเป็นทางการ ตัวอย่างเช่น: intเป็นประเภทพารามิเตอร์ประเภทTเป็นประเภทสำหรับประเภทใด ๆ ประเภทXอาร์เรย์X[]ยังเป็นประเภทและอื่น ๆ

  • กฎสำหรับ generics เกี่ยวข้องกับการทดแทน ตัวอย่างเช่นclass C with one type parameterไม่ใช่ประเภท มันเป็นรูปแบบสำหรับการทำประเภท class C with one type parameter called T, under substitution with int for T เป็นประเภท

  • กฎที่อธิบายถึงความสัมพันธ์ระหว่างประเภท - ความเข้ากันได้กับการกำหนดวิธีการกำหนดประเภทของการแสดงออกและอื่น ๆ - ได้รับการออกแบบและดำเนินการในคอมไพเลอร์

  • ภาษา bytecode ที่สนับสนุนประเภททั่วไปในระบบข้อมูลเมตาได้รับการออกแบบและดำเนินการ

  • ที่รันไทม์คอมไพเลอร์ JIT เปลี่ยน bytecode เป็นรหัสเครื่อง มันเป็นความรับผิดชอบในการสร้างรหัสเครื่องที่เหมาะสมที่ได้รับความเชี่ยวชาญทั่วไป

ตัวอย่างเช่นใน C # เมื่อคุณพูด

class C<T> { public void X(T t) { Console.WriteLine(t); } }
...
var c = new C<int>(); 
c.X(123);

จากนั้นคอมไพเลอร์ตรวจสอบว่าในC<int>การโต้แย้งintคือการทดแทนที่ถูกต้องสำหรับTและสร้างเมตาดาต้าและไบต์ตามลำดับ ที่รันไทม์ jitter ตรวจพบว่า a C<int>กำลังถูกสร้างขึ้นเป็นครั้งแรกและสร้างรหัสเครื่องที่เหมาะสมแบบไดนามิก


9

การใช้งานส่วนใหญ่ของยาชื่อสามัญ (หรือมากกว่า: ตัวแปรความหลากหลาย) ใช้ลบประเภท สิ่งนี้ช่วยลดความยุ่งยากในการรวบรวมรหัสทั่วไป แต่ใช้ได้กับกล่องที่พิมพ์เท่านั้น: เนื่องจากอาร์กิวเมนต์แต่ละตัวเป็นตัวชี้ทึบแสงได้อย่างมีประสิทธิภาพเราจึงจำเป็นต้องมี VTable หรือกลไกการจัดส่งที่คล้ายกันเพื่อดำเนินการกับอาร์กิวเมนต์ ใน Java:

<T extends Addable> T add(T a, T b) { … }

สามารถคอมไพล์ตรวจสอบชนิดและเรียกว่าแบบเดียวกับ

Addable add(Addable a, Addable b) { … }

ยกเว้นข้อมูลทั่วไปที่ให้ข้อมูลตัวตรวจสอบชนิดกับข้อมูลเพิ่มเติมที่ไซต์การโทร ข้อมูลพิเศษนี้สามารถจัดการกับตัวแปรประเภทโดยเฉพาะอย่างยิ่งเมื่ออนุมานประเภททั่วไป ในระหว่างการตรวจสอบประเภทประเภททั่วไปแต่ละประเภทสามารถถูกแทนที่ด้วยตัวแปรลองเรียกมันว่า$T1:

$T1 add($T1 a, $T1 b)

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

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

VTable สำหรับอาร์กิวเมนต์ที่พิมพ์ทั่วไปสามารถหลีกเลี่ยงได้หากฟังก์ชันทั่วไปไม่ได้ทำการดำเนินการใด ๆ กับประเภท แต่จะส่งผ่านไปยังฟังก์ชันอื่นเท่านั้น เช่นฟังก์ชัน Haskell call :: (a -> b) -> a -> b; call f x = f xจะไม่ต้องใส่กล่องxอาร์กิวเมนต์ อย่างไรก็ตามสิ่งนี้จะต้องมีการประชุมที่เรียกว่าสามารถผ่านค่าโดยไม่ทราบขนาดของพวกเขาซึ่งเป็นหลัก จำกัด ไว้ที่ตัวชี้ต่อไป


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

  1. ใช้เทมเพลตกับอาร์กิวเมนต์เท็มเพลตที่จัดเตรียม เช่นเรียกtemplate<class T> T add(T a, T b) { … }เป็นadd<int>(1, 2)จะให้เราฟังก์ชั่นที่เกิดขึ้นจริงint __add__T_int(int a, int b)(หรือสิ่งที่ชื่อ mangling วิธีการจะใช้)

  2. หากรหัสสำหรับฟังก์ชั่นนั้นได้ถูกสร้างขึ้นในหน่วยรวบรวมปัจจุบันแล้วให้ทำต่อ มิฉะนั้นสร้างรหัสราวกับว่าฟังก์ชั่นint __add__T_int(int a, int b) { … }ได้รับการเขียนในรหัสที่มา สิ่งนี้เกี่ยวข้องกับการแทนที่อาร์กิวเมนต์เท็มเพลตทั้งหมดที่เกิดขึ้นด้วยค่าของมัน นี่อาจเป็นการแปลง AST → AST จากนั้นทำการตรวจสอบชนิดบน AST ที่สร้างขึ้น

  3. __add__T_int(1, 2)รวบรวมโทรราวกับว่ารหัสที่มาได้รับ

โปรดทราบว่าเท็มเพลต C ++ มีการโต้ตอบที่ซับซ้อนกับกลไกการแก้ปัญหาโอเวอร์โหลดซึ่งฉันไม่ต้องการอธิบายที่นี่ โปรดทราบว่าการสร้างโค้ดนี้ทำให้เป็นไปไม่ได้ที่จะมีวิธีการ templated ที่เป็นเสมือน - วิธีการลบประเภทที่ไม่ได้รับผลกระทบจากข้อ จำกัด ที่สำคัญนี้


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

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

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