เหตุใดจึงมีประสิทธิภาพสูงในการคูณอาร์เรย์ 2048x2048 เทียบกับ 2047x2047


127

ฉันกำลังทำการเปรียบเทียบการคูณเมทริกซ์ตามที่กล่าวไว้ก่อนหน้านี้ใน เหตุใด MATLAB จึงเร็วมากในการคูณเมทริกซ์

ตอนนี้ฉันมีปัญหาอื่นเมื่อคูณสองเมทริกซ์ 2048x2048 มีความแตกต่างอย่างมากระหว่าง C # และอื่น ๆ เมื่อฉันลองคูณเมทริกซ์ 2047x2047 เท่านั้นดูเหมือนจะปกติ เพิ่มคนอื่น ๆ เพื่อเปรียบเทียบด้วย

1024x1024 - 10 วินาที

1027x1027 - 10 วินาที

2047x2047 - 90 วินาที

2048x2048 - 300 วินาที

2049x2049 - 91 วินาที (update)

2500x2500 - 166 วินาที

นั่นคือความแตกต่างสามนาทีครึ่งสำหรับเคส 2k คูณ 2k

ใช้อาร์เรย์ 2dim

//Array init like this
int rozmer = 2048;
float[,] matice = new float[rozmer, rozmer];

//Main multiply code
for(int j = 0; j < rozmer; j++)
{
   for (int k = 0; k < rozmer; k++)
   {
     float temp = 0;
     for (int m = 0; m < rozmer; m++)
     {
       temp = temp + matice1[j,m] * matice2[m,k];
     }
     matice3[j, k] = temp;
   }
 }

23
นี่จะเป็นคำถามสอบที่ดีสำหรับการเขียนโปรแกรม C ระดับสูงหรือคลาสการออกแบบระบบปฏิบัติการ ;-)
Dana the Sane

คุณได้ลองทดสอบทั้งอาร์เรย์ [,] และ [] [] แบบหยักรวมทั้ง 32 และ 64 บิตแล้วหรือยัง ฉันทดสอบเพียงไม่กี่ครั้ง แต่ดูเหมือนรอยหยักจะสอดคล้องกับผลลัพธ์ของคุณมากกว่า แต่ 64 บิตที่ขรุขระนั้นสูงฉันไม่รู้ว่ามีฮิวริสติกส์ใน jit ที่ใช้กับสถานการณ์นี้หรือไม่หรือแคชเกี่ยวข้องตามที่แนะนำไว้ก่อนหน้านี้ หากคุณต้องการโซลูชัน GPGPU มีresearch.microsoft.com/en-us/projects/acceleratorซึ่งควรแข่งขันกับเวลาในโพสต์อื่นของคุณ
Kris

คำถามค่อนข้างไร้เดียงสา แต่มีกี่ตัวเลือก (การเพิ่ม / การคูณ) ที่เกี่ยวข้องกับการคูณเมทริกซ์กำลังสองสองตัว?
Nick T

ปัญหาเดียวกันที่นี่stackoverflow.com/questions/12264970/... stackoverflow.com/questions/7905760/...
phuclv

คำตอบ:


61

สิ่งนี้อาจเกี่ยวข้องกับความขัดแย้งในแคช L2 ของคุณ

แคชที่ไม่มีบน matice1 ไม่ใช่ปัญหาเนื่องจากมีการเข้าถึงตามลำดับ อย่างไรก็ตามสำหรับ matice2 ถ้าคอลัมน์เต็มพอดีกับ L2 (เช่นเมื่อคุณเข้าถึง matice2 [0, 0], matice2 [1, 0], matice2 [2, 0] ... ฯลฯ ไม่มีอะไรถูกขับไล่) กว่าจะไม่มีปัญหา แคชพลาดกับ matice2 อย่างใดอย่างหนึ่ง

ตอนนี้เพื่อดูรายละเอียดวิธีการทำงานของแคชหากไบต์แอดเดรสของตัวแปรของคุณคือ X กว่าบรรทัดแคชจะเป็น (X >> 6) & (L - 1) โดยที่ L คือจำนวนบรรทัดแคชทั้งหมดในแคชของคุณ L คือกำลัง 2 เสมอหกมาจากข้อเท็จจริงที่ว่า 2 ^ 6 == 64 ไบต์คือขนาดมาตรฐานของแคชไลน์

