ทำไม memmove เร็วกว่า memcpy?


90

ฉันกำลังตรวจสอบฮอตสปอตประสิทธิภาพในแอปพลิเคชันซึ่งใช้เวลา 50% ใน memmove (3) แอปพลิเคชันจะแทรกจำนวนเต็ม 4 ไบต์นับล้านลงในอาร์เรย์ที่จัดเรียงและใช้ memmove เพื่อเลื่อนข้อมูล "ไปทางขวา" เพื่อให้มีพื้นที่ว่างสำหรับค่าที่แทรก

ความคาดหวังของฉันคือการคัดลอกหน่วยความจำนั้นเร็วมากและฉันรู้สึกประหลาดใจที่ใช้เวลากับ memmove นานมาก แต่แล้วฉันก็มีความคิดว่า memmove ทำงานช้าเพราะมันกำลังเคลื่อนย้ายพื้นที่ที่ทับซ้อนกันซึ่งจะต้องดำเนินการแบบวนซ้ำแทนการคัดลอกหน่วยความจำขนาดใหญ่ ฉันเขียน microbenchmark ขนาดเล็กเพื่อดูว่ามีความแตกต่างด้านประสิทธิภาพระหว่าง memcpy และ memmove หรือไม่โดยคาดหวังว่า memcpy จะชนะมือ

ฉันใช้เกณฑ์มาตรฐานบนสองเครื่อง (core i5, core i7) และเห็นว่า memmove เร็วกว่า memcpy จริง ๆ บน core i7 ที่เก่ากว่าแม้จะเร็วกว่าเกือบสองเท่า! ตอนนี้ผมกำลังหาคำอธิบาย

นี่คือเกณฑ์มาตรฐานของฉัน มันคัดลอก 100 mb ด้วย memcpy จากนั้นย้ายประมาณ 100 mb ด้วย memmove ต้นทางและปลายทางทับซ้อนกัน มีการลอง "ระยะทาง" ต่างๆสำหรับต้นทางและปลายทาง การทดสอบแต่ละครั้งจะทำงาน 10 ครั้งพิมพ์เวลาเฉลี่ย

https://gist.github.com/cruppstahl/78a57cdf937bca3d062c

