เหตุใด memcpy () และ memmove () จึงเร็วกว่าการเพิ่มตัวชี้


92

ฉันกำลังคัดลอก N ไบต์จากไปpSrc pDestสามารถทำได้ในลูปเดียว:

for (int i = 0; i < N; i++)
    *pDest++ = *pSrc++

ทำไมช้ากว่านี้memcpyหรือmemmove? พวกเขาใช้เทคนิคอะไรเพื่อเร่งความเร็ว?


2
ลูปของคุณคัดลอกเพียงตำแหน่งเดียว ฉันคิดว่าคุณตั้งใจที่จะเพิ่มคำแนะนำ
Mysticial

13
หรือคุณสามารถแก้ไขให้พวกเขาได้เหมือนที่ฉันทำ และ BTW ไม่มีโปรแกรมเมอร์ C จริงเคยนับจาก1ไปNก็มักจะจาก0ไปN-1:-)
paxdiablo

6
@paxdiablo: หากคุณวนลูปเหนืออาร์เรย์ให้แน่ใจ แต่มีหลายกรณีที่การวนซ้ำจาก 1 ถึง N นั้นใช้ได้ ขึ้นอยู่กับว่าคุณกำลังทำอะไรกับข้อมูล - หากคุณกำลังแสดงรายการที่มีหมายเลขเริ่มต้นที่ 1 เช่นให้กับผู้ใช้การเริ่มต้นที่ 1 อาจเหมาะสมกว่า ไม่ว่าในกรณีใดก็ตามจะไม่สนใจปัญหาใหญ่ที่ใช้intเป็นตัวนับเมื่อsize_tควรใช้ประเภทที่ไม่ได้ลงชื่อเช่นแทน
Billy ONeal

2
@paxdiablo นอกจากนี้คุณยังสามารถนับจาก N ถึง 1 ในโปรเซสเซอร์บางตัวที่จะกำจัดหนึ่งคำสั่งเปรียบเทียบเนื่องจากการลดลงจะตั้งค่าบิตที่เหมาะสมสำหรับคำสั่งสาขาเมื่อถึงศูนย์
onemasse

6
ฉันคิดว่าหลักฐานของคำถามเป็นเท็จ คอมไพเลอร์สมัยใหม่จะแปลงสิ่งนี้เป็นmemcpyหรือmemmove(ขึ้นอยู่กับว่าพวกเขาสามารถบอกได้ว่าพอยน์เตอร์อาจใช้นามแฝงหรือไม่)
David Schwartz

คำตอบ:


120

เนื่องจาก memcpy ใช้ตัวชี้คำแทนตัวชี้ไบต์การใช้งาน memcpy มักเขียนด้วยคำแนะนำSIMDซึ่งทำให้สามารถสับเปลี่ยนได้ครั้งละ 128 บิต

คำแนะนำ SIMD คือคำแนะนำในการประกอบที่สามารถดำเนินการเดียวกันกับแต่ละองค์ประกอบในเวกเตอร์ที่มีความยาวสูงสุด 16 ไบต์ ซึ่งรวมถึงคำแนะนำในการโหลดและจัดเก็บ


15
เมื่อคุณเปลี่ยนเป็น GCC -O3มันจะใช้ SIMD สำหรับลูปอย่างน้อยก็ถ้ามันรู้pDestและpSrcไม่ใช้นามแฝง
Dietrich Epp

ฉันกำลังทำงานกับ Xeon Phi ที่มี SIMD 64 ไบต์ (512 บิต) ดังนั้นสิ่งที่ "สูงสุด 16 ไบต์" นี้ทำให้ฉันยิ้มได้ นอกจากนี้คุณต้องระบุซีพียูที่คุณกำหนดเป้าหมายเพื่อเปิดใช้งาน SIMD ตัวอย่างเช่นด้วย -march = native
yakoudbz

บางทีฉันควรทบทวนคำตอบของฉัน :)
onemasse

สิ่งนี้ล้าสมัยอย่างมากแม้ในเวลาโพสต์ เวกเตอร์ AVX บน x86 (จัดส่งในปี 2554) มีความยาว 32 ไบต์และ AVX-512 ยาว 64 ไบต์ มีสถาปัตยกรรมบางอย่างที่มีเวกเตอร์ 1024 บิตหรือ 2048 บิตหรือแม้แต่เวกเตอร์ที่มีความกว้างตัวแปรเช่น ARM SVE
phuclv

@phuclv ในขณะที่อาจมีคำแนะนำคุณมีหลักฐานว่า memcpy ใช้หรือไม่? โดยปกติจะใช้เวลาสักพักกว่าห้องสมุดจะตามทันและล่าสุดที่ฉันพบได้ใช้ SSSE3 และล่าสุดมากกว่าปี 2011 มาก
Pete Kirkham

