ฉันกำลังคัดลอก N ไบต์จากไปpSrc
pDest
สามารถทำได้ในลูปเดียว:
for (int i = 0; i < N; i++)
*pDest++ = *pSrc++
ทำไมช้ากว่านี้memcpy
หรือmemmove
? พวกเขาใช้เทคนิคอะไรเพื่อเร่งความเร็ว?
ฉันกำลังคัดลอก N ไบต์จากไปpSrc
pDest
สามารถทำได้ในลูปเดียว:
for (int i = 0; i < N; i++)
*pDest++ = *pSrc++
ทำไมช้ากว่านี้memcpy
หรือmemmove
? พวกเขาใช้เทคนิคอะไรเพื่อเร่งความเร็ว?
1
ไปN
ก็มักจะจาก0
ไปN-1
:-)
int
เป็นตัวนับเมื่อsize_t
ควรใช้ประเภทที่ไม่ได้ลงชื่อเช่นแทน
memcpy
หรือmemmove
(ขึ้นอยู่กับว่าพวกเขาสามารถบอกได้ว่าพอยน์เตอร์อาจใช้นามแฝงหรือไม่)
คำตอบ:
เนื่องจาก memcpy ใช้ตัวชี้คำแทนตัวชี้ไบต์การใช้งาน memcpy มักเขียนด้วยคำแนะนำSIMDซึ่งทำให้สามารถสับเปลี่ยนได้ครั้งละ 128 บิต
คำแนะนำ SIMD คือคำแนะนำในการประกอบที่สามารถดำเนินการเดียวกันกับแต่ละองค์ประกอบในเวกเตอร์ที่มีความยาวสูงสุด 16 ไบต์ ซึ่งรวมถึงคำแนะนำในการโหลดและจัดเก็บ
-O3
มันจะใช้ SIMD สำหรับลูปอย่างน้อยก็ถ้ามันรู้pDest
และpSrc
ไม่ใช้นามแฝง
รูทีนการคัดลอกหน่วยความจำอาจซับซ้อนและรวดเร็วกว่าการคัดลอกหน่วยความจำแบบธรรมดาผ่านพอยน์เตอร์เช่น:
void simple_memory_copy(void* dst, void* src, unsigned int bytes)
{
unsigned char* b_dst = (unsigned char*)dst;
unsigned char* b_src = (unsigned char*)src;
for (int i = 0; i < bytes; ++i)
*b_dst++ = *b_src++;
}
การปรับปรุง
การปรับปรุงครั้งแรกที่ทำได้คือการจัดตำแหน่งหนึ่งในตัวชี้บนขอบเขตคำ (โดยคำว่าฉันหมายถึงขนาดจำนวนเต็มดั้งเดิมโดยปกติคือ 32 บิต / 4 ไบต์ แต่สามารถเป็น 64 บิต / 8 ไบต์ในสถาปัตยกรรมที่ใหม่กว่า) และใช้การย้ายขนาดคำ / copy คำแนะนำ สิ่งนี้ต้องใช้การคัดลอกแบบไบต์เพื่อไบต์จนกว่าตัวชี้จะถูกจัดแนว
void aligned_memory_copy(void* dst, void* src, unsigned int bytes)
{
unsigned char* b_dst = (unsigned char*)dst;
unsigned char* b_src = (unsigned char*)src;
// Copy bytes to align source pointer
while ((b_src & 0x3) != 0)
{
*b_dst++ = *b_src++;
bytes--;
}
unsigned int* w_dst = (unsigned int*)b_dst;
unsigned int* w_src = (unsigned int*)b_src;
while (bytes >= 4)
{
*w_dst++ = *w_src++;
bytes -= 4;
}
// Copy trailing bytes
if (bytes > 0)
{
b_dst = (unsigned char*)w_dst;
b_src = (unsigned char*)w_src;
while (bytes > 0)
{
*b_dst++ = *b_src++;
bytes--;
}
}
}
สถาปัตยกรรมที่แตกต่างกันจะทำงานแตกต่างกันไปขึ้นอยู่กับว่าตัวชี้ต้นทางหรือปลายทางอยู่ในแนวเดียวกันหรือไม่ ตัวอย่างเช่นในโปรเซสเซอร์ XScale ฉันได้รับประสิทธิภาพที่ดีขึ้นโดยการจัดตำแหน่งตัวชี้ปลายทางแทนที่จะเป็นตัวชี้ต้นทาง
ในการปรับปรุงประสิทธิภาพการทำงานให้ดียิ่งขึ้นการคลายลูปบางส่วนสามารถทำได้เพื่อให้รีจิสเตอร์ของโปรเซสเซอร์มีข้อมูลมากขึ้นและนั่นหมายความว่าคำสั่งโหลด / จัดเก็บสามารถแทรกสลับกันได้และซ่อนเวลาแฝงไว้ด้วยคำแนะนำเพิ่มเติม (เช่นการนับลูปเป็นต้น) ประโยชน์ที่ได้รับนี้แตกต่างกันไปเล็กน้อยตามโปรเซสเซอร์เนื่องจากเวลาแฝงของคำสั่งโหลด / จัดเก็บอาจแตกต่างกันมาก
ในขั้นตอนนี้โค้ดจะถูกเขียนใน Assembly แทนที่จะเป็น C (หรือ C ++) เนื่องจากคุณต้องวางโหลดและจัดเก็บคำแนะนำด้วยตนเองเพื่อให้ได้ประโยชน์สูงสุดของการซ่อนเวลาแฝงและปริมาณงาน
โดยทั่วไปควรคัดลอกบรรทัดข้อมูลแคชทั้งหมดในการวนซ้ำครั้งเดียวของลูปที่ไม่มีการควบคุม
ซึ่งนำฉันไปสู่การปรับปรุงครั้งต่อไปโดยเพิ่มการดึงข้อมูลล่วงหน้า นี่คือคำสั่งพิเศษที่บอกให้ระบบแคชของโปรเซสเซอร์โหลดส่วนต่างๆของหน่วยความจำลงในแคช เนื่องจากมีความล่าช้าระหว่างการออกคำสั่งและการเติมบรรทัดแคชจึงจำเป็นต้องวางคำแนะนำในลักษณะดังกล่าวเพื่อให้ข้อมูลพร้อมใช้งานเมื่อต้องการคัดลอกและไม่ช้าก็เร็ว
ซึ่งหมายถึงการใส่คำแนะนำในการดึงข้อมูลล่วงหน้าไว้ที่จุดเริ่มต้นของฟังก์ชันและในลูปการคัดลอกหลัก ด้วยคำแนะนำในการดึงข้อมูลล่วงหน้าที่อยู่ตรงกลางของการดึงข้อมูลลูปการคัดลอกซึ่งจะถูกคัดลอกในเวลาการทำซ้ำหลายครั้ง
ฉันจำไม่ได้ แต่การดึงข้อมูลที่อยู่ปลายทางและต้นทางไว้ล่วงหน้าอาจเป็นประโยชน์
ปัจจัย
ปัจจัยหลักที่ส่งผลต่อความรวดเร็วในการคัดลอกหน่วยความจำ ได้แก่ :
ดังนั้นหากคุณต้องการเขียนกิจวัตรการรับมือกับหน่วยความจำที่มีประสิทธิภาพและรวดเร็วคุณจำเป็นต้องรู้ค่อนข้างมากเกี่ยวกับโปรเซสเซอร์และสถาปัตยกรรมที่คุณกำลังเขียน พอจะพูดได้เว้นแต่คุณจะเขียนบนแพลตฟอร์มแบบฝังตัวมันจะง่ายกว่ามากที่จะใช้รูทีนการคัดลอกหน่วยความจำในตัว
b_src & 0x3
จะไม่คอมไพล์เนื่องจากคุณไม่ได้รับอนุญาตให้คำนวณเลขคณิตแบบบิตในประเภทตัวชี้ ต้องแคสให้ได้(u)intptr_t
ก่อน
memcpy
สามารถคัดลอกได้มากกว่าหนึ่งไบต์ในครั้งเดียวขึ้นอยู่กับสถาปัตยกรรมของคอมพิวเตอร์ คอมพิวเตอร์สมัยใหม่ส่วนใหญ่สามารถทำงานกับ 32 บิตหรือมากกว่าในคำสั่งโปรเซสเซอร์เดียว
00026 * เพื่อความรวดเร็วในการคัดลอกให้ปรับกรณีทั่วไปให้เหมาะสมซึ่งทั้งสองพอยน์เตอร์ 00027 * และความยาวจะเรียงกันเป็นคำและคัดลอกทีละคำแทน 00028 * ของไบต์ในแต่ละครั้ง มิฉะนั้นให้คัดลอกเป็นไบต์
คุณสามารถนำไปmemcpy()
ใช้โดยใช้เทคนิคใด ๆ ต่อไปนี้บางอย่างขึ้นอยู่กับสถาปัตยกรรมของคุณเพื่อเพิ่มประสิทธิภาพและทั้งหมดนี้จะเร็วกว่าโค้ดของคุณมาก:
ใช้หน่วยที่ใหญ่กว่าเช่นคำ 32 บิตแทนไบต์ คุณยังสามารถ (หรืออาจต้อง) จัดการกับการจัดตำแหน่งที่นี่เช่นกัน คุณไม่สามารถอ่าน / เขียนคำ 32 บิตไปยังตำแหน่งหน่วยความจำแปลก ๆ ได้เช่นในบางแพลตฟอร์มและในแพลตฟอร์มอื่น ๆ คุณจะต้องจ่ายค่าปรับประสิทธิภาพจำนวนมาก ในการแก้ไขปัญหานี้แอดเดรสจะต้องเป็นหน่วยที่หารด้วย 4 ได้คุณสามารถใช้สิ่งนี้ได้สูงสุด 64 บิตสำหรับ CPU 64 บิตหรือสูงกว่านั้นโดยใช้คำแนะนำSIMD (คำสั่งเดี่ยว, ข้อมูลหลายข้อมูล) ( MMX , SSEฯลฯ )
คุณสามารถใช้คำสั่ง CPU พิเศษที่คอมไพเลอร์ของคุณอาจไม่สามารถปรับให้เหมาะสมจาก C ได้ตัวอย่างเช่นใน 80386 คุณสามารถใช้คำสั่งนำหน้า "rep" + คำสั่ง "movsb" เพื่อย้าย N ไบต์ที่กำหนดโดยการวาง N ในการนับ ลงทะเบียน. คอมไพเลอร์ที่ดีจะทำสิ่งนี้ให้คุณ แต่คุณอาจอยู่บนแพลตฟอร์มที่ขาดคอมไพเลอร์ที่ดี โปรดทราบว่าตัวอย่างดังกล่าวมีแนวโน้มที่จะแสดงให้เห็นถึงความเร็วที่ไม่ดี แต่เมื่อรวมกับการจัดตำแหน่ง + คำสั่งหน่วยที่ใหญ่กว่าอาจเร็วกว่าทุกอย่างใน CPU บางตัว
การคลายการวนซ้ำ - สาขาอาจมีราคาค่อนข้างแพงในซีพียูบางตัวดังนั้นการคลายการวนซ้ำอาจทำให้จำนวนสาขาลดลง นี่เป็นเทคนิคที่ดีในการใช้ร่วมกับคำแนะนำ SIMD และหน่วยขนาดใหญ่มาก
ตัวอย่างเช่นhttp://www.agner.org/optimize/#asmlibมีmemcpy
การนำไปใช้งานที่ทำได้ดีที่สุด (ในจำนวนที่น้อยมาก) หากคุณอ่านซอร์สโค้ดมันจะเต็มไปด้วยโค้ดแอสเซมบลีแบบอินไลน์จำนวนมากซึ่งดึงเอาเทคนิคสามข้อข้างต้นทั้งหมดออกมาโดยเลือกว่าจะใช้เทคนิคใดตามซีพียูที่คุณใช้งานอยู่
หมายเหตุมีการเพิ่มประสิทธิภาพที่คล้ายกันซึ่งสามารถสร้างขึ้นเพื่อค้นหาไบต์ในบัฟเฟอร์ได้เช่นกัน strchr()
และเพื่อน ๆ มักจะเร็วกว่ามือของคุณในการรีด นี่คือความจริงโดยเฉพาะอย่างยิ่งสำหรับ.NETและJava ตัวอย่างเช่นใน. NET บิวท์อินString.IndexOf()
จะเร็วกว่าการค้นหาสตริง Boyer – Mooreมากเนื่องจากใช้เทคนิคการเพิ่มประสิทธิภาพข้างต้น
คำตอบสั้น ๆ :
ฉันไม่รู้ว่ามันถูกใช้ในการใช้งานจริงmemcpy
หรือไม่ แต่ฉันคิดว่าDuff's Deviceสมควรได้รับการกล่าวถึงที่นี่
จากWikipedia :
send(to, from, count)
register short *to, *from;
register count;
{
register n = (count + 7) / 8;
switch(count % 8) {
case 0: do { *to = *from++;
case 7: *to = *from++;
case 6: *to = *from++;
case 5: *to = *from++;
case 4: *to = *from++;
case 3: *to = *from++;
case 2: *to = *from++;
case 1: *to = *from++;
} while(--n > 0);
}
}
โปรดทราบว่าข้างต้นไม่ได้เป็นการmemcpy
จงใจไม่เพิ่มto
ตัวชี้ มันใช้การดำเนินการที่แตกต่างกันเล็กน้อย: การเขียนลงในรีจิสเตอร์ที่แมปหน่วยความจำ ดูบทความ Wikipedia สำหรับรายละเอียด
*to
หมายถึงการลงทะเบียนที่แมปหน่วยความจำและไม่ได้เพิ่มขึ้นโดยเจตนา - ดูบทความที่เชื่อมโยงกับ) อย่างที่ฉันคิดว่าฉันพูดชัดเจนคำตอบของฉันไม่ได้พยายามให้มีประสิทธิภาพmemcpy
แต่กล่าวถึงเทคนิคที่ค่อนข้างน่าสงสัย
เช่นเดียวกับคนอื่น ๆ พูดว่าสำเนา memcpy ที่มีขนาดใหญ่กว่าชิ้น 1 ไบต์ การคัดลอกในขนาดคำจะเร็วกว่ามาก อย่างไรก็ตามการใช้งานส่วนใหญ่จะต้องดำเนินการไปอีกขั้นและเรียกใช้คำแนะนำ MOV (word) หลาย ๆ คำก่อนที่จะวนซ้ำ ข้อได้เปรียบของการคัดลอกในการพูด 8 บล็อกคำต่อลูปคือการวนซ้ำนั้นมีราคาแพง เทคนิคนี้จะลดจำนวนกิ่งที่มีเงื่อนไขลงด้วยปัจจัย 8 โดยเพิ่มประสิทธิภาพการคัดลอกสำหรับบล็อกขนาดยักษ์
คำตอบที่ดี แต่ถ้าคุณยังต้องการดำเนินการอย่างรวดเร็วmemcpy
ด้วยตัวคุณเองที่มีการโพสต์บล็อกที่น่าสนใจเกี่ยว memcpy รวดเร็วmemcpy ด่วนใน C
void *memcpy(void* dest, const void* src, size_t count)
{
char* dst8 = (char*)dest;
char* src8 = (char*)src;
if (count & 1) {
dst8[0] = src8[0];
dst8 += 1;
src8 += 1;
}
count /= 2;
while (count--) {
dst8[0] = src8[0];
dst8[1] = src8[1];
dst8 += 2;
src8 += 2;
}
return dest;
}
แม้จะดีกว่าด้วยการเพิ่มประสิทธิภาพการเข้าถึงหน่วยความจำ
เนื่องจากเหมือนกับรูทีนไลบรารีจำนวนมากได้รับการปรับให้เหมาะสมกับสถาปัตยกรรมที่คุณใช้งานอยู่ คนอื่น ๆ ได้โพสต์เทคนิคต่างๆที่สามารถใช้ได้
ให้ทางเลือกใช้รูทีนไลบรารีแทนการหมุนของคุณเอง นี่คือรูปแบบของ DRY ที่ฉันเรียกว่า DRO (Don't Repeat Others) นอกจากนี้กิจวัตรของห้องสมุดมักจะผิดพลาดน้อยกว่าการใช้งานของคุณเอง
ฉันเคยเห็นตัวตรวจสอบการเข้าถึงหน่วยความจำบ่นเกี่ยวกับการอ่านนอกขอบเขตบนหน่วยความจำหรือบัฟเฟอร์สตริงซึ่งไม่ใช่ขนาดคำที่หลากหลาย นี่เป็นผลมาจากการใช้การเพิ่มประสิทธิภาพ
คุณสามารถดูการใช้งาน macOS ของ memset, memcpy และ memmove
ในเวลาบูตระบบปฏิบัติการจะกำหนดโปรเซสเซอร์ที่ทำงานอยู่ มีการสร้างโค้ดที่ปรับให้เหมาะสมโดยเฉพาะสำหรับโปรเซสเซอร์ที่รองรับแต่ละตัวและในเวลาบูตจะเก็บคำสั่ง jmp ไปยังโค้ดที่ถูกต้องในตำแหน่งที่อ่าน / อย่างเดียวคงที่
การใช้งาน C memset, memcpy และ memmove เป็นเพียงการข้ามไปยังตำแหน่งคงที่เท่านั้น
การใช้งานจะใช้รหัสที่แตกต่างกันขึ้นอยู่กับการจัดตำแหน่งของต้นทางและปลายทางสำหรับ memcpy และ memmove เห็นได้ชัดว่าพวกเขาใช้ความสามารถของเวกเตอร์ที่มีอยู่ทั้งหมด นอกจากนี้ยังใช้ตัวแปรที่ไม่ใช้แคชเมื่อคุณคัดลอกข้อมูลจำนวนมากและมีคำแนะนำในการลดการรอสำหรับตารางหน้า ไม่ใช่แค่รหัสแอสเซมเบลอร์เท่านั้น แต่เป็นโค้ดแอสเซมเบลอร์ที่เขียนโดยผู้ที่มีความรู้ดีมากเกี่ยวกับสถาปัตยกรรมโปรเซสเซอร์แต่ละตัว
Intel ยังเพิ่มคำสั่งแอสเซมเบลอร์ที่ทำให้การทำงานของสตริงเร็วขึ้น ตัวอย่างเช่นคำสั่งเพื่อสนับสนุน strstr ซึ่ง 256 ไบต์เปรียบเทียบในหนึ่งรอบ