นี่คือผลลัพธ์บน Core i5 (Linux 3.5.0-54-generic # 81 ~ precision1-Ubuntu SMP x86_64 GNU / Linux, gcc คือ 4.6.3 (Ubuntu / Linaro 4.6.3-1ubuntu5) ตัวเลขในวงเล็บคือ ระยะทาง (ขนาดช่องว่าง) ระหว่างต้นทางและปลายทาง:

memcpy        0.0140074
memmove (002) 0.0106168
memmove (004) 0.01065
memmove (008) 0.0107917
memmove (016) 0.0107319
memmove (032) 0.0106724
memmove (064) 0.0106821
memmove (128) 0.0110633

Memmove ถูกนำไปใช้เป็นรหัสแอสเซมเบลอร์ที่ปรับให้เหมาะสมกับ SSE โดยคัดลอกจากด้านหลังไปด้านหน้า ใช้การดึงข้อมูลฮาร์ดแวร์ล่วงหน้าเพื่อโหลดข้อมูลลงในแคชและคัดลอก 128 ไบต์ไปยังรีจิสเตอร์ XMM จากนั้นเก็บไว้ที่ปลายทาง

( memcpy-ssse3-back.S , บรรทัด 1650 ff)

L(gobble_ll_loop):
    prefetchnta -0x1c0(%rsi)
    prefetchnta -0x280(%rsi)
    prefetchnta -0x1c0(%rdi)
    prefetchnta -0x280(%rdi)
    sub $0x80, %rdx
    movdqu  -0x10(%rsi), %xmm1
    movdqu  -0x20(%rsi), %xmm2
    movdqu  -0x30(%rsi), %xmm3
    movdqu  -0x40(%rsi), %xmm4
    movdqu  -0x50(%rsi), %xmm5
    movdqu  -0x60(%rsi), %xmm6
    movdqu  -0x70(%rsi), %xmm7
    movdqu  -0x80(%rsi), %xmm8
    movdqa  %xmm1, -0x10(%rdi)
    movdqa  %xmm2, -0x20(%rdi)
    movdqa  %xmm3, -0x30(%rdi)
    movdqa  %xmm4, -0x40(%rdi)
    movdqa  %xmm5, -0x50(%rdi)
    movdqa  %xmm6, -0x60(%rdi)
    movdqa  %xmm7, -0x70(%rdi)
    movdqa  %xmm8, -0x80(%rdi)
    lea -0x80(%rsi), %rsi
    lea -0x80(%rdi), %rdi
    jae L(gobble_ll_loop)

ทำไม memmove เร็วกว่าแล้ว memcpy? ฉันคาดว่า memcpy จะคัดลอกหน้าหน่วยความจำซึ่งน่าจะเร็วกว่าการวนซ้ำมาก ในกรณีที่แย่ที่สุดฉันคาดว่า memcpy จะเร็วเท่า memmove

PS: ฉันรู้ว่าฉันไม่สามารถแทนที่ memmove ด้วย memcpy ในรหัสของฉันได้ ฉันรู้ว่าตัวอย่างโค้ดผสม C และ C ++ คำถามนี้เป็นเพียงเพื่อการศึกษาเท่านั้น

อัปเดต 1

ฉันทำการทดสอบรูปแบบต่างๆตามคำตอบที่หลากหลาย

  1. เมื่อเรียกใช้ memcpy สองครั้งการรันครั้งที่สองจะเร็วกว่าครั้งแรก
  2. เมื่อ "แตะ" บัฟเฟอร์ปลายทางของ memcpy ( memset(b2, 0, BUFFERSIZE...)) การเรียกใช้ memcpy ครั้งแรกก็เร็วขึ้นเช่นกัน
  3. memcpy ยังช้ากว่า memmove นิดหน่อย

นี่คือผลลัพธ์:

memcpy        0.0118526
memcpy        0.0119105
memmove (002) 0.0108151
memmove (004) 0.0107122
memmove (008) 0.0107262
memmove (016) 0.0108555
memmove (032) 0.0107171
memmove (064) 0.0106437
memmove (128) 0.0106648

ข้อสรุปของฉัน: จากความคิดเห็นของ @Oliver Charlesworth ระบบปฏิบัติการต้องส่งหน่วยความจำกายภาพทันทีที่เข้าถึงบัฟเฟอร์ปลายทาง memcpy เป็นครั้งแรก (หากมีใครรู้วิธี "พิสูจน์" โปรดเพิ่มคำตอบ! ). นอกจากนี้ตามที่ @Mats Petersson กล่าวว่า memmove เป็นแคชที่เป็นมิตรกว่า memcpy

ขอบคุณสำหรับคำตอบและความคิดเห็นที่ยอดเยี่ยม!


2
คุณดูรหัส memmove คุณดูรหัส memcpy ด้วยหรือไม่?
Oliver Charlesworth

9
ความคาดหวังของฉันคือการคัดลอกหน่วยความจำนั้นเร็วมาก - เฉพาะเมื่อหน่วยความจำอยู่ในแคช L1 เมื่อข้อมูลไม่พอดีกับแคชประสิทธิภาพการคัดลอกของคุณจะลดน้อยลง
Maxim Egorushkin

1
BTW คุณคัดลอกเพียงสาขาเดียวของmemmove. สาขานี้ไม่สามารถจัดการการย้ายเมื่อต้นทางทับซ้อนกับปลายทางและปลายทางอยู่ที่ที่อยู่ต่ำกว่า
Maxim Egorushkin

2
ฉันไม่มีเวลาเข้าถึงเครื่อง Linux ดังนั้นฉันจึงยังไม่สามารถทดสอบทฤษฎีนี้ได้ แต่อีกคำอธิบายที่เป็นไปได้คือovercommitting ; memcpyลูปของคุณเป็นครั้งแรกที่b2มีการเข้าถึงเนื้อหาดังนั้นระบบปฏิบัติการจึงต้องยอมรับหน่วยความจำกายภาพสำหรับมันในขณะที่ดำเนินการไป
Oliver Charlesworth

2
PS: หากนี่เป็นคอขวดฉันจะพิจารณาแนวทางใหม่ วิธีการใส่ค่าลงในรายการหรือโครงสร้างต้นไม้ (เช่นต้นไม้ไบนารี) จากนั้นอ่านค่าลงในอาร์เรย์ที่ส่วนท้าย โหนดในแนวทางดังกล่าวจะเป็นตัวเลือกที่ยอดเยี่ยมสำหรับการจัดสรรพูล พวกเขาจะถูกเพิ่มเข้าไปจนกว่าจะสิ้นสุดเมื่อปล่อยออกมาเป็นจำนวนมาก โดยเฉพาะอย่างยิ่งถ้าคุณรู้ว่าคุณต้องการจำนวนเท่าใดในตอนเริ่มต้น ไลบรารีเพิ่มมีตัวจัดสรรพูล
Persixty

คำตอบ:


58

การmemmoveโทรของคุณกำลังสับหน่วยความจำ 2 ถึง 128 ไบต์ในขณะที่memcpyต้นทางและปลายทางของคุณแตกต่างกันอย่างสิ้นเชิง อย่างใดที่บัญชีสำหรับความแตกต่างของประสิทธิภาพการทำงาน: ถ้าคุณคัดลอกไปยังสถานที่เดียวกันคุณจะเห็นmemcpyปลายขึ้นอาจ smidge ได้เร็วขึ้นเช่นในideone.com :

memmove (002) 0.0610362
memmove (004) 0.0554264
memmove (008) 0.0575859
memmove (016) 0.057326
memmove (032) 0.0583542
memmove (064) 0.0561934
memmove (128) 0.0549391
memcpy 0.0537919

แทบจะไม่มีอะไรอยู่ในนั้น - ไม่มีหลักฐานว่าการเขียนกลับไปยังหน้าหน่วยความจำที่ผิดพลาดไปแล้วนั้นมีผลกระทบมากและเราไม่เห็นเวลาที่ลดลงครึ่งหนึ่งอย่างแน่นอน ... แต่มันแสดงให้เห็นว่าไม่มีอะไรผิดพลาดที่ทำให้memcpyช้าลงโดยไม่จำเป็นเมื่อเทียบกับแอปเปิ้ล - สำหรับแอปเปิ้ล


ฉันคาดว่าแคช CPU จะไม่ก่อให้เกิดความแตกต่างเนื่องจากบัฟเฟอร์ของฉันมีขนาดใหญ่กว่าแคชมาก
cruppstahl

2
แต่แต่ละคนต้องการการเข้าถึงหน่วยความจำหลักจำนวนเท่ากันใช่ไหม? (เช่นอ่าน 100MB และเขียน 100MB) รูปแบบแคชไม่ได้รอบที่ ดังนั้นวิธีเดียวที่จะช้ากว่าอีกวิธีหนึ่งคือถ้าต้องอ่าน / เขียนจาก / ไปยังหน่วยความจำมากกว่าหนึ่งครั้ง
Oliver Charlesworth

2
@Tony D - ข้อสรุปของฉันคือถามคนที่ฉลาดกว่าฉัน;)
cruppstahl

