มีคำตอบที่ดีมากมายที่นี่ซึ่งครอบคลุมประเด็นสำคัญหลายข้อดังนั้นฉันจะเพิ่มปัญหาสองสามข้อที่ฉันไม่ได้เห็นโดยตรงข้างต้น นั่นคือคำตอบนี้ไม่ควรพิจารณาถึงข้อดีและข้อเสีย แต่เป็นภาคผนวกของคำตอบอื่น ๆ ที่นี่
mmap ดูเหมือนเวทมนตร์
การกรณีที่ไฟล์ที่มีอยู่แล้วอย่างเต็มที่ที่แคช1เป็นพื้นฐาน2 , mmap
อาจจะดูสวยมากเช่นมายากล :
mmap
เพียงต้องการ 1 การเรียกระบบเพื่อ (อาจ) แมปไฟล์ทั้งหมดหลังจากนั้นไม่จำเป็นต้องเรียกระบบเพิ่มเติมอีก
mmap
ไม่ต้องการสำเนาข้อมูลไฟล์จากเคอร์เนลไปยังพื้นที่ผู้ใช้
mmap
อนุญาตให้คุณเข้าถึงไฟล์ "ในฐานะหน่วยความจำ" รวมถึงการประมวลผลด้วยเทคนิคขั้นสูงที่คุณสามารถทำได้กับหน่วยความจำเช่นคอมไพเลอร์อัตโนมัติ - เวกเตอร์, ซิมอินทรินอิน, การดึงข้อมูลล่วงหน้า
ในกรณีที่ไฟล์นั้นมีอยู่ในแคชดูเหมือนจะเป็นไปไม่ได้ที่จะเอาชนะ: คุณเพียงแค่เข้าถึงแคชหน้าเคอร์เนลเป็นหน่วยความจำโดยตรงและจะไม่ได้เร็วกว่านั้น
ก็สามารถ
mmap ไม่ใช่เวทมนต์เพราะ ...
mmap ยังคงทำงานต่อหน้าได้
ต้นทุนหลักที่ซ่อนอยู่ของmmap
vs read(2)
(ซึ่งจริงๆแล้วเป็น syscall ระดับ OS ที่เทียบเท่ากับการอ่านบล็อก ) คือmmap
คุณจะต้อง "ทำงานบางอย่าง" สำหรับหน้า 4K ทุกหน้าในพื้นที่ผู้ใช้แม้ว่ามันอาจจะถูกซ่อนโดย กลไกความผิดหน้า
ตัวอย่างเช่นการนำไปใช้โดยทั่วไปที่เพียงแค่mmap
ไฟล์ทั้งหมดจะต้องมีข้อบกพร่องดังนั้น 100 GB / 4K = 25 ล้านข้อบกพร่องเพื่ออ่านไฟล์ 100 GB ตอนนี้สิ่งเหล่านี้จะเป็นความผิดพลาดเล็กน้อยแต่ความผิดพลาดของหน้าเว็บถึง 25 พันล้านเพจนั้นยังไม่เร็วนัก ค่าใช้จ่ายของความผิดเล็กน้อยอาจเป็นใน 100s ของ nanos ในกรณีที่ดีที่สุด
mmap อาศัยประสิทธิภาพ TLB อย่างมาก
ตอนนี้คุณสามารถส่งผ่านMAP_POPULATE
ไปmmap
ยังบอกให้ตั้งค่าตารางหน้าทั้งหมดก่อนที่จะกลับมาดังนั้นจึงไม่ควรมีข้อบกพร่องของหน้าในขณะที่เข้าถึง ตอนนี้มีปัญหาเล็ก ๆ น้อย ๆ ว่ามันยังอ่านไฟล์ทั้งหมดลงใน RAM, ซึ่งจะระเบิดขึ้นถ้าคุณพยายามที่จะแมไฟล์ 100GB - แต่ขอไม่สนใจว่าตอนนี้3 เคอร์เนลต้องทำงานต่อหน้าเพื่อตั้งค่าตารางหน้าเหล่านี้ (แสดงเป็นเวลาเคอร์เนล) ปลายนี้ขึ้นเป็นค่าใช้จ่ายที่สำคัญในmmap
วิธีการและมันสัดส่วนกับขนาดของไฟล์ (เช่นจะไม่ได้รับความสำคัญน้อยกว่าขนาดของไฟล์ที่เติบโต) 4
ในที่สุดแม้ในพื้นที่ของผู้ใช้ที่เข้าถึงการแมปดังกล่าวจะไม่ฟรี (เทียบกับบัฟเฟอร์หน่วยความจำขนาดใหญ่ที่ไม่ได้มาจากไฟล์mmap
) - แม้เมื่อตั้งค่าหน้าตารางแล้วการเข้าถึงหน้าใหม่แต่ละครั้งก็จะเกิดขึ้น ในเชิงแนวคิดต้องมี TLB miss เนื่องจากการmmap
อ้างถึงไฟล์หมายถึงการใช้แคชหน้าและหน้า 4K ของไฟล์คุณจึงต้องเสียค่าใช้จ่าย 25 ล้านครั้งสำหรับไฟล์ 100GB
ตอนนี้ต้นทุนที่แท้จริงของการพลาด TLB เหล่านี้ขึ้นอยู่กับฮาร์ดแวร์ของคุณอย่างน้อยที่สุดดังต่อไปนี้: (a) จำนวน TLB 4K ที่คุณเข้าร่วมและวิธีการแคชการแปลที่เหลือทำงานอย่างมีประสิทธิภาพ (b) ความสัมพันธ์กับฮาร์ดแวร์ ด้วย TLB - เช่นสามารถดึงการเรียกหน้าเพจล่วงหน้าได้หรือไม่ (c) ความรวดเร็วและความขนานของฮาร์ดแวร์ในการเดินหน้า ในโปรเซสเซอร์ x86 Intel ระดับไฮเอนด์ที่ทันสมัยฮาร์ดแวร์การเดินหน้าโดยทั่วไปมีความแข็งแกร่งมาก: มีวอล์กเกอร์เพจขนานกันอย่างน้อย 2 เพจการเดินเพจสามารถเกิดขึ้นพร้อมกันกับการดำเนินการต่อเนื่องและการดึงฮาร์ดแวร์ล่วงหน้าสามารถเรียกเพจได้ ดังนั้นผลกระทบ TLB ต่อโหลดการอ่านแบบสตรีมจึงค่อนข้างต่ำ - และโหลดดังกล่าวมักจะทำงานในลักษณะเดียวกันโดยไม่คำนึงถึงขนาดหน้ากระดาษ ฮาร์ดแวร์อื่น ๆ มักจะแย่กว่านั้นมาก!
อ่าน () หลีกเลี่ยงข้อผิดพลาดเหล่านี้
read()
syscall ซึ่งเป็นสิ่งที่โดยทั่วไปรองรับ "บล็อกอ่าน" ชนิดสายนำเสนอเช่นใน C, C ++ และภาษาอื่น ๆ ที่มีข้อเสียอย่างหนึ่งหลักที่ทุกคนเป็นอย่างดีตระหนักถึง:
- การ
read()
เรียกใช้ N ไบต์ทุกครั้งต้องคัดลอก N ไบต์จากเคอร์เนลไปยังพื้นที่ผู้ใช้
ในอีกทางหนึ่งก็เป็นการหลีกเลี่ยงค่าใช้จ่ายส่วนใหญ่ - คุณไม่จำเป็นต้องแมปในหน้าเว็บขนาด 25 ล้าน 4K ลงในพื้นที่ผู้ใช้ โดยปกติคุณสามารถmalloc
บัฟเฟอร์ขนาดเล็กบัฟเฟอร์เดียวในพื้นที่ผู้ใช้และนำกลับมาใช้ซ้ำสำหรับการread
โทรทั้งหมดของคุณ ทางด้านเคอร์เนลแทบจะไม่มีปัญหากับหน้า 4K หรือ TLB ที่พลาดเพราะ RAM ทั้งหมดมักจะถูกแมปเชิงเส้นโดยใช้หน้าขนาดใหญ่มากสองสามหน้า (เช่น 1 GB หน้าใน x86) ดังนั้นเพจที่อยู่ในแคชหน้าจะถูกครอบคลุม มีประสิทธิภาพมากในพื้นที่เคอร์เนล
ดังนั้นโดยทั่วไปคุณมีการเปรียบเทียบดังต่อไปนี้เพื่อพิจารณาว่าจะเร็วกว่าสำหรับการอ่านไฟล์ขนาดใหญ่เพียงครั้งเดียว:
การทำงานพิเศษต่อหน้าโดยนัยmmap
มีค่าใช้จ่ายมากกว่างานต่อไบต์ของการคัดลอกเนื้อหาไฟล์จากเคอร์เนลไปยังพื้นที่ผู้ใช้โดยนัยread()
หรือไม่?
ในหลาย ๆ ระบบพวกเขามีความสมดุลโดยประมาณ โปรดทราบว่าแต่ละสเกลนั้นมีคุณลักษณะที่แตกต่างกันโดยสิ้นเชิงของฮาร์ดแวร์และสแต็ค
โดยเฉพาะอย่างยิ่งmmap
วิธีการจะค่อนข้างเร็วเมื่อ:
- ระบบปฏิบัติการมีการจัดการข้อผิดพลาดเล็กน้อยอย่างรวดเร็วและโดยเฉพาะอย่างยิ่งการเพิ่มประสิทธิภาพการพะรุงพะรังเล็กน้อยเช่นการแก้ไขข้อบกพร่อง
- ระบบปฏิบัติการมีการ
MAP_POPULATE
ใช้งานที่ดีซึ่งสามารถประมวลผลแผนที่ขนาดใหญ่ได้อย่างมีประสิทธิภาพในกรณีที่หน้าต้นแบบอยู่ติดกันในหน่วยความจำกายภาพ
- ฮาร์ดแวร์มีประสิทธิภาพการแปลหน้าสูงเช่น TLB ขนาดใหญ่ TLB ระดับที่สองอย่างรวดเร็ววอล์กเกอร์เพจแบบเร็วและแบบขนานการโต้ตอบแบบดึงข้อมูลล่วงหน้าที่ดีกับการแปลและอื่น ๆ
... ในขณะที่read()
วิธีการจะค่อนข้างเร็วเมื่อ:
read()
syscall มีประสิทธิภาพสำเนาที่ดี เช่นcopy_to_user
ประสิทธิภาพที่ดีในด้านเคอร์เนล
- เคอร์เนลมีวิธีที่มีประสิทธิภาพ (เทียบกับ userland) ในการแมปหน่วยความจำเช่นใช้หน้าใหญ่เพียงไม่กี่หน้าพร้อมการสนับสนุนฮาร์ดแวร์
- เคอร์เนลมี syscalls ที่รวดเร็วและวิธีเก็บรักษารายการ TLB ของเคอร์เนลทั่ว syscalls
ปัจจัยดังกล่าวข้างต้นฮาร์ดแวร์แตกต่างกันอย่างดุเดือดข้ามแพลตฟอร์มที่แตกต่างกันแม้จะอยู่ในครอบครัวเดียวกัน (เช่นภายใน x86 รุ่นและโดยเฉพาะอย่างยิ่งกลุ่มตลาด) และแน่นอนทั่วสถาปัตยกรรม (เช่น ARM VS x86 VS PPC)
ปัจจัยของระบบปฏิบัติการมีการเปลี่ยนแปลงเช่นกันพร้อมกับการปรับปรุงที่หลากหลายทั้งสองด้านทำให้เกิดการกระโดดเร็วในความเร็วสัมพัทธ์สำหรับวิธีหนึ่งหรืออีกวิธีหนึ่ง รายการล่าสุดประกอบด้วย:
- การเพิ่มข้อผิดพลาดตามที่อธิบายไว้ข้างต้นซึ่งช่วย
mmap
กรณีโดยไม่MAP_POPULATE
ได้
- การเพิ่ม
copy_to_user
เมธอดของพา ธ ด่วนในarch/x86/lib/copy_user_64.S
เช่นใช้REP MOVQ
เมื่อมันเร็วซึ่งช่วยเรื่องread()
นี้ได้จริง
อัปเดตหลังจาก Spectre และ Meltdown
การลดช่องโหว่ Spectre และ Meltdown ทำให้ค่าใช้จ่ายในการโทรระบบเพิ่มขึ้นอย่างเห็นได้ชัด ในระบบที่ฉันวัดค่าใช้จ่ายของการเรียกระบบ "ไม่ทำอะไรเลย" (ซึ่งเป็นการประเมินค่าใช้จ่ายที่แท้จริงของการเรียกระบบนอกเหนือจากงานจริงใด ๆ ที่ทำโดยการโทร) ไปจากประมาณ 100 ns ในแบบทั่วไป ระบบ Linux ที่ทันสมัยประมาณ 700 ns นอกจากนี้ขึ้นอยู่กับระบบของคุณการแยกการแก้ไขตารางหน้าโดยเฉพาะสำหรับ Meltdown สามารถมีผลต่อเนื่องเพิ่มเติมนอกเหนือจากค่าโทรระบบโดยตรงเนื่องจากจำเป็นต้องโหลดรายการ TLB ซ้ำ
ทั้งหมดนี้เป็นข้อเสียเปรียบสัมพัทธ์สำหรับread()
วิธีการตามเมื่อเทียบกับmmap
วิธีการตามเนื่องจากread()
วิธีการจะต้องทำการเรียกระบบหนึ่งสำหรับแต่ละ "ขนาดบัฟเฟอร์" มูลค่าของข้อมูล คุณไม่สามารถเพิ่มขนาดบัฟเฟอร์โดยพลการเพื่อลดค่าใช้จ่ายนี้เนื่องจากการใช้บัฟเฟอร์ขนาดใหญ่มักจะทำงานได้แย่ลงเนื่องจากคุณมีขนาดเกินกว่า L1 และด้วยเหตุนี้จึงทำให้แคชหายไปอย่างต่อเนื่อง
ในอีกทางหนึ่งด้วยmmap
คุณสามารถแมปในพื้นที่ขนาดใหญ่ของหน่วยความจำด้วยMAP_POPULATE
และเข้าถึงได้อย่างมีประสิทธิภาพด้วยค่าใช้จ่ายเพียงการโทรระบบเดียว
1สิ่งนี้มากหรือน้อยก็รวมถึงกรณีที่ไฟล์ไม่ได้ถูกแคชอย่างสมบูรณ์เพื่อเริ่มต้นด้วย แต่เมื่อ OS อ่านล่วงหน้าดีพอที่จะทำให้มันปรากฏขึ้นมา (เช่นหน้ามักจะถูกแคชเมื่อคุณ ต้องการ). นี่เป็นปัญหาที่ลึกซึ้ง แต่เพราะวิธีการทำงานข้างหน้าอ่านมักจะค่อนข้างแตกต่างระหว่างmmap
และread
สายและสามารถปรับเปลี่ยนต่อไปโดย "คำแนะนำ" โทรตามที่อธิบายใน2
2 ... เพราะถ้าไฟล์ไม่ถูกเก็บไว้พฤติกรรมของคุณจะถูกครอบงำโดยความกังวลของ IO อย่างสมบูรณ์รวมถึงความเห็นอกเห็นใจของรูปแบบการเข้าถึงของคุณกับฮาร์ดแวร์ที่อยู่ภายใต้ - และความพยายามทั้งหมดของคุณควรจะทำให้มั่นใจได้ว่า เป็นไปได้เช่นผ่านการใช้madvise
หรือการfadvise
โทร (และการเปลี่ยนแปลงระดับแอปพลิเคชันใด ๆ ที่คุณสามารถทำได้เพื่อปรับปรุงรูปแบบการเข้าถึง)
3คุณสามารถหลีกเลี่ยงสิ่งนั้นได้เช่นโดยเรียงลำดับmmap
ไอเอ็นจีในหน้าต่างที่มีขนาดเล็กลงให้พูดว่า 100 MB
4ในความเป็นจริงมันกลับกลายเป็นMAP_POPULATE
วิธี (อย่างน้อยหนึ่งชุดฮาร์ดแวร์ / OS) เพียงเล็กน้อยเร็วกว่าไม่ได้ใช้อาจเป็นเพราะเคอร์เนลใช้faultaround - ดังนั้นจำนวนจริงของความผิดพลาดเล็กน้อยจะลดลงโดยปัจจัย 16 หรือไม่ก็.
mmap()
คือ 2-6 ครั้งเร็วกว่าการใช้ syscallsread()
เช่น