ตอนนี้หมายความว่าอย่างไร? หมายความว่าถ้าฉันมีที่อยู่ X และที่อยู่ Y และ (X >> 6) - (Y >> 6) หารด้วย L ได้ (เช่นกำลังใหญ่ของ 2) พวกมันจะถูกเก็บไว้ในแคชไลน์เดียวกัน

ตอนนี้กลับไปที่ปัญหาของคุณว่าอะไรคือความแตกต่างระหว่างปี 2048 และ 2049

เมื่อ 2048 คือขนาดของคุณ:

ถ้าคุณเอา & matice2 [x, k] และ & matice2 [y, k] ผลต่าง (& matice2 [x, k] >> 6) - (& matice2 [y, k] >> 6) จะหารด้วย 2048 * 4 (ขนาด ลอย) พลังมหาศาลของ 2

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

เมื่อขนาดคือ 2049 ความแตกต่างคือ 2049 * 4 ซึ่งไม่ใช่กำลัง 2 ดังนั้นคุณจะมีความขัดแย้งน้อยลงและคอลัมน์ของคุณจะพอดีกับแคชของคุณอย่างปลอดภัย

ตอนนี้เพื่อทดสอบทฤษฎีนี้มีสองสิ่งที่คุณสามารถทำได้:

จัดสรรอาร์เรย์ matice2 อาร์เรย์ของคุณเช่น matice2 [razmor, 4096] และรันด้วย razmor = 1024, 1025 หรือขนาดใดก็ได้และคุณจะเห็นประสิทธิภาพที่แย่มากเมื่อเทียบกับที่คุณมีก่อนหน้านี้ เนื่องจากคุณจัดแนวคอลัมน์ทั้งหมดให้ขัดแย้งกัน

จากนั้นลอง matice2 [razmor, 4097] และเรียกใช้ด้วยขนาดใดก็ได้และคุณจะเห็นประสิทธิภาพที่ดีขึ้นมาก


คุณทำผิดพลาดใน 2 ย่อหน้าสุดท้ายหรือไม่? การลองทั้งสองเหมือนกันทุกประการ :)
Xeo

การเชื่อมโยงแคชยังมีบทบาท
Ben Jackson

20

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

ถ้าคุณใช้เวลาคู่อื่น ๆ (2 ^ n-1,2 ^ n) ฉันคาดหวังว่าคุณจะเห็นเอฟเฟกต์ที่คล้ายกัน

เพื่ออธิบายให้ละเอียดยิ่งขึ้นในวงในซึ่งคุณเข้าถึง matice2 [m, k] มีแนวโน้มว่า matice2 [m, k] และ matice2 [m + 1, k] จะหักล้างกันด้วย 2048 * sizeof (float) จึงแมปกับดัชนีเดียวกันในแคช L1 ด้วย N-way Associative cache คุณจะมีตำแหน่งแคช 1-8 ตำแหน่งสำหรับสิ่งเหล่านี้ทั้งหมด ดังนั้นการเข้าถึงเหล่านี้เกือบทั้งหมดจะทำให้เกิดการขับไล่แคช L1 และการดึงข้อมูลจากแคชหรือหน่วยความจำหลักที่ช้าลง


+1 ฟังดูเป็นไปได้ หนึ่งต้องระวังการเชื่อมโยงแคช
Macke

16

สิ่งนี้อาจเกี่ยวข้องกับขนาดของแคช cpu ของคุณ หากเมทริกซ์ 2 แถวไม่พอดีกันคุณจะเสียเวลาในการสลับองค์ประกอบจาก RAM องค์ประกอบ 4095 พิเศษอาจเพียงพอที่จะป้องกันไม่ให้แถวพอดี

ในกรณีของคุณ 2 แถวสำหรับเมทริกซ์ 2047 2d จะอยู่ภายในหน่วยความจำ 16KB (สมมติว่าเป็นประเภท 32 บิต) ตัวอย่างเช่นหากคุณมีแคช L1 (ใกล้กับซีพียูบนบัสมากที่สุด) ที่ 64KB คุณสามารถใส่อย่างน้อย 4 แถว (จาก 2047 * 32) ลงในแคชพร้อมกันได้ ด้วยแถวที่ยาวขึ้นหากจำเป็นต้องมีช่องว่างภายในที่ดันคู่ของแถวเกิน 16KB สิ่งต่างๆก็เริ่มยุ่งเหยิง นอกจากนี้ทุกครั้งที่คุณ 'พลาด' แคชการแลกเปลี่ยนข้อมูลจากแคชอื่นหรือหน่วยความจำหลักจะทำให้สิ่งต่างๆล่าช้า

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


