คอมไพเลอร์ควรจะผลิตแอสเซมเบลอร์ (และท้ายที่สุดรหัสเครื่อง) สำหรับเครื่องบางเครื่องและโดยทั่วไป C ++ พยายามเห็นอกเห็นใจต่อเครื่องดังกล่าว
การเห็นอกเห็นใจต่อเครื่องต้นแบบนั้นหมายถึงคร่าวๆ: ทำให้ง่ายต่อการเขียนรหัส C ++ ซึ่งจะแมปอย่างมีประสิทธิภาพสู่การปฏิบัติงานที่เครื่องสามารถดำเนินการได้อย่างรวดเร็ว ดังนั้นเราต้องการให้การเข้าถึงชนิดข้อมูลและการดำเนินการที่รวดเร็วและ "เป็นธรรมชาติ" บนแพลตฟอร์มฮาร์ดแวร์ของเรา
ให้พิจารณาสถาปัตยกรรมเครื่องเฉพาะ เรามาดูตระกูล Intel x86 ปัจจุบันกัน
คู่มือสำหรับนักพัฒนาซอฟท์แวร์สถาปัตยกรรมIntel® 64 และ IA-32 เล่ม 1 ( ลิงค์ ) ส่วน 3.4.1 กล่าวว่า
วัตถุประสงค์ทั่วไป 32- บิตลงทะเบียน EAX, EBX, ECX, EDX, ESI, EDI, EBP และ ESP มีไว้สำหรับการถือครองรายการต่อไปนี้:
•ดำเนินการสำหรับการดำเนินการทางตรรกะและเลขคณิต
•ดำเนินการสำหรับการคำนวณที่อยู่
•ตัวชี้หน่วยความจำ
ดังนั้นเราต้องการให้คอมไพเลอร์ใช้ EAX, EBX และอื่น ๆ ลงทะเบียนเมื่อคอมไพล์เลขจำนวนเต็ม C ++ อย่างง่าย ซึ่งหมายความว่าเมื่อฉันประกาศint
มันควรเป็นสิ่งที่เข้ากันได้กับการลงทะเบียนเหล่านี้เพื่อให้ฉันสามารถใช้พวกเขาได้อย่างมีประสิทธิภาพ
รีจิสเตอร์นั้นมีขนาดเท่ากันเสมอ (ที่นี่ 32 บิต) ดังนั้นint
ตัวแปรของฉันจะเป็น 32 บิตเช่นกัน ฉันจะใช้เลย์เอาต์เดียวกัน (little-endian) เพื่อที่ฉันจะได้ไม่ต้องทำการแปลงทุกครั้งที่ฉันโหลดค่าตัวแปรลงในรีจิสเตอร์หรือเก็บรีจิสเตอร์กลับเข้าไปในตัวแปร
การใช้godboltเราสามารถเห็นได้อย่างชัดเจนว่าคอมไพเลอร์ทำอะไรกับโค้ดเล็ก ๆ น้อย ๆ :
int square(int num) {
return num * num;
}
คอมไพล์ (ด้วย GCC 8.1 และ-fomit-frame-pointer -O3
เพื่อความง่าย) ไปที่:
square(int):
imul edi, edi
mov eax, edi
ret
หมายความว่า:
int num
พารามิเตอร์ก็ผ่านไปได้ในการลงทะเบียน EDI ความหมายมันว่าขนาดและรูปแบบของอินเทลคาดหวังสำหรับการลงทะเบียนพื้นเมือง ฟังก์ชั่นไม่ต้องแปลงอะไรเลย
- การคูณเป็นคำสั่งเดียว (
imul
) ซึ่งเร็วมาก
- การส่งคืนผลลัพธ์เป็นเพียงเรื่องของการคัดลอกไปยังการลงทะเบียนอื่น (ผู้โทรคาดว่าจะได้ผลลัพธ์ที่จะใส่ใน EAX)
แก้ไข: เราสามารถเพิ่มการเปรียบเทียบที่เกี่ยวข้องเพื่อแสดงความแตกต่างโดยใช้รูปแบบที่ไม่ใช่เจ้าของภาษา กรณีที่ง่ายที่สุดคือการเก็บค่าในสิ่งอื่นที่ไม่ใช่ความกว้างดั้งเดิม
การใช้godboltอีกครั้งเราสามารถเปรียบเทียบการคูณพื้นเมืองอย่างง่าย
unsigned mult (unsigned x, unsigned y)
{
return x*y;
}
mult(unsigned int, unsigned int):
mov eax, edi
imul eax, esi
ret
ด้วยรหัสเทียบเท่าสำหรับความกว้างที่ไม่ได้มาตรฐาน
struct pair {
unsigned x : 31;
unsigned y : 31;
};
unsigned mult (pair p)
{
return p.x*p.y;
}
mult(pair):
mov eax, edi
shr rdi, 32
and eax, 2147483647
and edi, 2147483647
imul eax, edi
ret
คำแนะนำพิเศษทั้งหมดเกี่ยวข้องกับการแปลงรูปแบบอินพุต (จำนวนเต็ม 31 บิตที่ไม่ได้ลงชื่อสองบิต) เป็นรูปแบบที่โปรเซสเซอร์สามารถจัดการได้ หากเราต้องการเก็บผลลัพธ์กลับเป็นค่า 31 บิตจะมีอีกหนึ่งหรือสองคำแนะนำในการทำเช่นนี้
ความซับซ้อนพิเศษนี้หมายความว่าคุณจะต้องใส่ใจกับสิ่งนี้เมื่อการประหยัดพื้นที่สำคัญมาก ในกรณีนี้เราประหยัดเพียงสองบิตเมื่อเทียบกับการใช้เนทิฟunsigned
หรือuint32_t
ประเภทซึ่งจะสร้างรหัสที่ง่ายกว่ามาก
หมายเหตุเกี่ยวกับขนาดแบบไดนามิก:
ตัวอย่างข้างต้นยังคงเป็นค่าความกว้างคงที่มากกว่าความกว้างผันแปร แต่ความกว้าง (และการจัดตำแหน่ง) ไม่ตรงกับการลงทะเบียนดั้งเดิม
แพลตฟอร์ม x86 มีหลายขนาดรวมถึง 8 บิตและ 16 บิตนอกเหนือจากหลัก 32 บิต (ฉันกำลังคัดสรรโหมด 64 บิตและสิ่งอื่น ๆ เพื่อความเรียบง่าย)
ประเภทนี้ (ถ่าน, int8_t, uint8_t, int16_t ฯลฯ ) นอกจากนี้ยังได้รับการสนับสนุนโดยตรงจากสถาปัตยกรรม - ส่วนหนึ่งหลังเข้ากันได้กับรุ่นเก่า 8086/286/386 / etc ชุดคำสั่ง ฯลฯ
แน่นอนว่าการเลือกประเภทขนาดคงที่ตามธรรมชาติที่เล็กที่สุดซึ่งเพียงพอจะเป็นแนวปฏิบัติที่ดี - พวกเขายังคงรวดเร็วโหลดคำสั่งเดียวและเก็บคุณยังได้รับเลขคณิตพื้นเมืองเต็มความเร็วและคุณยังสามารถปรับปรุงประสิทธิภาพโดย ลดแคชคิดถึง
นี่แตกต่างอย่างมากกับการเข้ารหัสความยาวผันแปรได้ฉันทำงานกับมันแล้วมันแย่มาก การโหลดทุกครั้งจะกลายเป็นลูปแทนที่จะเป็นคำสั่งเดียว ทุกร้านค้ายังเป็นวง โครงสร้างทั้งหมดมีความยาวผันแปรได้ดังนั้นคุณไม่สามารถใช้อาร์เรย์ได้ตามธรรมชาติ
หมายเหตุเพิ่มเติมเกี่ยวกับประสิทธิภาพ
ในความคิดเห็นที่ตามมาคุณใช้คำว่า "มีประสิทธิภาพ" เท่าที่ฉันจะบอกได้เกี่ยวกับขนาดของพื้นที่จัดเก็บ บางครั้งเราเลือกที่จะลดขนาดพื้นที่เก็บข้อมูล - อาจสำคัญเมื่อเราบันทึกค่าจำนวนมากลงในไฟล์หรือส่งผ่านเครือข่าย ข้อเสียคือเราต้องโหลดค่าเหล่านั้นไปยังรีจิสเตอร์เพื่อทำทุกอย่างกับพวกเขาและทำการแปลงไม่ฟรี
เมื่อเราพูดถึงประสิทธิภาพเราจำเป็นต้องรู้ว่าเรากำลังเพิ่มประสิทธิภาพและสิ่งที่ไม่ดี การใช้ประเภทการจัดเก็บที่ไม่ใช่เจ้าของภาษาเป็นวิธีหนึ่งในการแลกเปลี่ยนความเร็วในการประมวลผลสำหรับพื้นที่ โดยใช้การจัดเก็บข้อมูลความยาวตัวแปร (ประเภทเลขคณิตอย่างน้อย), การซื้อขายมากขึ้นในการประมวลผลความเร็วสูง (รหัสและความซับซ้อนและเวลาที่นักพัฒนา) สำหรับการประหยัดต่อไปมักจะน้อยที่สุดของพื้นที่
การลงโทษด้วยความเร็วที่คุณจ่ายสำหรับสิ่งนี้หมายความว่ามันคุ้มค่าเมื่อคุณต้องการลดแบนด์วิดท์หรือการจัดเก็บข้อมูลระยะยาวและสำหรับกรณีเหล่านี้มักจะใช้รูปแบบที่เรียบง่ายและเป็นธรรมชาติได้ง่ายกว่า - จากนั้นบีบอัดด้วยระบบอเนกประสงค์ (เช่น zip, gzip, bzip2, xy หรืออะไรก็ตาม)
TL; DR
แต่ละแพลตฟอร์มมีสถาปัตยกรรมเดียว แต่คุณสามารถหาวิธีที่แตกต่างกันมากมายในการแสดงข้อมูล มันไม่สมเหตุสมผลสำหรับภาษาใด ๆ ที่จะให้ชนิดข้อมูลในตัวไม่ จำกัด ดังนั้น C ++ จึงให้การเข้าถึงชุดข้อมูลแบบดั้งเดิมที่เป็นธรรมชาติของแพลตฟอร์มและช่วยให้คุณสามารถเขียนโค้ดการแสดงอื่น ๆ
unsinged
คุ้มค่าที่สามารถแทนด้วย 1255
ไบต์คือ 2) พิจารณาค่าใช้จ่ายในการคำนวณขนาดของที่เก็บข้อมูลที่เหมาะสมและลดขนาด / ขยายพื้นที่เก็บข้อมูลของตัวแปรเมื่อค่าเปลี่ยนแปลง