1
นอกจากนี้จะเกิดอะไรขึ้นหากคุณคัดลอกไปที่เดิม แต่ทำmemcpyก่อนอีกครั้ง
Oliver Charlesworth

1
@OliverCharlesworth: การทดสอบครั้งแรกมักจะได้รับความนิยมอย่างมาก แต่การทดสอบ memcpy สองครั้ง: memcpy 0.0688002 0.0583162 | memmove 0.0577443 0.05862 0.0601029 ... ดูideone.com/8EEAcA
Tony Delroy

27

เมื่อคุณใช้memcpyงานการเขียนจะต้องเข้าไปในแคช เมื่อคุณใช้memmoveตำแหน่งที่คุณกำลังคัดลอกในขั้นตอนเล็ก ๆ ไปข้างหน้าหน่วยความจำที่คุณกำลังคัดลอกจะอยู่ในแคชอยู่แล้ว (เนื่องจากอ่าน 2, 4, 16 หรือ 128 ไบต์ "กลับ") ลองทำโดยmemmoveที่ปลายทางมีขนาดหลายเมกะไบต์ (ขนาดแคช> 4 *) และฉันสงสัย (แต่ไม่สามารถทดสอบได้) ว่าคุณจะได้ผลลัพธ์ที่คล้ายกัน

ฉันรับประกันว่า ALL เป็นเรื่องเกี่ยวกับการบำรุงรักษาแคชเมื่อคุณใช้งานหน่วยความจำขนาดใหญ่


+1 ฉันคิดว่าด้วยเหตุผลที่คุณกล่าวมา memmove แบบวนซ้ำย้อนกลับเป็นแคชที่เป็นมิตรกว่า memcpy อย่างไรก็ตามฉันพบว่าเมื่อเรียกใช้การทดสอบ memcpy สองครั้งการวิ่งครั้งที่สองจะเร็วเท่ากับ memmove ทำไม? บัฟเฟอร์มีขนาดใหญ่มากจนการรัน memcpy ครั้งที่สองน่าจะไม่มีประสิทธิภาพ (cache-wise) เท่ากับการรันครั้งแรก ดังนั้นจึงดูเหมือนว่ามีปัจจัยเพิ่มเติมที่ทำให้เกิดการลงโทษด้านประสิทธิภาพ
cruppstahl