81

รูทีนการคัดลอกหน่วยความจำอาจซับซ้อนและรวดเร็วกว่าการคัดลอกหน่วยความจำแบบธรรมดาผ่านพอยน์เตอร์เช่น:

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 ++) เนื่องจากคุณต้องวางโหลดและจัดเก็บคำแนะนำด้วยตนเองเพื่อให้ได้ประโยชน์สูงสุดของการซ่อนเวลาแฝงและปริมาณงาน

โดยทั่วไปควรคัดลอกบรรทัดข้อมูลแคชทั้งหมดในการวนซ้ำครั้งเดียวของลูปที่ไม่มีการควบคุม

ซึ่งนำฉันไปสู่การปรับปรุงครั้งต่อไปโดยเพิ่มการดึงข้อมูลล่วงหน้า นี่คือคำสั่งพิเศษที่บอกให้ระบบแคชของโปรเซสเซอร์โหลดส่วนต่างๆของหน่วยความจำลงในแคช เนื่องจากมีความล่าช้าระหว่างการออกคำสั่งและการเติมบรรทัดแคชจึงจำเป็นต้องวางคำแนะนำในลักษณะดังกล่าวเพื่อให้ข้อมูลพร้อมใช้งานเมื่อต้องการคัดลอกและไม่ช้าก็เร็ว

ซึ่งหมายถึงการใส่คำแนะนำในการดึงข้อมูลล่วงหน้าไว้ที่จุดเริ่มต้นของฟังก์ชันและในลูปการคัดลอกหลัก ด้วยคำแนะนำในการดึงข้อมูลล่วงหน้าที่อยู่ตรงกลางของการดึงข้อมูลลูปการคัดลอกซึ่งจะถูกคัดลอกในเวลาการทำซ้ำหลายครั้ง

ฉันจำไม่ได้ แต่การดึงข้อมูลที่อยู่ปลายทางและต้นทางไว้ล่วงหน้าอาจเป็นประโยชน์

ปัจจัย

ปัจจัยหลักที่ส่งผลต่อความรวดเร็วในการคัดลอกหน่วยความจำ ได้แก่ :

  • เวลาแฝงระหว่างโปรเซสเซอร์แคชและหน่วยความจำหลัก
  • ขนาดและโครงสร้างของแคชของโปรเซสเซอร์
  • คำแนะนำในการย้าย / คัดลอกหน่วยความจำของโปรเซสเซอร์ (latency, throughput, register size, etc)

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


ซีพียูสมัยใหม่จะตรวจจับรูปแบบการเข้าถึงหน่วยความจำเชิงเส้นและเริ่มการดึงข้อมูลล่วงหน้าด้วยตัวเอง ฉันคาดว่าคำแนะนำในการดึงข้อมูลล่วงหน้าจะไม่สร้างความแตกต่างมากนักเพราะเหตุนี้
maxy

@maxy ในสถาปัตยกรรมบางอย่างที่ฉันใช้ขั้นตอนการคัดลอกหน่วยความจำการเพิ่มการดึงข้อมูลล่วงหน้าช่วยในการวัดผลได้ แม้ว่าอาจจะเป็นความจริงที่ชิป Intel / AMD รุ่นปัจจุบันสามารถดึงข้อมูลล่วงหน้าได้ไกลพอสมควร แต่ก็ยังมีชิปรุ่นเก่าและสถาปัตยกรรมอื่น ๆ อีกมากมายที่ไม่มี
Daemin

ใครช่วยอธิบาย "(b_src & 0x3)! = 0"? ฉันไม่เข้าใจและ - มันจะไม่คอมไพล์ (แสดงข้อผิดพลาด: ตัวดำเนินการที่ไม่ถูกต้องเป็นไบนารี &: ถ่านที่ไม่ได้ลงชื่อและ int);
Maverick Meerkat

"(b_src & 0x3)! = 0" กำลังตรวจสอบว่า 2 บิตต่ำสุดไม่ใช่ 0 หรือไม่ดังนั้นหากตัวชี้ต้นทางถูกจัดตำแหน่งให้เป็นจำนวน 4 ไบต์หรือไม่ ข้อผิดพลาดในการคอมไพล์ของคุณเกิดขึ้นเนื่องจากถือว่า 0x3 เป็นไบต์ไม่ใช่ในคุณสามารถแก้ไขได้โดยใช้ 0x00000003 หรือ 0x3i (ฉันคิดว่า)
Daemin