2
แต่ไม่น่าเป็นไปได้มากที่เขามีแคช CPU 16.7 MB
Marino Šimić

ฉันอัปเดตผลลัพธ์ด้วย 2049x2049 - 91 วินาที ถ้าเป็น "ปัญหาแคช" นี่ยังไม่ควรถึง 300+ วินาทีใช่หรือไม่
Wolf

@Marino คำตอบได้รับการอัปเดตเพื่อคำนึงถึงสิ่งนั้น
Dana the Sane

1
ฉันรู้สึกว่าไม่มีคำอธิบายใดที่สามารถระบุรายละเอียดใหม่ ๆ ได้อย่างเพียงพอเกี่ยวกับขนาดที่หลากหลายและเบาบางที่ทำให้เกิดปัญหาโดยที่คนอื่นไม่ได้รับผลกระทบ
Ken Rockot

2
ฉันไม่คิดว่าคำอธิบายนี้ถูกต้อง ปัญหาอยู่ที่การไม่ใช้ความจุแคชอย่างเต็มที่เนื่องจากความขัดแย้งของสายแคชเมื่อขนาดเป็น 2 นอกจากนี้ระบบปฏิบัติการก็ไม่มีส่วนเกี่ยวข้องกับแคชเพราะไม่ใช่ระบบปฏิบัติการที่ตัดสินใจว่าจะแคชอะไรและจะขับไล่อะไรทั้งหมด ในฮาร์ดแวร์ ระบบปฏิบัติการมีส่วนเกี่ยวข้องกับการจัดตำแหน่งข้อมูล แต่ในกรณีนี้เป็นข้อมูลเกี่ยวกับวิธีที่ C # ตัดสินใจจัดสรรข้อมูลและวิธีการแสดงอาร์เรย์ 2 มิติในหน่วยความจำ OS ไม่มีส่วนเกี่ยวข้องใด ๆ
zviadm

10

Louis Brandy เขียนบล็อกโพสต์สองรายการเพื่อวิเคราะห์ปัญหานี้:

More Cache Craziness and Computational Performance - กรณีศึกษาสำหรับผู้เริ่มต้นที่มีสถิติที่น่าสนใจและพยายามอธิบายพฤติกรรมโดยละเอียดมากขึ้น แต่ก็มีข้อ จำกัด ด้านขนาดแคช


5

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


ส่วนที่ 5 ของลิงก์เกี่ยวกับการเชื่อมโยงแคชดูเหมือนจะใช้โดยเฉพาะ
Dana the Sane

4

ในขณะที่คุณเข้าถึงmatice2อาร์เรย์ในแนวตั้งมันจะถูกสลับเข้าและออกจากแคชมากขึ้น หากคุณมิเรอร์อาร์เรย์ในแนวทแยงเพื่อให้คุณสามารถเข้าถึงได้โดยใช้[k,m]แทน[m,k]โค้ดจะทำงานได้เร็วขึ้นมาก

ฉันทดสอบสิ่งนี้สำหรับเมทริกซ์ 1024x1024 และเร็วกว่าประมาณสองเท่า สำหรับเมทริกซ์ 2048x2048 จะเร็วกว่าประมาณสิบเท่า


นี่ไม่ได้อธิบายว่าทำไมปี 2049 ถึงเร็วกว่าปี 2048
Macke

@ Macke: นั่นเป็นเพราะมันผ่านขีด จำกัด บางอย่างในการแคชหน่วยความจำดังนั้นจึงมีการพลาดแคชมากขึ้น
Guffa

ทำไมต้องโหวตลง? ถ้าคุณไม่พูดในสิ่งที่คุณคิดว่าผิดมันจะไม่สามารถปรับปรุงคำตอบได้
Guffa