3
ในสถานการณ์ที่เหมาะสมวินาทีmemcpyจะเร็วขึ้นอย่างเห็นได้ชัดเนื่องจากมีการเติม TLB ไว้ล่วงหน้า นอกจากนี้วินาทีmemcpyจะไม่ต้องล้างแคชของสิ่งที่คุณอาจต้อง "กำจัด" (บรรทัดแคชที่สกปรกนั้น "ไม่ดี" สำหรับประสิทธิภาพในหลาย ๆ วิธีอย่างไรก็ตามคุณจะต้อง เรียกใช้บางสิ่งเช่น "perf" และตัวอย่างเช่นแคชพลาด TLB คิดถึงเป็นต้น
Mats Petersson

15

ในอดีต memmove และ memcopy เป็นฟังก์ชันเดียวกัน พวกเขาทำงานในลักษณะเดียวกันและมีการใช้งานแบบเดียวกัน จากนั้นก็ตระหนักว่า memcopy ไม่จำเป็นต้อง (และมักไม่ได้กำหนด) เพื่อจัดการพื้นที่ทับซ้อนในลักษณะใดวิธีหนึ่งโดยเฉพาะ

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

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

คุณสามารถเปรียบเทียบสิ่งที่คุณกำลังทำอยู่หรือเพิกเฉยต่อปัญหาและพึ่งพาเกณฑ์มาตรฐานที่ทำกับไลบรารี C ได้

แก้ไข: โอ้สิ่งสุดท้าย; การเปลี่ยนเนื้อหาหน่วยความจำจำนวนมากไปรอบ ๆ นั้นช้ามาก ฉันเดาว่าแอปพลิเคชันของคุณจะทำงานได้เร็วขึ้นด้วยการใช้งาน B-Tree อย่างง่ายเพื่อจัดการกับจำนวนเต็มของคุณ (โอ้คุณโอเค)

แก้ไข 2: เพื่อสรุปการขยายตัวของฉันในความคิดเห็น: microbenchmark เป็นปัญหาที่นี่มันไม่ได้วัดสิ่งที่คุณคิดว่าเป็น งานที่มอบให้กับ memcpy และ memmove นั้นแตกต่างกันอย่างมีนัยสำคัญ หากงานที่กำหนดให้กับ memcpy ทำซ้ำหลายครั้งด้วย memmove หรือ memcpy ผลลัพธ์สุดท้ายจะไม่ขึ้นอยู่กับฟังก์ชันการเปลี่ยนหน่วยความจำที่คุณใช้ UNLESS พื้นที่ที่ทับซ้อนกัน


แต่นั่นคือสิ่งที่เกี่ยวกับ - ฉันกำลังเปรียบเทียบสิ่งที่ฉันกำลังทำอยู่ คำถามนี้เกี่ยวกับการตีความผลลัพธ์ของเกณฑ์มาตรฐานซึ่งขัดแย้งกับสิ่งที่คุณอ้าง - memcpy นั้นเร็วกว่าสำหรับภูมิภาคที่ไม่ทับซ้อนกัน
cruppstahl

ใบสมัครของฉันคือ b-tree! เมื่อใดก็ตามที่มีการแทรกจำนวนเต็มในโหนดแบบลีฟจะถูกเรียกให้สร้างช่องว่าง ฉันกำลังทำงานกับเครื่องมือฐานข้อมูล
cruppstahl

1
คุณกำลังใช้มาตรฐานขนาดเล็กและคุณไม่มี memcopy และ memmove จะเปลี่ยนข้อมูลเดียวกัน ตำแหน่งที่แน่นอนในหน่วยความจำที่ข้อมูลที่คุณกำลังเผชิญอยู่สร้างความแตกต่างให้กับการแคชและจำนวนรอบการเดินทางไปยังหน่วยความจำที่ CPU ต้องทำ
user3710044

แม้ว่าคำตอบนี้จะถูกต้อง แต่ก็ไม่ได้อธิบายว่าทำไมจึงช้าลงในกรณีนี้ แต่โดยพื้นฐานแล้วจะบอกว่า "ช้ากว่าเพราะในบางกรณีอาจช้ากว่า"
Oliver Charlesworth

ฉันกำลังบอกว่าสำหรับสถานการณ์เดียวกันรวมถึงเลย์เอาต์หน่วยความจำเดียวกันในการคัดลอก / ย้ายเกณฑ์มาตรฐานจะเหมือนกันเนื่องจากการใช้งานเหมือนกัน ปัญหาอยู่ในไมโครเบนช์มาร์ก
user3710044

2

"memcpy มีประสิทธิภาพมากกว่า memmove" ในกรณีของคุณคุณอาจไม่ได้ทำสิ่งเดียวกันทั้งหมดในขณะที่คุณเรียกใช้ฟังก์ชันทั้งสอง

โดยทั่วไปให้ใช้ memmove เฉพาะเมื่อคุณต้องการ ใช้เมื่อมีโอกาสที่เหมาะสมมากที่ภูมิภาคต้นทางและปลายทางมีการทับซ้อนกัน

อ้างอิง: https://www.youtube.com/watch?v=Yr1YnOVG-4g Dr. Jerry Cain (Stanford Intro Systems Lecture - 7) เวลา: 36:00 น.

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