เวอร์ชั่นสั้น: ใช้calloc()
แทนmalloc()+memset()
ทุกครั้ง ในกรณีส่วนใหญ่พวกเขาจะเหมือนกัน ในบางกรณีcalloc()
จะทำงานน้อยลงเพราะสามารถข้ามได้memset()
ทั้งหมด ในกรณีอื่น ๆcalloc()
สามารถโกงและไม่จัดสรรหน่วยความจำได้! อย่างไรก็ตามmalloc()+memset()
จะทำงานเต็มจำนวนเสมอ
การทำความเข้าใจสิ่งนี้ต้องมีการทัวร์ระยะสั้น ๆ ของระบบหน่วยความจำ
ทัวร์หน่วยความจำอย่างรวดเร็ว
มีสี่ส่วนหลักที่นี่: โปรแกรมของคุณไลบรารีมาตรฐานเคอร์เนลและตารางหน้า คุณรู้จักโปรแกรมของคุณอยู่แล้วดังนั้น ...
ตัวจัดสรรหน่วยความจำชอบmalloc()
และcalloc()
ส่วนใหญ่อยู่ที่นั่นเพื่อทำการจัดสรรขนาดเล็ก (อะไรก็ได้ตั้งแต่ 1 ไบต์ถึง 100s ของ KB) และจัดกลุ่มไว้ในกลุ่มหน่วยความจำขนาดใหญ่ ตัวอย่างเช่นถ้าคุณจัดสรร 16 ไบต์malloc()
แรกจะพยายามรับ 16 ไบต์จากหนึ่งในสระแล้วขอหน่วยความจำเพิ่มเติมจากเคอร์เนลเมื่อสระว่ายน้ำแห้ง อย่างไรก็ตามเนื่องจากโปรแกรมที่คุณถามกำลังจัดสรรหน่วยความจำจำนวนมากในคราวเดียวmalloc()
และcalloc()
จะขอหน่วยความจำนั้นโดยตรงจากเคอร์เนล เกณฑ์สำหรับพฤติกรรมนี้ขึ้นอยู่กับระบบของคุณ แต่ฉันเคยเห็น 1 MiB ใช้เป็นเกณฑ์
เคอร์เนลรับผิดชอบการจัดสรร RAM จริงให้กับแต่ละกระบวนการและทำให้แน่ใจว่ากระบวนการไม่รบกวนหน่วยความจำของกระบวนการอื่น สิ่งนี้เรียกว่าการป้องกันหน่วยความจำมันเป็นสิ่งสกปรกทั่วไปมาตั้งแต่ทศวรรษ 1990 และเป็นสาเหตุที่ทำให้โปรแกรมหนึ่งสามารถทำงานผิดพลาดได้โดยไม่ทำให้ระบบทั้งหมดพัง ดังนั้นเมื่อโปรแกรมที่ต้องการหน่วยความจำมากขึ้นก็ไม่สามารถใช้เวลาเพียงแค่ความทรงจำ แต่จะถามสำหรับหน่วยความจำจากเมล็ดโดยใช้สายระบบเหมือนหรือmmap()
sbrk()
เคอร์เนลจะให้ RAM กับแต่ละกระบวนการโดยการปรับเปลี่ยนตารางหน้า
ตารางหน้าจะแมปหน่วยความจำกับ RAM จริงจริง ที่อยู่ของกระบวนการของคุณ 0x00000000 ถึง 0xFFFFFFFF ในระบบ 32 บิตไม่ใช่หน่วยความจำจริง แต่เป็นที่อยู่ในหน่วยความจำเสมือน หน่วยประมวลผลแบ่งที่อยู่เหล่านี้ออกเป็น 4 หน้า KiB และแต่ละหน้าสามารถกำหนดให้กับชิ้นส่วนของ RAM ทางกายภาพที่แตกต่างกันโดยการปรับเปลี่ยนตารางหน้า เคอร์เนลเท่านั้นที่ได้รับอนุญาตให้แก้ไขตารางหน้า
มันไม่ทำงาน
นี่คือวิธีการจัดสรร 256 MiB ไม่ทำงาน:
กระบวนการของคุณโทรcalloc()
และถาม 256 MiB
ห้องสมุดมาตรฐานโทรmmap()
และขอ 256 MiB
เคอร์เนลค้นหา RAM ที่ไม่ได้ใช้ขนาด 256 MiB และมอบให้กับกระบวนการของคุณโดยแก้ไขตารางหน้า
ห้องสมุดมาตรฐาน zeroes แรมด้วยและผลตอบแทนจากmemset()
calloc()
ในที่สุดกระบวนการของคุณจะออกและเคอร์เนลจะเรียกคืน RAM เพื่อให้กระบวนการอื่นสามารถใช้งานได้
มันใช้งานได้จริง
กระบวนการข้างต้นจะได้ผล แต่ก็ไม่เกิดขึ้นเช่นนี้ มีความแตกต่างที่สำคัญสามประการ
เมื่อกระบวนการของคุณได้รับหน่วยความจำใหม่จากเคอร์เนลอาจมีการใช้หน่วยความจำนั้นโดยกระบวนการอื่นก่อนหน้านี้ นี่คือความเสี่ยงด้านความปลอดภัย ถ้าหน่วยความจำนั้นมีรหัสผ่าน, คีย์เข้ารหัส, หรือสูตรลับซัลซ่า? เพื่อป้องกันไม่ให้ข้อมูลที่มีความสำคัญรั่วไหลเคอร์เนลจะขัดหน่วยความจำเสมอก่อนที่จะส่งไปยังกระบวนการ เราอาจขัดหน่วยความจำโดย zeroing และถ้าหน่วยความจำใหม่เป็นศูนย์เราอาจทำให้การรับประกันดังนั้นmmap()
รับประกันว่าหน่วยความจำใหม่ที่ส่งกลับจะเป็นศูนย์เสมอ
มีโปรแกรมมากมายที่จัดสรรหน่วยความจำ แต่ไม่ใช้หน่วยความจำทันที บางครั้งมีการจัดสรรหน่วยความจำ แต่ไม่เคยใช้ เคอร์เนลรู้เรื่องนี้และขี้เกียจ เมื่อคุณจัดสรรหน่วยความจำใหม่เคอร์เนลจะไม่แตะที่ตารางหน้าเลยและจะไม่ให้ RAM กับกระบวนการของคุณ แต่จะค้นหาพื้นที่ที่อยู่บางส่วนในกระบวนการของคุณจดบันทึกสิ่งที่ควรจะไปที่นั่นและให้สัญญาว่าจะใส่ RAM ที่นั่นหากโปรแกรมของคุณใช้งานจริง เมื่อโปรแกรมของคุณพยายามอ่านหรือเขียนจากที่อยู่เหล่านั้นตัวประมวลผลจะทริกเกอร์ข้อผิดพลาดของหน้าและขั้นตอนเคอร์เนลในการกำหนด RAM ให้กับที่อยู่เหล่านั้นและดำเนินการโปรแกรมของคุณต่อ หากคุณไม่เคยใช้หน่วยความจำความผิดพลาดของหน้าจะไม่เกิดขึ้นและโปรแกรมของคุณจะไม่ได้รับ RAM จริง
บางกระบวนการจัดสรรหน่วยความจำแล้วอ่านจากมันโดยไม่ต้องแก้ไข ซึ่งหมายความว่าหน้าจำนวนมากในหน่วยความจำในกระบวนการที่แตกต่างกันอาจเต็มไปด้วยศูนย์ที่กลับมาจากmmap()
เดิม เนื่องจากหน้าเหล่านี้เหมือนกันทั้งหมดเคอร์เนลทำให้ที่อยู่เสมือนเหล่านี้ชี้ไปที่หน้าหน่วยความจำ 4 KiB ที่ใช้ร่วมกันซึ่งเต็มไปด้วยเลขศูนย์ หากคุณพยายามที่จะเขียนไปยังหน่วยความจำตัวประมวลผลทริกเกอร์ข้อผิดพลาดหน้าอื่นและขั้นตอนเคอร์เนลเพื่อให้หน้าใหม่ของศูนย์ที่ไม่ได้ใช้ร่วมกับโปรแกรมอื่น ๆ
กระบวนการสุดท้ายมีลักษณะเช่นนี้มากขึ้น:
กระบวนการของคุณโทรcalloc()
และถาม 256 MiB
ห้องสมุดมาตรฐานโทรmmap()
และขอ 256 MiB
เคอร์เนลค้นหาพื้นที่ที่อยู่ที่ไม่ได้ใช้ขนาด 256 MiB ทำการบันทึกเกี่ยวกับพื้นที่ที่อยู่นั้นที่ใช้ในปัจจุบันและส่งคืน
ไลบรารีมาตรฐานรู้ว่าผลลัพธ์ของการmmap()
เติมด้วยศูนย์เสมอ (หรือจะได้รับเมื่อ RAM จริง) ดังนั้นจึงไม่ได้สัมผัสหน่วยความจำดังนั้นจึงไม่มีข้อบกพร่องของหน้าและ RAM ไม่เคยได้รับในกระบวนการของคุณ .
ในที่สุดกระบวนการของคุณจะออกและเคอร์เนลไม่จำเป็นต้องเรียกคืน RAM เพราะไม่เคยมีการจัดสรรตั้งแต่แรก
หากคุณใช้memset()
เพื่อทำให้หน้าเป็นศูนย์memset()
จะทริกเกอร์ข้อผิดพลาดของหน้าทำให้ RAM ได้รับการจัดสรรและจากนั้นให้เป็นศูนย์แม้ว่าจะเต็มไปด้วยศูนย์แล้ว นี้เป็นจำนวนเงินมหาศาลในการทำงานพิเศษและอธิบายว่าทำไมcalloc()
จะเร็วกว่าและmalloc()
memset()
หากใช้หน่วยความจำต่อไปcalloc()
ก็ยังเร็วกว่าmalloc()
และmemset()
แตกต่างกัน แต่ก็ไม่ได้ไร้สาระ
มันไม่ได้ผลเสมอไป
ไม่ใช่ทุกระบบที่มีหน่วยความจำเสมือนเพจดังนั้นบางระบบไม่สามารถใช้การปรับให้เหมาะสมเหล่านี้ได้ สิ่งนี้ใช้กับโปรเซสเซอร์เก่ามากเช่น 80286 รวมถึงโปรเซสเซอร์แบบฝังซึ่งเล็กเกินไปสำหรับหน่วยจัดการหน่วยความจำที่ซับซ้อน
สิ่งนี้จะไม่ทำงานกับการจัดสรรที่เล็กกว่าเสมอ ด้วยการจัดสรรที่น้อยลงcalloc()
รับหน่วยความจำจากพูลที่แบ่งใช้แทนที่จะไปที่เคอร์เนลโดยตรง โดยทั่วไปพูลที่แบ่งใช้อาจมีข้อมูลขยะที่จัดเก็บไว้ในหน่วยความจำเก่าที่ใช้และปล่อยด้วยfree()
ดังนั้นจึงcalloc()
สามารถใช้หน่วยความจำนั้นและการโทรmemset()
เพื่อล้างข้อมูลออก การใช้งานทั่วไปจะติดตามว่าส่วนใดของพูลที่แบ่งใช้นั้นเก่าและยังเต็มไปด้วยเลขศูนย์ แต่การปรับใช้ไม่ได้ทำสิ่งนี้ทั้งหมด
กำจัดคำตอบที่ผิดบางอย่าง
เคอร์เนลอาจหรือไม่เป็นศูนย์หน่วยความจำในเวลาว่างขึ้นอยู่กับระบบปฏิบัติการในกรณีที่คุณต้องการรับหน่วยความจำที่เป็นศูนย์ในภายหลัง ลินุกซ์ไม่ได้เป็นศูนย์หน่วยความจำไปข้างหน้าของเวลาและแมลงปอ BSD เมื่อเร็ว ๆ นี้ออกยังคุณลักษณะนี้จากเมล็ดของพวกเขา เมล็ดอื่น ๆ บางหน่วยความจำศูนย์ก่อนเวลาอย่างไรก็ตาม การใช้หน้าจอเป็นศูนย์ในการใช้งานที่ไม่ได้ใช้งานนั้นไม่เพียงพอที่จะอธิบายความแตกต่างของประสิทธิภาพการทำงานขนาดใหญ่ได้
calloc()
ฟังก์ชั่นไม่ได้ใช้บางรุ่นหน่วยความจำชิดพิเศษmemset()
และที่จะไม่ทำให้มันเร็วขึ้นมากเลยล่ะค่ะ memset()
การใช้งานส่วนใหญ่สำหรับโปรเซสเซอร์ที่ทันสมัยมีลักษณะเช่นนี้:
function memset(dest, c, len)
// one byte at a time, until the dest is aligned...
while (len > 0 && ((unsigned int)dest & 15))
*dest++ = c
len -= 1
// now write big chunks at a time (processor-specific)...
// block size might not be 16, it's just pseudocode
while (len >= 16)
// some optimized vector code goes here
// glibc uses SSE2 when available
dest += 16
len -= 16
// the end is not aligned, so one byte at a time
while (len > 0)
*dest++ = c
len -= 1
ดังนั้นคุณจะเห็นว่าmemset()
เร็วมากและคุณจะไม่ได้อะไรที่ดีไปกว่านี้สำหรับหน่วยความจำขนาดใหญ่
ความจริงที่ว่าการmemset()
zeroing memory ที่มี zeroed อยู่แล้วนั้นหมายความว่า memory ได้รับ zeroed สองครั้ง แต่นั่นจะอธิบายความแตกต่างของประสิทธิภาพที่ 2x เท่านั้น ความแตกต่างด้านประสิทธิภาพที่นี่มีขนาดใหญ่กว่ามาก (ฉันวัดมากกว่าสามคำสั่งขนาดในระบบของฉันระหว่างmalloc()+memset()
และcalloc()
)
เคล็ดลับปาร์ตี้
แทนที่จะวนลูป 10 ครั้งให้เขียนโปรแกรมที่จัดสรรหน่วยความจำจนกระทั่งmalloc()
หรือcalloc()
คืนค่า NULL
จะเกิดอะไรขึ้นถ้าคุณเพิ่มmemset()
?