โหวตลงอีกโดยไม่มีคำอธิบายใด ๆ ... คำตอบของฉันมีคำว่า "น่าจะ" "เดา" และ "ควร" น้อยเกินไปหรือเปล่าเหมือนกับคำตอบที่ได้รับการโหวตมากที่สุด ... ?
Guffa

4

นามแฝงแคช

หรือแคชเฆี่ยนถ้าฉันสามารถเหรียญคำ

แคชทำงานโดยการสร้างดัชนีด้วยบิตลำดับต่ำและแท็กด้วยบิตลำดับสูง

การสร้างภาพที่แคชของคุณมี 4 คำและเมทริกซ์ของคุณคือ 4 x 4 เมื่อเข้าถึงคอลัมน์และแถวนั้นมีความยาวสองส่วนใด ๆ องค์ประกอบของคอลัมน์ในหน่วยความจำจะแมปกับองค์ประกอบแคชเดียวกัน

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

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

เนื่องจากแคชนั้นเร็วกว่า DRAM อย่างมาก (โดยส่วนใหญ่เป็นอัตราการโจมตีบนชิป) จึงเป็นทุกอย่าง


2

ดูเหมือนว่าคุณจะถึงขีด จำกัด ขนาดแคชหรืออาจมีปัญหาในการทำซ้ำในการกำหนดเวลาของคุณ

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


1
ฉันรู้จัก BLAS แต่งานนี้ไม่ได้ทำให้เร็วที่สุดเท่าที่จะเป็นไปได้ แต่ต้องเขียนและทดสอบในภาษาต่างๆ นี่เป็นปัญหาที่แปลกมากสำหรับฉันและ Iam ก็สงสัยจริงๆว่าทำไมผลลัพธ์ถึงเป็นเช่นนั้น
Wolf

3
@ หมาป่าฉันรู้สึกว่ายากที่จะรู้สึกตื่นเต้นว่าสิ่งที่ควรใช้เวลาสักวินาทีนั้นใช้เวลา 90 วินาทีหรือ 300 วินาที
David Heffernan

4
วิธีที่ดีที่สุดในการเรียนรู้วิธีการทำงานคือการเขียนด้วยตัวคุณเองและดูว่าคุณสามารถปรับปรุงการนำไปใช้งานได้อย่างไร นี่คือ (หวังว่า) สิ่งที่ Wolf กำลังทำ
Callum Rogers

@Callum Rogers เห็นด้วย นั่นคือวิธีที่ฉันได้เรียนรู้ความสำคัญของขนาดบัฟเฟอร์ในการคัดลอกไฟล์
Kelly S. French

1

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


อืม - แต่อาร์เรย์ที่ประกาศเป็น 2D (float [,] matice = new float [rozmer, rozmer];) จะถูกจัดสรรใน RAM เป็นอาร์เรย์มิติเดียวและการคำนวณแถว / ก้าวที่ทำภายใต้ประทุน เหตุใดจึงประกาศเป็น 1D และการคำนวณแถว / ก้าวด้วยตนเองจึงเร็วกว่า? คุณหมายถึง sol'n จัดสรรอาร์เรย์ขนาดใหญ่เป็นอาร์เรย์ของกระเบื้องขนาดเล็กซึ่งแต่ละอันสามารถใส่ลงในแคชที่อาร์เรย์ขนาดใหญ่ไม่ได้หรือไม่?
Eric M

1
หากไลบรารีของคุณหรือเครื่องมือใด ๆ ที่คุณใช้ทำการปูกระเบื้องคุณก็ไม่จำเป็นต้องทำ แต่ถ้าคุณใช้อาร์เรย์ 2 มิติแบบเดิมพูดว่า C / C ++ การเรียงต่อกันจะช่วยเพิ่มประสิทธิภาพ
Arlen

0

ฉันสงสัยว่าเป็นผลมาจากสิ่งที่เรียกว่า " Sequential Flooding " นี่คืออะไรที่คุณพยายามวนซ้ำรายการวัตถุที่มีขนาดใหญ่กว่าขนาดแคชเล็กน้อยดังนั้นทุกคำขอไปยังรายการ (อาร์เรย์) จะต้องทำจากหน่วยความจำและคุณจะไม่ได้รับแคชเดียว ตี.

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


1
ไม่ถูกต้อง 2049 เร็วกว่าปี 2048 ซึ่งหักล้างข้อเรียกร้องของคุณ
Macke

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