b_src & 0x3จะไม่คอมไพล์เนื่องจากคุณไม่ได้รับอนุญาตให้คำนวณเลขคณิตแบบบิตในประเภทตัวชี้ ต้องแคสให้ได้(u)intptr_tก่อน
phuclv

18

memcpyสามารถคัดลอกได้มากกว่าหนึ่งไบต์ในครั้งเดียวขึ้นอยู่กับสถาปัตยกรรมของคอมพิวเตอร์ คอมพิวเตอร์สมัยใหม่ส่วนใหญ่สามารถทำงานกับ 32 บิตหรือมากกว่าในคำสั่งโปรเซสเซอร์เดียว

จากการใช้งานตัวอย่างหนึ่ง :

    00026 * เพื่อความรวดเร็วในการคัดลอกให้ปรับกรณีทั่วไปให้เหมาะสมซึ่งทั้งสองพอยน์เตอร์
    00027 * และความยาวจะเรียงกันเป็นคำและคัดลอกทีละคำแทน
    00028 * ของไบต์ในแต่ละครั้ง มิฉะนั้นให้คัดลอกเป็นไบต์

8
ใน 386 (ตัวอย่างหนึ่ง) ซึ่งไม่มีแคชบนบอร์ดสิ่งนี้สร้างความแตกต่างอย่างมาก สำหรับโปรเซสเซอร์ที่ทันสมัยส่วนใหญ่การอ่านและเขียนจะเกิดขึ้นทีละบรรทัดแคชและบัสไปยังหน่วยความจำมักจะเป็นคอขวดดังนั้นคาดว่าจะมีการปรับปรุงไม่กี่เปอร์เซ็นต์ไม่ใช่ที่ใดก็ได้ใกล้เคียงกับสี่เท่า
Jerry Coffin

2
ฉันคิดว่าคุณน่าจะชัดเจนกว่านี้เมื่อพูดว่า "จากแหล่งที่มา" แน่นอนว่านั่นคือ "แหล่งที่มา" ในบางสถาปัตยกรรม แต่ก็ไม่ได้อยู่ในเครื่อง BSD หรือ Windows (และนรกแม้ระหว่างระบบ GNU มักจะมีความแตกต่างกันมากในฟังก์ชันนี้)
Billy ONeal

@Billy ONeal: +1 อย่างถูกต้อง ... มีมากกว่าหนึ่งวิธีในการถลกหนังแมว นั่นเป็นเพียงตัวอย่างเดียว แก้ไขแล้ว! ขอบคุณสำหรับความคิดเห็นที่สร้างสรรค์
Mark Byers

7

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

  1. ใช้หน่วยที่ใหญ่กว่าเช่นคำ 32 บิตแทนไบต์ คุณยังสามารถ (หรืออาจต้อง) จัดการกับการจัดตำแหน่งที่นี่เช่นกัน คุณไม่สามารถอ่าน / เขียนคำ 32 บิตไปยังตำแหน่งหน่วยความจำแปลก ๆ ได้เช่นในบางแพลตฟอร์มและในแพลตฟอร์มอื่น ๆ คุณจะต้องจ่ายค่าปรับประสิทธิภาพจำนวนมาก ในการแก้ไขปัญหานี้แอดเดรสจะต้องเป็นหน่วยที่หารด้วย 4 ได้คุณสามารถใช้สิ่งนี้ได้สูงสุด 64 บิตสำหรับ CPU 64 บิตหรือสูงกว่านั้นโดยใช้คำแนะนำSIMD (คำสั่งเดี่ยว, ข้อมูลหลายข้อมูล) ( MMX , SSEฯลฯ )

  2. คุณสามารถใช้คำสั่ง CPU พิเศษที่คอมไพเลอร์ของคุณอาจไม่สามารถปรับให้เหมาะสมจาก C ได้ตัวอย่างเช่นใน 80386 คุณสามารถใช้คำสั่งนำหน้า "rep" + คำสั่ง "movsb" เพื่อย้าย N ไบต์ที่กำหนดโดยการวาง N ในการนับ ลงทะเบียน. คอมไพเลอร์ที่ดีจะทำสิ่งนี้ให้คุณ แต่คุณอาจอยู่บนแพลตฟอร์มที่ขาดคอมไพเลอร์ที่ดี โปรดทราบว่าตัวอย่างดังกล่าวมีแนวโน้มที่จะแสดงให้เห็นถึงความเร็วที่ไม่ดี แต่เมื่อรวมกับการจัดตำแหน่ง + คำสั่งหน่วยที่ใหญ่กว่าอาจเร็วกว่าทุกอย่างใน CPU บางตัว

  3. การคลายการวนซ้ำ - สาขาอาจมีราคาค่อนข้างแพงในซีพียูบางตัวดังนั้นการคลายการวนซ้ำอาจทำให้จำนวนสาขาลดลง นี่เป็นเทคนิคที่ดีในการใช้ร่วมกับคำแนะนำ SIMD และหน่วยขนาดใหญ่มาก

