เหตุใดโปรแกรมของฉันจึงช้าเมื่อวนลูปมากกว่า 8192 องค์ประกอบ


755

นี่คือสารสกัดจากโปรแกรมที่เป็นปัญหา เมทริกซ์img[][]มีขนาด SIZE × SIZE และเริ่มต้นได้ที่:

img[j][i] = 2 * j + i

จากนั้นคุณสร้างเมทริกซ์res[][]และแต่ละฟิลด์ในที่นี้จะถูกทำให้เป็นค่าเฉลี่ยของ 9 ฟิลด์รอบ ๆ มันในเมทริกซ์ img เส้นขอบจะเหลือ 0 เพื่อความเรียบง่าย

for(i=1;i<SIZE-1;i++) 
    for(j=1;j<SIZE-1;j++) {
        res[j][i]=0;
        for(k=-1;k<2;k++) 
            for(l=-1;l<2;l++) 
                res[j][i] += img[j+l][i+k];
        res[j][i] /= 9;
}

นั่นคือทั้งหมดที่มีให้กับโปรแกรม เพื่อความสมบูรณ์ 'นี่คือสิ่งที่มาก่อน ไม่มีรหัสมา อย่างที่คุณเห็นมันเป็นเพียงการเริ่มต้น

#define SIZE 8192
float img[SIZE][SIZE]; // input image
float res[SIZE][SIZE]; //result of mean filter
int i,j,k,l;
for(i=0;i<SIZE;i++) 
    for(j=0;j<SIZE;j++) 
        img[j][i] = (2*j+i)%8196;

โดยทั่วไปโปรแกรมนี้จะช้าเมื่อ SIZE มีค่าเท่ากับ 2048 เช่นเวลาดำเนินการ:

SIZE = 8191: 3.44 secs
SIZE = 8192: 7.20 secs
SIZE = 8193: 3.18 secs

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

นอกจากนี้วิธีการแก้ไขนี้จะดี แต่ถ้ามีคนสามารถอธิบายเวลาดำเนินการเหล่านี้ฉันมีความสุขมากพอ

ฉันรู้แล้วว่า malloc / free แต่ปัญหาไม่ใช่จำนวนหน่วยความจำที่ใช้มันเป็นเพียงเวลาดำเนินการดังนั้นฉันจึงไม่ทราบว่าจะช่วยได้อย่างไร


67
@bokan มันเกิดขึ้นเมื่อขนาดเป็นหลายก้าวสำคัญของแคช
Luchian Grigore

5
@ อย่างเป็นทางการมันไม่สำคัญว่ามันจะทำให้เกิดปัญหาที่แน่นอนเหมือนกัน; รหัสอาจแตกต่างกัน แต่โดยทั่วไปคำถามทั้งสองถามเกี่ยวกับเวลาเดียวกัน (และชื่อของพวกเขาคล้ายกันแน่นอน)
Griwes

33
คุณไม่ควรประมวลผลภาพโดยใช้อาร์เรย์ 2 มิติหากคุณต้องการประสิทธิภาพสูง พิจารณาพิกเซลทั้งหมดที่อยู่ใน raw และประมวลผลพวกเขาเหมือนอาร์เรย์หนึ่งมิติ ทำภาพเบลอนี้ในสองรอบ เพิ่มมูลค่าของพิกเซลโดยรอบโดยใช้ผลรวมการเลื่อน 3 พิกเซล: slideSum + = src [i + 1] -src [i-1]; ปลายทาง [ผม] = slideSum ;. จากนั้นทำสิ่งเดียวกันในแนวตั้งและหารในเวลาเดียวกัน: dest [i] = (src [i-width] + src [i] + src [i + width]) / 9 www-personal.engin.umd.umich.edu/~jwvm/ece581/18_RankedF.pdf
bokan

8
ที่จริงมีสองสิ่งเกิดขึ้นที่นี่ มันไม่ได้เป็นเพียงการจัดตำแหน่ง super
Mysticial

7
(เพียงเล็กน้อย nitpick ในคำตอบของคุณสำหรับส่วนรหัสแรกมันคงจะดีถ้าลูปของคุณมีเครื่องหมายปีกกา)
Trevor Boyd Smith

คำตอบ:


954

ความแตกต่างนั้นเกิดจากปัญหาการจัดเรียงแนวเดียวกันจากคำถามที่เกี่ยวข้องต่อไปนี้:

แต่นั่นเป็นเพราะมีปัญหาอีกอย่างหนึ่งเกี่ยวกับรหัส

เริ่มต้นจากวงเดิม:

for(i=1;i<SIZE-1;i++) 
    for(j=1;j<SIZE-1;j++) {
        res[j][i]=0;
        for(k=-1;k<2;k++) 
            for(l=-1;l<2;l++) 
                res[j][i] += img[j+l][i+k];
        res[j][i] /= 9;
}

การสังเกตครั้งแรกว่าลูปภายในทั้งสองนั้นเล็กน้อย พวกเขาสามารถ unrolled ดังนี้