ตัวอย่างเช่นhttp://www.agner.org/optimize/#asmlibมีmemcpyการนำไปใช้งานที่ทำได้ดีที่สุด (ในจำนวนที่น้อยมาก) หากคุณอ่านซอร์สโค้ดมันจะเต็มไปด้วยโค้ดแอสเซมบลีแบบอินไลน์จำนวนมากซึ่งดึงเอาเทคนิคสามข้อข้างต้นทั้งหมดออกมาโดยเลือกว่าจะใช้เทคนิคใดตามซีพียูที่คุณใช้งานอยู่

หมายเหตุมีการเพิ่มประสิทธิภาพที่คล้ายกันซึ่งสามารถสร้างขึ้นเพื่อค้นหาไบต์ในบัฟเฟอร์ได้เช่นกัน strchr()และเพื่อน ๆ มักจะเร็วกว่ามือของคุณในการรีด นี่คือความจริงโดยเฉพาะอย่างยิ่งสำหรับ.NETและJava ตัวอย่างเช่นใน. NET บิวท์อินString.IndexOf()จะเร็วกว่าการค้นหาสตริง Boyer – Mooreมากเนื่องจากใช้เทคนิคการเพิ่มประสิทธิภาพข้างต้น


1
เช่นเดียวกับที่ Agner หมอกคุณกำลังเชื่อมโยงไปยัง theorizes ที่คลี่ห่วงต่อต้านในซีพียูที่ทันสมัย

ซีพียูส่วนใหญ่ในปัจจุบันมีการทำนายสาขาที่ดีซึ่งควรลบล้างประโยชน์ของการคลายลูปในกรณีทั่วไป คอมไพเลอร์การปรับให้เหมาะสมที่ดียังสามารถใช้งานได้ในบางครั้ง
thomasrutter


4

ฉันไม่รู้ว่ามันถูกใช้ในการใช้งานจริง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 สำหรับรายละเอียด


อุปกรณ์ของ Duff หรือเพียงแค่กลไกการกระโดดเริ่มต้นเป็นวิธีที่ดีในการคัดลอกไบต์ 1..3 (หรือ 1..7) แรกเพื่อให้ตัวชี้อยู่ในแนวเดียวกับขอบเขตที่ดีกว่าซึ่งสามารถใช้คำสั่งย้ายหน่วยความจำที่ใหญ่กว่าได้
Daemin

@MarkByers: รหัสนี้แสดงให้เห็นถึงการดำเนินการที่แตกต่างกันเล็กน้อย ( *toหมายถึงการลงทะเบียนที่แมปหน่วยความจำและไม่ได้เพิ่มขึ้นโดยเจตนา - ดูบทความที่เชื่อมโยงกับ) อย่างที่ฉันคิดว่าฉันพูดชัดเจนคำตอบของฉันไม่ได้พยายามให้มีประสิทธิภาพmemcpyแต่กล่าวถึงเทคนิคที่ค่อนข้างน่าสงสัย
NPE

@Daemin เห็นด้วยอย่างที่คุณบอกว่าคุณสามารถข้าม do {} while () ได้และตัวคอมไพเลอร์จะแปลสวิตช์เป็นตารางกระโดด มีประโยชน์มากเมื่อคุณต้องการดูแลข้อมูลที่เหลือ ควรกล่าวถึงคำเตือนเกี่ยวกับอุปกรณ์ของ Duff ซึ่งเห็นได้ชัดว่าในสถาปัตยกรรมรุ่นใหม่ (ใหม่กว่า x86) การทำนายสาขามีประสิทธิภาพมากจนอุปกรณ์ของ Duff ช้ากว่าการวนซ้ำแบบธรรมดา
onemasse

1
โอ้ไม่ .. ไม่ใช่อุปกรณ์ของ Duff กรุณาอย่าใช้อุปกรณ์ของ Duff กรุณา. ใช้ PGO และให้ฉันคอมไพเลอร์ทำการคลายการวนซ้ำให้คุณในที่ที่เหมาะสม
Billy ONeal

ไม่อุปกรณ์ของ Duff นั้นไม่ได้ถูกนำมาใช้อย่างแน่นอนในการใช้งานสมัยใหม่ใด ๆ
gnasher729

3

เช่นเดียวกับคนอื่น ๆ พูดว่าสำเนา memcpy ที่มีขนาดใหญ่กว่าชิ้น 1 ไบต์ การคัดลอกในขนาดคำจะเร็วกว่ามาก อย่างไรก็ตามการใช้งานส่วนใหญ่จะต้องดำเนินการไปอีกขั้นและเรียกใช้คำแนะนำ MOV (word) หลาย ๆ คำก่อนที่จะวนซ้ำ ข้อได้เปรียบของการคัดลอกในการพูด 8 บล็อกคำต่อลูปคือการวนซ้ำนั้นมีราคาแพง เทคนิคนี้จะลดจำนวนกิ่งที่มีเงื่อนไขลงด้วยปัจจัย 8 โดยเพิ่มประสิทธิภาพการคัดลอกสำหรับบล็อกขนาดยักษ์


1
ฉันไม่คิดว่านี่เป็นเรื่องจริง คุณสามารถคลายการวนซ้ำได้ แต่คุณไม่สามารถคัดลอกข้อมูลในคำสั่งเดียวได้มากกว่าข้อมูลที่ระบุได้ในแต่ละครั้งบนสถาปัตยกรรมเป้าหมาย นอกจากนี้ยังมีค่าใช้จ่ายในการปลดห่วงอีกด้วย ...
Billy ONeal

@Billy ONeal: ฉันไม่คิดว่านั่นคือสิ่งที่ VoidStar หมายถึง การมีคำสั่งย้ายติดต่อกันหลายครั้งจะทำให้ค่าใช้จ่ายในการนับจำนวนหน่วยลดลง
wallyk

@Billy ONeal: คุณพลาดประเด็นนี้ ทีละคำก็เหมือนกับ MOV, JMP, MOV, JMP เป็นต้นโดยที่คุณสามารถทำ MOV MOV MOV MOV MOV JMP ฉันเคยเขียน mempcy มาก่อนและฉันได้เปรียบเทียบหลายวิธีในการทำมัน;)
VoidStar

@wallyk: บางที แต่เขาบอกว่า "คัดลอกชิ้นที่ใหญ่กว่านี้" ซึ่งเป็นไปไม่ได้จริงๆ ถ้าเขาหมายถึงการคลายการวนซ้ำเขาควรจะพูดว่า "การใช้งานส่วนใหญ่ต้องดำเนินการไปอีกขั้นและคลายการวนซ้ำ" คำตอบตามที่เขียนไว้นั้นทำให้เข้าใจผิดได้ดีที่สุดผิดอย่างที่สุด
Billy ONeal

@VoidStar: ตกลง --- ตอนนี้ดีขึ้นแล้ว +1.
Billy ONeal

2

คำตอบที่ดี แต่ถ้าคุณยังต้องการดำเนินการอย่างรวดเร็ว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;
}

แม้จะดีกว่าด้วยการเพิ่มประสิทธิภาพการเข้าถึงหน่วยความจำ


1

เนื่องจากเหมือนกับรูทีนไลบรารีจำนวนมากได้รับการปรับให้เหมาะสมกับสถาปัตยกรรมที่คุณใช้งานอยู่ คนอื่น ๆ ได้โพสต์เทคนิคต่างๆที่สามารถใช้ได้

ให้ทางเลือกใช้รูทีนไลบรารีแทนการหมุนของคุณเอง นี่คือรูปแบบของ DRY ที่ฉันเรียกว่า DRO (Don't Repeat Others) นอกจากนี้กิจวัตรของห้องสมุดมักจะผิดพลาดน้อยกว่าการใช้งานของคุณเอง

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


0

คุณสามารถดูการใช้งาน macOS ของ memset, memcpy และ memmove

ในเวลาบูตระบบปฏิบัติการจะกำหนดโปรเซสเซอร์ที่ทำงานอยู่ มีการสร้างโค้ดที่ปรับให้เหมาะสมโดยเฉพาะสำหรับโปรเซสเซอร์ที่รองรับแต่ละตัวและในเวลาบูตจะเก็บคำสั่ง jmp ไปยังโค้ดที่ถูกต้องในตำแหน่งที่อ่าน / อย่างเดียวคงที่

การใช้งาน C memset, memcpy และ memmove เป็นเพียงการข้ามไปยังตำแหน่งคงที่เท่านั้น

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

Intel ยังเพิ่มคำสั่งแอสเซมเบลอร์ที่ทำให้การทำงานของสตริงเร็วขึ้น ตัวอย่างเช่นคำสั่งเพื่อสนับสนุน strstr ซึ่ง 256 ไบต์เปรียบเทียบในหนึ่งรอบ


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