for(i=1;i<SIZE-1;i++) {
    for(j=1;j<SIZE-1;j++) {
        res[j][i]=0;
        res[j][i] += img[j-1][i-1];
        res[j][i] += img[j  ][i-1];
        res[j][i] += img[j+1][i-1];
        res[j][i] += img[j-1][i  ];
        res[j][i] += img[j  ][i  ];
        res[j][i] += img[j+1][i  ];
        res[j][i] += img[j-1][i+1];
        res[j][i] += img[j  ][i+1];
        res[j][i] += img[j+1][i+1];
        res[j][i] /= 9;
    }
}

นั่นก็ทำให้วงรอบนอกทั้งสองวงที่เราสนใจ

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

คุณกำลังทำซ้ำคอลัมน์เมทริกซ์ฉลาดแทนแถวฉลาด


เพื่อแก้ปัญหานี้คุณควรแลกเปลี่ยนสองลูป

for(j=1;j<SIZE-1;j++) {
    for(i=1;i<SIZE-1;i++) {
        res[j][i]=0;
        res[j][i] += img[j-1][i-1];
        res[j][i] += img[j  ][i-1];
        res[j][i] += img[j+1][i-1];
        res[j][i] += img[j-1][i  ];
        res[j][i] += img[j  ][i  ];
        res[j][i] += img[j+1][i  ];
        res[j][i] += img[j-1][i+1];
        res[j][i] += img[j  ][i+1];
        res[j][i] += img[j+1][i+1];
        res[j][i] /= 9;
    }
}

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


Core i7 920 @ 3.5 GHz

รหัสเดิม:

8191: 1.499 seconds
8192: 2.122 seconds
8193: 1.582 seconds

Interchanged Outer-Loops:

8191: 0.376 seconds
8192: 0.357 seconds
8193: 0.351 seconds

217
ฉันจะทราบด้วยว่าการปลดลูปด้านในนั้นไม่มีผลต่อประสิทธิภาพการทำงาน คอมไพเลอร์อาจทำโดยอัตโนมัติ ฉันคลี่ออกเพื่อจุดประสงค์เดียวในการกำจัดพวกมันเพื่อให้ง่ายต่อการสังเกตปัญหากับลูปด้านนอก
Mysticial

29
และคุณสามารถเร่งโค้ดนี้ให้สูงขึ้นอีกสามส่วนด้วยการแคชผลรวมตามแต่ละแถว แต่นั่นและการเพิ่มประสิทธิภาพอื่น ๆ ที่อยู่นอกขอบเขตของคำถามเดิม
Eric Postpischil

34
@ClickUpvote นี่เป็นปัญหาของฮาร์ดแวร์ (แคช) มันไม่มีอะไรเกี่ยวข้องกับภาษา หากคุณลองใช้ภาษาอื่นที่คอมไพล์หรือ JIT เป็นรหัสเนทีฟคุณอาจจะเห็นผลเหมือนกัน
Mysticial

19
@ClickUpvote: คุณดูเหมือนจะเข้าใจผิด "วงที่สอง" นั้นเป็นเพียงการคลี่คลายลึกลับด้วยมือ นี่คือสิ่งที่คอมไพเลอร์ของคุณเกือบจะทำอยู่แล้วและ Mystical เพียง แต่ทำมันเพื่อให้ปัญหากับลูปด้านนอกชัดเจนขึ้น มันไม่ได้เป็นสิ่งที่คุณควรจะทำเอง
Lily Ballard

154
นี่เป็นตัวอย่างที่สมบูรณ์แบบของคำตอบที่ดีสำหรับ SO: อ้างอิงคำถามที่คล้ายกันอธิบายทีละขั้นตอนวิธีที่คุณเข้าหาอธิบายปัญหาอธิบายวิธีแก้ไขปัญหามีการจัดรูปแบบที่ดีและแม้แต่ตัวอย่างของรหัสที่ใช้ บนเครื่องของคุณ ขอขอบคุณสำหรับการสนับสนุนของคุณ.
MattSayar

57

มีการทดสอบต่อไปนี้กับคอมไพเลอร์ Visual C ++ ตามที่ใช้โดยการติดตั้ง Qt Creator เริ่มต้น (ฉันเดาว่าไม่มีการตั้งค่าสถานะการปรับให้เหมาะสม) เมื่อใช้ GCC ไม่มีความแตกต่างอย่างใหญ่หลวงระหว่างเวอร์ชั่นของ Mystical และรหัส "ที่ได้รับการปรับปรุง" ของฉัน ดังนั้นข้อสรุปคือการเพิ่มประสิทธิภาพของคอมไพเลอร์จะดูแลการเพิ่มประสิทธิภาพของไมโครดีกว่ามนุษย์ (ในที่สุดฉัน) ฉันออกจากส่วนที่เหลือของคำตอบของฉันสำหรับการอ้างอิง


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

pointer + (x + y*width)*(sizeOfOnePixel)

โดยเฉพาะอย่างยิ่งในกรณีนี้การคำนวณและแคชผลรวมของกลุ่มพิกเซลทั้งสามในแนวนอนจะดีกว่าเพราะจะใช้กลุ่มละสามครั้ง

ฉันทำแบบทดสอบเสร็จแล้วและฉันคิดว่ามันคุ้มค่าที่จะแบ่งปัน ผลลัพธ์แต่ละรายการมีค่าเฉลี่ยห้าการทดสอบ

รหัสเดิมโดย user1615209:

8193: 4392 ms
8192: 9570 ms

เวอร์ชั่นของ Mystical:

8193: 2393 ms
8192: 2190 ms

สองรอบโดยใช้อาร์เรย์ 1D: ผ่านครั้งแรกสำหรับผลบวกในแนวนอน, สองสำหรับผลรวมแนวตั้งและค่าเฉลี่ย การผ่านสองครั้งด้วยตัวชี้สามตัวและการเพิ่มขึ้นเช่นนี้:

imgPointer1 = &avg1[0][0];
imgPointer2 = &avg1[0][SIZE];
imgPointer3 = &avg1[0][SIZE+SIZE];

for(i=SIZE;i<totalSize-SIZE;i++){
    resPointer[i]=(*(imgPointer1++)+*(imgPointer2++)+*(imgPointer3++))/9;
}

8193: 938 ms
8192: 974 ms

สองรอบโดยใช้อาร์เรย์ 1D และจัดการกับสิ่งนี้:

for(i=SIZE;i<totalSize-SIZE;i++){
    resPointer[i]=(hsumPointer[i-SIZE]+hsumPointer[i]+hsumPointer[i+SIZE])/9;
}

8193: 932 ms
8192: 925 ms

One pass caching เป็นผลบวกตามแนวนอนเพียงแถวเดียวข้างหน้าดังนั้นพวกมันจึงอยู่ในแคช:

// Horizontal sums for the first two lines
for(i=1;i<SIZE*2;i++){
    hsumPointer[i]=imgPointer[i-1]+imgPointer[i]+imgPointer[i+1];
}
// Rest of the computation
for(;i<totalSize;i++){
    // Compute horizontal sum for next line
    hsumPointer[i]=imgPointer[i-1]+imgPointer[i]+imgPointer[i+1];
    // Final result
    resPointer[i-SIZE]=(hsumPointer[i-SIZE-SIZE]+hsumPointer[i-SIZE]+hsumPointer[i])/9;
}

8193: 599 ms
8192: 652 ms

สรุป:

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

ฉันแน่ใจว่ามันเป็นไปได้ที่จะทำดีกว่ามาก

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


9
"ฉันคิดว่าเร็วกว่าอย่างน้อย 3 ครั้ง" - ยินดีที่จะสำรองข้อมูลการอ้างสิทธิ์นั้นด้วยการวัดหรือการอ้างอิงบางส่วนใช่หรือไม่
Adam Rosenfield

8
@AdamRosenfield "ฉันคิดว่า" = supposition! = "เป็น" = การเรียกร้อง ฉันไม่มีการวัดสำหรับสิ่งนี้และฉันต้องการดูการทดสอบ แต่ฉันต้องการเพิ่มขึ้น 7, 2 ย่อย, 2 เพิ่มและหนึ่ง div ต่อพิกเซล แต่ละลูปใช้ var ท้องถิ่นน้อยกว่าที่มีการลงทะเบียนใน CPU อีกอันต้องการการเพิ่มขึ้น 7 ครั้งการลดลง 6 ครั้ง 1 div และระหว่าง 10 ถึง 20 mul สำหรับการกำหนดที่อยู่ทั้งนี้ขึ้นอยู่กับการปรับแต่งคอมไพเลอร์ นอกจากนี้แต่ละคำสั่งในลูปต้องการผลลัพธ์ของคำสั่งก่อนหน้านี้ทิ้งประโยชน์ของสถาปัตยกรรมซูเปอร์สเกลาร์ของ Pentium ดังนั้นมันจะต้องเร็วขึ้น
bokan

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

1
ทำได้ดีนั่นเป็นตัวเลขที่น่าประทับใจ ตามที่คุณพบมันคือทั้งหมดที่เกี่ยวกับประสิทธิภาพของหน่วยความจำ - การใช้ตัวชี้หลายตัวพร้อมกับส่วนเพิ่มนั้นไม่ได้ให้ประโยชน์ใด ๆ
Adam Rosenfield

2
@ AdamRosenfield ฉันค่อนข้างกังวลเมื่อเช้านี้เพราะฉันไม่สามารถทำซ้ำการทดสอบได้ ปรากฏว่าการเพิ่มประสิทธิภาพนั้นมีคอมไพเลอร์ Visual C ++ เท่านั้น ใช้ gcc มีความแตกต่างเพียงเล็กน้อย
bokan
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.