ทำไมการเติมแบบวนรอบในแบบวนรอบจึงเร็วกว่าการวนซ้ำแบบรวมกันมาก?


2246

สมมติว่าa1, b1, c1และd1ชี้ไปที่หน่วยความจำและกองรหัสตัวเลขของฉันมีห่วงหลักดังต่อไปนี้

const int n = 100000;

for (int j = 0; j < n; j++) {
    a1[j] += b1[j];
    c1[j] += d1[j];
}

การวนซ้ำนี้จะดำเนินการ 10,000 ครั้งผ่านการforวนรอบนอกอื่น เพื่อเพิ่มความเร็วฉันเปลี่ยนรหัสเป็น:

for (int j = 0; j < n; j++) {
    a1[j] += b1[j];
}

for (int j = 0; j < n; j++) {
    c1[j] += d1[j];
}

คอมไพล์บน MS Visual C ++ 10.0พร้อมการปรับแต่งเต็มรูปแบบและเปิดใช้งานSSE2เป็น 32 บิตบนIntel Core 2 Duo (x64) ตัวอย่างแรกใช้เวลา 5.5 วินาทีและตัวอย่างดับเบิลลูปใช้เวลาเพียง 1.9 วินาที คำถามของฉันคือ: (โปรดอ้างอิงคำถามที่ขอใช้ใหม่ของฉันที่ด้านล่าง)

PS: ฉันไม่แน่ใจว่านี้จะช่วย:

การถอดแยกชิ้นส่วนสำหรับลูปแรกโดยทั่วไปจะมีลักษณะเช่นนี้ (บล็อกนี้ถูกทำซ้ำประมาณห้าครั้งในโปรแกรมเต็ม):

movsd       xmm0,mmword ptr [edx+18h]
addsd       xmm0,mmword ptr [ecx+20h]
movsd       mmword ptr [ecx+20h],xmm0
movsd       xmm0,mmword ptr [esi+10h]
addsd       xmm0,mmword ptr [eax+30h]
movsd       mmword ptr [eax+30h],xmm0
movsd       xmm0,mmword ptr [edx+20h]
addsd       xmm0,mmword ptr [ecx+28h]
movsd       mmword ptr [ecx+28h],xmm0
movsd       xmm0,mmword ptr [esi+18h]
addsd       xmm0,mmword ptr [eax+38h]

แต่ละลูปของตัวอย่างลูปสองครั้งสร้างโค้ดนี้ (บล็อกต่อไปนี้จะถูกทำซ้ำประมาณสามครั้ง):

addsd       xmm0,mmword ptr [eax+28h]
movsd       mmword ptr [eax+28h],xmm0
movsd       xmm0,mmword ptr [ecx+20h]
addsd       xmm0,mmword ptr [eax+30h]
movsd       mmword ptr [eax+30h],xmm0
movsd       xmm0,mmword ptr [ecx+28h]
addsd       xmm0,mmword ptr [eax+38h]
movsd       mmword ptr [eax+38h],xmm0
movsd       xmm0,mmword ptr [ecx+30h]
addsd       xmm0,mmword ptr [eax+40h]
movsd       mmword ptr [eax+40h],xmm0

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

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

อาจเป็นเรื่องที่น่าสนใจที่จะชี้ให้เห็นความแตกต่างระหว่างสถาปัตยกรรมของ CPU / แคชโดยจัดทำกราฟที่คล้ายกันสำหรับ CPU เหล่านี้

PPS: นี่คือรหัสเต็ม มันใช้TBB Tick_Countสำหรับการกำหนดเวลาความละเอียดสูงขึ้นซึ่งสามารถปิดใช้งานได้โดยไม่กำหนดTBB_TIMINGแมโคร:

#include <iostream>
#include <iomanip>
#include <cmath>
#include <string>

//#define TBB_TIMING

#ifdef TBB_TIMING   
#include <tbb/tick_count.h>
using tbb::tick_count;
#else
#include <time.h>
#endif

using namespace std;

//#define preallocate_memory new_cont

enum { new_cont, new_sep };

double *a1, *b1, *c1, *d1;


void allo(int cont, int n)
{
    switch(cont) {
      case new_cont:
        a1 = new double[n*4];
        b1 = a1 + n;
        c1 = b1 + n;
        d1 = c1 + n;
        break;
      case new_sep:
        a1 = new double[n];
        b1 = new double[n];
        c1 = new double[n];
        d1 = new double[n];
        break;
    }

    for (int i = 0; i < n; i++) {
        a1[i] = 1.0;
        d1[i] = 1.0;
        c1[i] = 1.0;
        b1[i] = 1.0;
    }
}

void ff(int cont)
{
    switch(cont){
      case new_sep:
        delete[] b1;
        delete[] c1;
        delete[] d1;
      case new_cont:
        delete[] a1;
    }
}

double plain(int n, int m, int cont, int loops)
{
#ifndef preallocate_memory
    allo(cont,n);
#endif

#ifdef TBB_TIMING   
    tick_count t0 = tick_count::now();
#else
    clock_t start = clock();
#endif

    if (loops == 1) {
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++){
                a1[j] += b1[j];
                c1[j] += d1[j];
            }
        }
    } else {
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                a1[j] += b1[j];
            }
            for (int j = 0; j < n; j++) {
                c1[j] += d1[j];
            }
        }
    }
    double ret;

#ifdef TBB_TIMING   
    tick_count t1 = tick_count::now();
    ret = 2.0*double(n)*double(m)/(t1-t0).seconds();
#else
    clock_t end = clock();
    ret = 2.0*double(n)*double(m)/(double)(end - start) *double(CLOCKS_PER_SEC);
#endif

#ifndef preallocate_memory
    ff(cont);
#endif

    return ret;
}


void main()
{   
    freopen("C:\\test.csv", "w", stdout);

    char *s = " ";

    string na[2] ={"new_cont", "new_sep"};

    cout << "n";

    for (int j = 0; j < 2; j++)
        for (int i = 1; i <= 2; i++)
#ifdef preallocate_memory
            cout << s << i << "_loops_" << na[preallocate_memory];
#else
            cout << s << i << "_loops_" << na[j];
#endif

    cout << endl;

    long long nmax = 1000000;

#ifdef preallocate_memory
    allo(preallocate_memory, nmax);
#endif

    for (long long n = 1L; n < nmax; n = max(n+1, long long(n*1.2)))
    {
        const long long m = 10000000/n;
        cout << n;

        for (int j = 0; j < 2; j++)
            for (int i = 1; i <= 2; i++)
                cout << s << plain(n, m, j, i);
        cout << endl;
    }
}

(แสดง FLOP / s สำหรับค่าที่แตกต่างกันของn)

ป้อนคำอธิบายรูปภาพที่นี่


4
อาจเป็นระบบปฏิบัติการที่ช้าลงในขณะที่ค้นหาหน่วยความจำกายภาพทุกครั้งที่คุณเข้าถึงและมีบางสิ่งบางอย่างเช่นแคชในกรณีที่มีการเข้าถึงรองไปยัง memblock เดียวกัน
AlexTheo

7
คุณกำลังรวบรวมการเพิ่มประสิทธิภาพหรือไม่ นั่นดูเหมือนรหัส asm มากมายสำหรับ O2 ...
Luchian Grigore

1
ฉันถามสิ่งที่ดูเหมือนจะเป็นคำถามที่คล้ายกันเมื่อไม่นานมานี้ หรือคำตอบอาจมีข้อมูลที่น่าสนใจ
Mark Wilkins

61
เพียงแค่จะจู้จี้จุกจิกโค้ดสองตัวนี้ไม่เท่ากันเนื่องจากพอยน์เตอร์ที่อาจซ้อนทับกัน C99 มีrestrictคำสำคัญสำหรับสถานการณ์เช่นนี้ ฉันไม่รู้ว่า MSVC มีบางอย่างที่คล้ายกันหรือไม่ แน่นอนหากเป็นปัญหารหัส SSE จะไม่ถูกต้อง
user510306

8
สิ่งนี้อาจเกี่ยวข้องกับนามแฝงของหน่วยความจำ ด้วยการวนซ้ำหนึ่งd1[j]อาจเป็นนามแฝงด้วยa1[j]ดังนั้นคอมไพเลอร์อาจถอนออกจากการทำ optimisations ในขณะที่ไม่เกิดขึ้นถ้าคุณแยกงานเขียนไปยังหน่วยความจำในสองลูป
rturrado

คำตอบ:


1690

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

ถ้าผมเดาได้อย่างถูกต้องในวิธีการที่คุณจะจัดสรรอาร์เรย์ของพวกเขามีแนวโน้มที่จะสอดคล้องกับสายหน้า

ซึ่งหมายความว่าการเข้าถึงทั้งหมดของคุณในแต่ละลูปจะลดลงตามวิธีการแคชเดียวกัน อย่างไรก็ตามโปรเซสเซอร์ Intel มีการเชื่อมโยงแคช L1 แบบ 8 ทิศทางอยู่พักหนึ่ง แต่ในความเป็นจริงแล้วการแสดงไม่เหมือนกันทั้งหมด การเข้าถึง 4-way นั้นช้ากว่าการพูด 2-way

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

นี่คือรหัสทดสอบ:

int main(){
    const int n = 100000;

#ifdef ALLOCATE_SEPERATE
    double *a1 = (double*)malloc(n * sizeof(double));
    double *b1 = (double*)malloc(n * sizeof(double));
    double *c1 = (double*)malloc(n * sizeof(double));
    double *d1 = (double*)malloc(n * sizeof(double));
#else
    double *a1 = (double*)malloc(n * sizeof(double) * 4);
    double *b1 = a1 + n;
    double *c1 = b1 + n;
    double *d1 = c1 + n;
#endif

    //  Zero the data to prevent any chance of denormals.
    memset(a1,0,n * sizeof(double));
    memset(b1,0,n * sizeof(double));
    memset(c1,0,n * sizeof(double));
    memset(d1,0,n * sizeof(double));

    //  Print the addresses
    cout << a1 << endl;
    cout << b1 << endl;
    cout << c1 << endl;
    cout << d1 << endl;

    clock_t start = clock();

    int c = 0;
    while (c++ < 10000){

#if ONE_LOOP
        for(int j=0;j<n;j++){
            a1[j] += b1[j];
            c1[j] += d1[j];
        }
#else
        for(int j=0;j<n;j++){
            a1[j] += b1[j];
        }
        for(int j=0;j<n;j++){
            c1[j] += d1[j];
        }
#endif

    }

    clock_t end = clock();
    cout << "seconds = " << (double)(end - start) / CLOCKS_PER_SEC << endl;

    system("pause");
    return 0;
}

ผลลัพธ์มาตรฐาน:

แก้ไข: ผลลัพธ์บนเครื่องสถาปัตยกรรม Core 2 จริง :

2 x Intel Xeon X5482 Harpertown ที่ 3.2 GHz:

#define ALLOCATE_SEPERATE
#define ONE_LOOP
00600020
006D0020
007A0020
00870020
seconds = 6.206

#define ALLOCATE_SEPERATE
//#define ONE_LOOP
005E0020
006B0020
00780020
00850020
seconds = 2.116

//#define ALLOCATE_SEPERATE
#define ONE_LOOP
00570020
00633520
006F6A20
007B9F20
seconds = 1.894

//#define ALLOCATE_SEPERATE
//#define ONE_LOOP
008C0020
00983520
00A46A20
00B09F20
seconds = 1.993

ข้อสังเกต:

  • 6.206 วินาทีกับหนึ่งวงและ2.116 วินาทีกับสองวง เป็นการจำลองผลลัพธ์ของ OP อย่างแน่นอน

  • ในการทดสอบสองครั้งแรกอาร์เรย์จะถูกจัดสรรแยกกัน คุณจะสังเกตเห็นว่าพวกเขาทั้งหมดมีการจัดตำแหน่งเดียวกันกับหน้า

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

เนื่องจาก @Stephen Cannon ชี้ให้เห็นในความคิดเห็นมีความเป็นไปได้มากที่การจัดตำแหน่งนี้ทำให้เกิดนามแฝงเท็จในหน่วยโหลด / ร้านค้าหรือแคช ฉันไปหาสิ่งนี้และพบว่า Intel มีตัวนับฮาร์ดแวร์สำหรับแผงขายชื่อแทนบางส่วน :

http://software.intel.com/sites/products/documentation/doclib/stdxe/2013/~amplifierxe/pmw_dp/events/partial_address_alias.html


5 ภูมิภาค - คำอธิบาย

ภูมิภาค 1:

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

ภูมิภาค 2:

ที่นี่เมื่อขนาดของข้อมูลเพิ่มขึ้นปริมาณของค่าใช้จ่ายที่สัมพันธ์กันจะลดลงและประสิทธิภาพ "อิ่มตัว" นี่สองลูปช้ากว่าเพราะมันมีลูปมากกว่าสองเท่า

ฉันไม่แน่ใจว่าสิ่งที่เกิดขึ้นที่นี่ ... จัดยังคงสามารถเล่นมีผล ณ Agner หมอกกล่าวถึงความขัดแย้งธนาคารแคช (ลิงค์นั้นเกี่ยวกับ Sandy Bridge แต่ความคิดนั้นควรจะยังคงใช้กับ Core 2 ได้)

ภูมิภาค 3:

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

ภูมิภาค 4:

การลดลงของประสิทธิภาพในวงเดียวคือสิ่งที่เรากำลังสังเกต และตามที่กล่าวมานี้เป็นผลมาจากการจัดตำแหน่งที่ (ส่วนใหญ่) ทำให้เกิดaliasingแผงลอยเท็จในหน่วยโหลด / เก็บตัวประมวลผล

อย่างไรก็ตามเพื่อให้การสร้างสมนามเท็จเกิดขึ้นต้องมีชุดข้อมูลขนาดใหญ่พอระหว่างชุดข้อมูล นี่คือเหตุผลที่คุณไม่เห็นสิ่งนี้ในภูมิภาค 3

ภูมิภาค 5:

ณ จุดนี้ไม่มีสิ่งใดเหมาะกับแคช ดังนั้นคุณจึงถูกแบนด์วิดธ์ของหน่วยความจำ


2 x Intel X5482 Harpertown ที่ 3.2 GHz Intel Core i7 870 @ 2.8 GHz Intel Core i7 2600K @ 4.4 GHz


162
+1: ฉันคิดว่านี่เป็นคำตอบ ตรงกันข้ามกับคำตอบอื่น ๆ ทั้งหมดที่กล่าวมาไม่ใช่เกี่ยวกับตัวแปรลูปเดี่ยวโดยเนื้อแท้ที่มีแคชหายไปมากกว่า แต่เป็นเรื่องเกี่ยวกับการจัดตำแหน่งอาร์เรย์โดยเฉพาะทำให้แคชหายไป
Oliver Charlesworth

30
นี้; aliasing เท็จคอกเป็นคำอธิบายได้มากที่สุด
สตีเฟ่นแคนนอน

7
@VictorT ฉันใช้รหัส OP ที่เชื่อมโยงกับ มันสร้างไฟล์. css ซึ่งฉันสามารถเปิดใน Excel และสร้างกราฟจากมัน
Mysticial

5
@Nawaz โดยทั่วไปหน้าเป็น 4KB หากคุณดูที่อยู่เลขฐานสิบหกที่ฉันพิมพ์ออกมาการทดสอบที่ได้รับการจัดสรรแยกต่างหากทั้งหมดจะมีโมดูโล 4096 เหมือนกัน (นั่นคือ 32- ไบต์จากจุดเริ่มต้นของขอบเขต 4KB) บางที GCC อาจไม่มีพฤติกรรมนี้ นั่นอาจอธิบายได้ว่าทำไมคุณถึงไม่เห็นความแตกต่าง
Mysticial


224

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

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

@ คำตอบของ Mysticial ทำให้คนจำนวนมากเชื่อ (รวมถึงฉัน) อาจเป็นเพราะมันเป็นคนเดียวที่ดูเหมือนจะพึ่งพาข้อเท็จจริง แต่มันก็เป็นเพียง "จุดข้อมูล" ของความจริง

นั่นเป็นเหตุผลที่ฉันรวมการทดสอบของเขา (โดยใช้การจัดสรรแบบต่อเนื่องกับการแยกต่างหาก) และคำแนะนำของผู้ตอบคำถามของ @James

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

โปรดทราบว่าคำถามแรกของฉันอยู่ที่n = 100.000 จุดนี้ (โดยบังเอิญ) แสดงถึงพฤติกรรมพิเศษ:

  1. มันมีความแตกต่างที่ยิ่งใหญ่ที่สุดระหว่างหนึ่งและสองรุ่นวนลูป (เกือบหนึ่งในสามของปัจจัย)

  2. มันเป็นเพียงจุดเดียวที่วงหนึ่ง (กล่าวคือมีการจัดสรรอย่างต่อเนื่อง) เต้นรุ่นสองวง (นี่ทำให้คำตอบของ Mysticial เป็นไปได้ทั้งหมด)

ผลลัพธ์ที่ใช้ข้อมูลเริ่มต้น:

ป้อนคำอธิบายภาพที่นี่

ผลลัพธ์ที่ใช้ข้อมูลที่ไม่ได้กำหนดค่าเริ่มต้น (นี่คือสิ่งที่ Mysticial ทดสอบ):

ป้อนคำอธิบายภาพที่นี่

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

ป้อนคำอธิบายภาพที่นี่

ข้อเสนอ

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


18
+1 การวิเคราะห์ที่ดี ฉันไม่ได้ตั้งใจที่จะทิ้งข้อมูลไว้โดยไม่กำหนดค่าเริ่มต้นตั้งแต่แรก มันเพิ่งเกิดขึ้นที่ตัวจัดสรรเป็นศูนย์ต่อไป ดังนั้นข้อมูลเริ่มต้นคือสิ่งที่สำคัญ ฉันเพิ่งแก้ไขคำตอบของฉันพร้อมกับผลลัพธ์บนเครื่องสถาปัตยกรรม Core 2 จริงและพวกเขาก็ใกล้เคียงกับสิ่งที่คุณสังเกต อีกสิ่งหนึ่งคือฉันทดสอบช่วงของขนาดnและมันแสดงให้เห็นถึงช่องว่างของประสิทธิภาพที่เหมือนกันสำหรับn = 80000, n = 100000, n = 200000ฯลฯ ...
Mysticial

2
@ อย่างเป็นทางการฉันคิดว่าระบบปฏิบัติการจะใช้หน้าเว็บเป็นศูนย์เมื่อใดก็ตามที่ให้หน้าใหม่กับกระบวนการเพื่อหลีกเลี่ยงการสอดแนมระหว่างกระบวนการที่เป็นไปได้
v.oddou

1
@ v.oddou: พฤติกรรมขึ้นอยู่กับระบบปฏิบัติการด้วย IIRC, Windows มีเธรดที่เป็นพื้นหลังเพจที่ไม่มีการปล่อยให้เป็นศูนย์และหากคำขอไม่สามารถทำให้พอใจจากเพจที่มีค่าศูนย์อยู่แล้วการVirtualAllocโทรจะบล็อกจนกว่ามันจะเป็นศูนย์เพียงพอที่จะตอบสนองคำขอ ในทางตรงกันข้ามลินุกซ์ก็แมปหน้าศูนย์เป็นสำเนาคัดลอกเขียนเท่าที่จำเป็นและเมื่อเขียนมันคัดลอกศูนย์ใหม่ไปยังหน้าใหม่ก่อนที่จะเขียนในข้อมูลใหม่ จากมุมมองของกระบวนการโหมดผู้ใช้หน้าเว็บจะเป็นศูนย์ แต่การใช้งานครั้งแรกของหน่วยความจำที่ไม่ได้กำหนดค่าเริ่มต้นมักจะมีราคาแพงกว่าบน Linux มากกว่าบน Windows
ShadowRanger

81

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


1
คุณกำลังบอกว่าตัวแปรตัวที่สองนั้นมีแคชน้อยกว่าหรือเปล่า ทำไม?
Oliver Charlesworth

2
@Oli: ในตัวแปรแรกประมวลผลความต้องการที่จะเข้าถึงสี่สายของหน่วยความจำที่เวลาa[i], b[i], c[i]และd[i]ตัวแปรที่สองก็ต้องการเพียงสอง ทำให้สามารถเติมได้ในขณะที่เพิ่ม
ลูกสุนัข

4
แต่ตราบใดที่อาร์เรย์ไม่ได้ชนกันในแคชแต่ละตัวแปรต้องใช้จำนวนการอ่านและเขียนจาก / ถึงหน่วยความจำหลักที่เท่ากัน ดังนั้นข้อสรุปคือ (ฉันคิดว่า) ว่าสองอาร์เรย์นี้เกิดการปะทะกันตลอดเวลา
Oliver Charlesworth

3
ฉันไม่ทำตาม ตามคำสั่ง (เช่นต่ออินสแตนซ์ของx += y) จะมีการอ่านสองครั้งและหนึ่งการเขียน สิ่งนี้เป็นจริงสำหรับทั้งสองรุ่น ดังนั้นแคช <-> ความต้องการแบนด์วิดท์ของ CPU จึงเหมือนกัน ตราบใดที่ไม่มีความขัดแย้ง, แคช <-> ความต้องการแบนด์วิดท์ของ RAM ก็เหมือนกัน ..
Oliver Charlesworth

2
ตามที่ระบุไว้ในstackoverflow.com/a/1742231/102916 , prefetch ฮาร์ดแวร์ Pentium M สามารถติดตาม 12 ลำธารข้างหน้าที่แตกต่างกัน (และผมจะคาดหวังฮาร์ดแวร์หลังจากนั้นจะมีอย่างน้อยความสามารถ) วนรอบ 2 ยังคงอ่านเฉพาะสตรีมสี่สตรีมเท่านั้นดังนั้นจึงอยู่ในขีด จำกัด นั้น
Brooks Moses

50

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

สมมติว่านโยบายการแคช LIFO ง่าย ๆ รหัสนี้:

for(int j=0;j<n;j++){
    a[j] += b[j];
}
for(int j=0;j<n;j++){
    c[j] += d[j];
}

จะทำให้เกิดaและbจะโหลดลงใน RAM และจากนั้นจะทำงานใน RAM ทั้งหมด เมื่อสองวงเริ่มต้นcและdจากนั้นก็จะถูกโหลดจากดิสก์ลงใน RAM และดำเนินการใน

วงอื่น ๆ

for(int j=0;j<n;j++){
    a[j] += b[j];
    c[j] += d[j];
}

หน้าประสงค์ออกสองอาร์เรย์และหน้าอีกสองทุกครั้งรอบวง นี้เห็นได้ชัดจะเป็นมากช้าลง

คุณอาจไม่เห็นดิสก์แคชในการทดสอบของคุณ แต่คุณอาจเห็นผลข้างเคียงของการแคชรูปแบบอื่น


ดูเหมือนจะมีความสับสนเล็กน้อย / ความเข้าใจผิดที่นี่ดังนั้นฉันจะพยายามอธิบายเล็ก ๆ น้อย ๆ โดยใช้ตัวอย่าง

พูดn = 2และเรากำลังทำงานกับไบต์ ในสถานการณ์ของฉันเราจึงมีRAM เพียง 4 ไบต์และส่วนที่เหลือของหน่วยความจำของเราก็ช้าลงอย่างมาก (พูดได้นานกว่า 100 เท่า)

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

  • กับ

    for(int j=0;j<n;j++){
     a[j] += b[j];
    }
    for(int j=0;j<n;j++){
     c[j] += d[j];
    }
  • แคชa[0]และa[1]จากนั้นb[0]และb[1]และการตั้งค่าa[0] = a[0] + b[0]ในแคช - มีตอนนี้สี่ไบต์ในแคชและa[0], a[1] b[0], b[1]ราคา = 100 + 100

  • ตั้งเป็นa[1] = a[1] + b[1]แคช ราคา = 1 + 1
  • ทำซ้ำสำหรับ cdและ
  • ต้นทุนทั้งหมด = (100 + 100 + 1 + 1) * 2 = 404

  • กับ

    for(int j=0;j<n;j++){
     a[j] += b[j];
     c[j] += d[j];
    }
  • แคชa[0]และa[1]จากนั้นb[0]และb[1]และตั้งค่าa[0] = a[0] + b[0]ในแคช - ขณะนี้มีสี่ไบต์ในแคชa[0], a[1]b[0], b[1]และ ราคา = 100 + 100

  • ดีดออกa[0], a[1], b[0], b[1]จากแคชและแคชc[0]และc[1]แล้วd[0]และd[1]และการตั้งค่าc[0] = c[0] + d[0]ในแคช ราคา = 100 + 100
  • ฉันสงสัยว่าคุณเริ่มเห็นว่าฉันกำลังจะไปไหน
  • ต้นทุนทั้งหมด = (100 + 100 + 100 + 100) * 2 = 800

นี่เป็นสถานการณ์จำลองแคชแบบคลาสสิก


12
สิ่งนี้ไม่ถูกต้อง การอ้างอิงถึงองค์ประกอบเฉพาะของอาร์เรย์ไม่ทำให้อาร์เรย์ทั้งหมดถูกทำเพจจากดิสก์ (หรือจากหน่วยความจำที่ไม่ใช่แคช) เพจหรือแคชบรรทัดที่เกี่ยวข้องเท่านั้นที่สามารถทำเพจได้
Brooks Moses

1
@Brooks Moses - ถ้าคุณเดินผ่านแถวลำดับทั้งหมดที่เกิดขึ้นที่นี่ก็จะ
OldCurmudgeon

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

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

1
เหตุใดคุณจึงอยู่ที่วนซ้ำ 2 หน้าทั้งหน้าa1และb1สำหรับการมอบหมายครั้งแรกแทนที่จะเป็นเพียงหน้าแรกของแต่ละหน้า (คุณสมมติหน้า 5 ไบต์ดังนั้นหน้าหนึ่งครึ่งของ RAM ของคุณหรือไม่นั่นไม่ใช่แค่การปรับขนาดเท่านั้นซึ่งแตกต่างจากตัวประมวลผลที่แท้จริงทั้งหมด)
Brooks Moses

35

ไม่ใช่เพราะรหัสอื่น แต่เนื่องจากการแคช: RAM ช้ากว่าการลงทะเบียน CPU และหน่วยความจำแคชอยู่ภายใน CPU เพื่อหลีกเลี่ยงการเขียน RAM ทุกครั้งที่มีการเปลี่ยนแปลงตัวแปร แต่แคชไม่ใหญ่เท่า RAM ดังนั้นจึงแมปเพียงเศษเสี้ยวของมัน

รหัสแรกจะปรับเปลี่ยนที่อยู่หน่วยความจำที่อยู่ห่างไกลสลับกันที่แต่ละวงดังนั้นต้องการอย่างต่อเนื่องในการทำให้แคชใช้ไม่ได้

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


เหตุใดจึงเป็นสาเหตุให้แคชใช้งานไม่ได้อย่างต่อเนื่อง
Oliver Charlesworth

1
@OliCharlesworth: คิดว่าแคชเป็นสำเนาของที่อยู่หน่วยความจำที่ต่อเนื่อง หากคุณแกล้งเข้าถึงที่อยู่ที่ไม่ใช่ส่วนหนึ่งของที่อยู่เหล่านั้นคุณจะต้องโหลดแคชใหม่ และถ้าบางสิ่งในแคชได้ถูกแก้ไขมันจะต้องถูกเขียนกลับมาใน RAM หรือมันจะหายไป ในตัวอย่างรหัส 4 เวกเตอร์ของจำนวนเต็ม 100'000 (400kBytes) มักจะมากกว่าความจุของแคช L1 (128 หรือ 256K)
Emilio Garavaglia

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

2
@OliCharlesworth: ถ้าฉันต้องโหลดค่าใหม่ในแคชและมีค่าอยู่ในนั้นที่ได้รับการแก้ไขแล้วฉันต้องเขียนลงไปก่อนและสิ่งนี้ทำให้ฉันรอให้การเขียนเกิดขึ้น
Emilio Garavaglia

2
แต่ในทั้งสองตัวแปรของรหัส OP แต่ละค่าจะได้รับการแก้ไขอย่างแม่นยำเพียงครั้งเดียว คุณทำจำนวนการเขียนย้อนหลังเท่ากันในแต่ละตัวแปร
Oliver Charlesworth

22

ฉันไม่สามารถทำซ้ำผลลัพธ์ที่กล่าวถึงที่นี่

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

ขนาดของอาร์เรย์มีตั้งแต่ 2 ^ 16 ถึง 2 ^ 24 โดยใช้แปดลูป ฉันระมัดระวังในการเริ่มต้นอาร์เรย์แหล่งข้อมูลดังนั้นการ+=มอบหมายไม่ได้ถามFPUเพิ่มหน่วยความจำขยะที่ตีความว่าเป็นสองเท่า

ฉันเล่นกับแผนการต่าง ๆ เช่นการกำหนดของ b[j] , d[j]การInitToZero[j]ภายในลูป, และยังมีการใช้+= b[j] = 1และ+= d[j] = 1และฉันได้ผลลัพธ์ที่สอดคล้องกันอย่างเป็นธรรม

ตามที่คุณคาดหวังเริ่มต้น bและdภายในลูปโดยใช้InitToZero[j]วิธีการแบบรวมทำให้ได้เปรียบเนื่องจากพวกเขาทำกลับไปกลับก่อนที่จะมอบหมายaและcแต่ยังคงอยู่ภายใน 10% ไปคิด

ฮาร์ดแวร์คือ Dell XPS 8500 ที่มีรุ่น 3 Core i7 @ 3.4 GHz และ 8 GB สำหรับ 2 ^ 16 ถึง 2 ^ 24 โดยใช้แปดลูปเวลาสะสมคือ 44.987 และ 40.965 ตามลำดับ Visual C ++ 2010 ปรับให้เหมาะสมอย่างเต็มที่

PS: ฉันเปลี่ยนลูปเป็นนับเป็นศูนย์และวิธีการรวมนั้นเร็วขึ้นเล็กน้อย เกาหัวฉัน หมายเหตุการนับขนาดและการวนซ้ำอาร์เรย์ใหม่

// MemBufferMystery.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <iostream>
#include <cmath>
#include <string>
#include <time.h>

#define  dbl    double
#define  MAX_ARRAY_SZ    262145    //16777216    // AKA (2^24)
#define  STEP_SZ           1024    //   65536    // AKA (2^16)

int _tmain(int argc, _TCHAR* argv[]) {
    long i, j, ArraySz = 0,  LoopKnt = 1024;
    time_t start, Cumulative_Combined = 0, Cumulative_Separate = 0;
    dbl *a = NULL, *b = NULL, *c = NULL, *d = NULL, *InitToOnes = NULL;

    a = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    b = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    c = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    d = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    InitToOnes = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    // Initialize array to 1.0 second.
    for(j = 0; j< MAX_ARRAY_SZ; j++) {
        InitToOnes[j] = 1.0;
    }

    // Increase size of arrays and time
    for(ArraySz = STEP_SZ; ArraySz<MAX_ARRAY_SZ; ArraySz += STEP_SZ) {
        a = (dbl *)realloc(a, ArraySz * sizeof(dbl));
        b = (dbl *)realloc(b, ArraySz * sizeof(dbl));
        c = (dbl *)realloc(c, ArraySz * sizeof(dbl));
        d = (dbl *)realloc(d, ArraySz * sizeof(dbl));
        // Outside the timing loop, initialize
        // b and d arrays to 1.0 sec for consistent += performance.
        memcpy((void *)b, (void *)InitToOnes, ArraySz * sizeof(dbl));
        memcpy((void *)d, (void *)InitToOnes, ArraySz * sizeof(dbl));

        start = clock();
        for(i = LoopKnt; i; i--) {
            for(j = ArraySz; j; j--) {
                a[j] += b[j];
                c[j] += d[j];
            }
        }
        Cumulative_Combined += (clock()-start);
        printf("\n %6i miliseconds for combined array sizes %i and %i loops",
                (int)(clock()-start), ArraySz, LoopKnt);
        start = clock();
        for(i = LoopKnt; i; i--) {
            for(j = ArraySz; j; j--) {
                a[j] += b[j];
            }
            for(j = ArraySz; j; j--) {
                c[j] += d[j];
            }
        }
        Cumulative_Separate += (clock()-start);
        printf("\n %6i miliseconds for separate array sizes %i and %i loops \n",
                (int)(clock()-start), ArraySz, LoopKnt);
    }
    printf("\n Cumulative combined array processing took %10.3f seconds",
            (dbl)(Cumulative_Combined/(dbl)CLOCKS_PER_SEC));
    printf("\n Cumulative seperate array processing took %10.3f seconds",
        (dbl)(Cumulative_Separate/(dbl)CLOCKS_PER_SEC));
    getchar();

    free(a); free(b); free(c); free(d); free(InitToOnes);
    return 0;
}

ฉันไม่แน่ใจว่าทำไมจึงมีการตัดสินใจว่า MFLOPS เป็นตัวชี้วัดที่เกี่ยวข้อง ฉันคิดว่าจะมุ่งเน้นไปที่การเข้าถึงหน่วยความจำดังนั้นฉันจึงพยายามลดเวลาการคำนวณจำนวนจุดลอยตัว ฉันทิ้งไว้+=แต่ฉันไม่แน่ใจว่าทำไม

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


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

18

เป็นเพราะ CPU ไม่มีแคชจำนวนมากที่ขาดหายไป (ซึ่งจะต้องรอให้ข้อมูลอาร์เรย์มาจากชิป RAM) เป็นเรื่องที่น่าสนใจสำหรับคุณที่จะปรับขนาดของอาร์เรย์อย่างต่อเนื่องเพื่อให้คุณมีขนาดเกินกว่าระดับแคช 1 (L1) จากนั้นแคชระดับ 2 (L2) ของ CPU ของคุณและวางแผนเวลาที่ใช้รหัสของคุณ เพื่อดำเนินการกับขนาดของอาร์เรย์ กราฟไม่ควรเป็นเส้นตรงตามที่คุณคาดหวัง


2
ฉันไม่เชื่อว่าจะมีการโต้ตอบระหว่างขนาดแคชและขนาดอาร์เรย์ แต่ละองค์ประกอบของอาร์เรย์จะใช้เพียงครั้งเดียวเท่านั้นและสามารถขับออกมาได้อย่างปลอดภัย อาจมีการโต้ตอบกันระหว่างขนาดแคชของบรรทัดและขนาดของอาเรย์หากเกิดความขัดแย้งกัน
Oliver Charlesworth

15

ลูปแรกสลับการเขียนในแต่ละตัวแปร อันที่สองและสามนั้นจะทำการกระโดดของขนาดองค์ประกอบเพียงเล็กน้อย

ลองเขียนเส้นขนานสองเส้นที่มีกากบาท 20 เส้นโดยใช้ปากกาและกระดาษคั่นด้วย 20 ซม. ลองทำหนึ่งครั้งให้เสร็จก่อนจากนั้นจึงลองอีกบรรทัดหนึ่งแล้วลองอีกครั้งโดยเขียนกากบาทในแต่ละบรรทัดสลับกัน


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

7

คำถามเดิม

เหตุใดหนึ่งลูปจึงช้ากว่าสองลูปมาก


สรุป:

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

ดูจากวิธีการประเภทนี้โดยไม่ต้องเกี่ยวข้องกับวิธีการทำงานของฮาร์ดแวร์ระบบปฏิบัติการและคอมไพเลอร์เพื่อทำการจัดสรรฮีปที่เกี่ยวข้องกับการทำงานกับ RAM, แคช, ไฟล์เพจ ฯลฯ คณิตศาสตร์ที่เป็นรากฐานของอัลกอริธึมเหล่านี้แสดงให้เราเห็นว่าในสองสิ่งนี้เป็นทางออกที่ดีกว่า

เราสามารถใช้การเปรียบเทียบสิ่งมีBossชีวิตSummationที่จะเป็นตัวแทนของสิ่งFor Loopที่ต้องเดินทางระหว่างคนงานA&Bและ

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


ตอนนี้ฉันจะเริ่มอธิบายวิธีการทำงานด้านล่างทั้งหมด


การประเมินปัญหา

รหัสของ OP:

const int n=100000;

for(int j=0;j<n;j++){
    a1[j] += b1[j];
    c1[j] += d1[j];
}

และ

for(int j=0;j<n;j++){
    a1[j] += b1[j];
}
for(int j=0;j<n;j++){
    c1[j] += d1[j];
}

การพิจารณา

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


วิธีการ

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


มุมมอง

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


สิ่งที่เรารู้

เรารู้ว่าวงนี้จะวิ่ง 100,000 ครั้ง เราก็รู้เช่นกันa1 , b1, c1และd1มีคำแนะนำเกี่ยวกับสถาปัตยกรรม 64 บิต ภายใน C ++ บนเครื่อง 32 บิตพอยน์เตอร์ทั้งหมดคือ 4 ไบต์และบนเครื่อง 64 บิตจะมีขนาด 8 ไบต์เนื่องจากพอยน์เตอร์มีความยาวคงที่

เรารู้ว่าเรามี 32 ไบต์ที่จะจัดสรรในทั้งสองกรณี ความแตกต่างเพียงอย่างเดียวคือเรากำลังจัดสรร 32 ไบต์หรือ 2 ชุด 2-8bytes ในการทำซ้ำแต่ละครั้งโดยกรณีที่ 2 เราจัดสรร 16 ไบต์สำหรับการทำซ้ำแต่ละครั้งสำหรับลูปอิสระทั้งสอง

ลูปทั้งสองยังคงเท่ากับ 32 ไบต์ในการจัดสรรทั้งหมด ด้วยข้อมูลนี้เราจะไปข้างหน้าและแสดงคณิตศาสตร์ทั่วไปอัลกอริทึมและการเปรียบเทียบแนวคิดเหล่านี้

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


สิ่งที่เราไม่รู้

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


ตรวจสอบกันเถอะ

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


คำยืนยันของเรา:

  • เราจะให้ลูปและการวนซ้ำของมันเป็นการสรุปที่เริ่มต้นที่ 1 และสิ้นสุดที่ 100000 แทนที่จะเริ่มด้วย 0 ในลูปเพราะเราไม่จำเป็นต้องกังวลเกี่ยวกับรูปแบบการจัดทำดัชนี 0 ของหน่วยความจำเนื่องจากเราสนใจ อัลกอริทึมของตัวเอง
  • ในทั้งสองกรณีเรามี 4 ฟังก์ชั่นในการทำงานกับและ 2 ฟังก์ชั่นการโทรด้วย 2 การดำเนินการที่จะทำในแต่ละการเรียกใช้ฟังก์ชั่น เราจะตั้งค่าเหล่านี้เป็นฟังก์ชั่นและการโทรไปยังฟังก์ชั่นเป็นต่อไปนี้: F1(), F2(), f(a), f(b), และf(c)f(d)

อัลกอริทึม:

กรณีที่ 1: - การรวมหนึ่งครั้งเดียว แต่การเรียกใช้ฟังก์ชั่นอิสระสองครั้ง

Sum n=1 : [1,100000] = F1(), F2();
                       F1() = { f(a) = f(a) + f(b); }
                       F2() = { f(c) = f(c) + f(d); }

กรณีที่สอง: - การสรุปสองครั้ง แต่แต่ละตัวมีการเรียกใช้ฟังก์ชันของตัวเอง

Sum1 n=1 : [1,100000] = F1();
                        F1() = { f(a) = f(a) + f(b); }

Sum2 n=1 : [1,100000] = F1();
                        F1() = { f(c) = f(c) + f(d); }

ถ้าคุณสังเกตเห็นF2()มีอยู่เพียงในSumจากCase1ที่F1()มีอยู่ในSumจากCase1และทั้งในSum1และจากSum2 Case2สิ่งนี้จะปรากฏในภายหลังเมื่อเราเริ่มสรุปว่ามีการเพิ่มประสิทธิภาพที่เกิดขึ้นภายในอัลกอริทึมที่สอง

การวนซ้ำผ่านการSumเรียกใช้เคสแรกf(a)ที่จะเพิ่มเข้ากับตัวมันเองf(b)จากนั้นจะเรียกf(c)ว่าจะทำเหมือนเดิม แต่เพิ่มf(d)ไปยังตัวเองสำหรับ100000การวนซ้ำแต่ละครั้ง ในกรณีที่สองเรามีSum1และSum2ที่ทั้งสองทำหน้าที่เหมือนกับว่าพวกเขาเป็นฟังก์ชั่นเดียวกันที่ถูกเรียกสองครั้งในแถว

ในกรณีนี้เราสามารถปฏิบัติต่อSum1และSum2เป็นเพียงแค่เก่าSumที่Sumในกรณีนี้มีลักษณะเช่นนี้Sum n=1 : [1,100000] { f(a) = f(a) + f(b); }และตอนนี้ดูเหมือนว่าการเพิ่มประสิทธิภาพที่เราสามารถพิจารณาให้มันเป็นฟังก์ชั่นเดียวกัน


สรุปด้วยระบบอะนาล็อก

จากสิ่งที่เราเห็นในกรณีที่สองมันเกือบจะปรากฏขึ้นราวกับว่ามีการเพิ่มประสิทธิภาพเนื่องจากลูปทั้งคู่มีลายเซ็นที่เหมือนกัน แต่นี่ไม่ใช่ปัญหาจริง ปัญหาไม่ได้เป็นงานที่จะถูกทำโดยf(a), f(b), f(c)และf(d)และในทั้งสองกรณีและการเปรียบเทียบระหว่างสองกรณีนี้คือความแตกต่างของระยะทางที่การสรุปจะต้องเดินทางในแต่ละกรณีที่ให้ความแตกต่างในเวลาดำเนินการ

คิดว่าการFor Loopsเป็นคนSummationsทำซ้ำเป็นการBossสั่งให้คนสองคนA& Bและงานของพวกเขาคือการกินเนื้อC& Dตามลำดับและรับพัสดุจากพวกเขาและส่งคืน Bossในการเปรียบเทียบนี้สำหรับลูปหรือทำซ้ำบวกและการตรวจสอบสภาพตัวเองไม่จริงเป็นตัวแทนของ สิ่งที่แสดงถึงBossไม่ได้มาจากอัลกอริทึมทางคณิตศาสตร์ที่เกิดขึ้นจริงโดยตรง แต่จากแนวคิดที่แท้จริงของScopeและCode Blockภายในรูทีนย่อยหรือรูทีนย่อยวิธีการฟังก์ชั่นหน่วยแปล ฯลฯ อัลกอริทึมแรกมี 1 ขอบเขต

ภายในกรณีแรกในแต่ละสลิปการโทรBossไปที่Aและให้คำสั่งซื้อและAออกไปเรียกB'sแพคเกจจากนั้นBossไปที่Cและให้คำสั่งซื้อเพื่อทำแบบเดียวกันและรับแพคเกจจากDในแต่ละการทำซ้ำ

ภายในกรณีที่สองการBossทำงานโดยตรงกับAไปและดึงB'sแพคเกจจนกว่าจะได้รับแพคเกจทั้งหมด จากนั้นการBossทำงานด้วยCจะทำเช่นเดียวกันเพื่อรับD'sแพ็คเกจทั้งหมด

เนื่องจากเรากำลังทำงานกับตัวชี้ขนาด 8 ไบต์และจัดการกับการจัดสรรฮีปลองพิจารณาปัญหาต่อไปนี้ ขอบอกว่าBossเป็น 100 ฟุตจากAและที่Aอยู่ที่ 500 Cฟุตจาก เราไม่จำเป็นต้องกังวลว่าในBossตอนแรกจะมีจำนวนเท่าไรCเนื่องจากลำดับของการประหารชีวิต ในทั้งสองกรณีBossแรกเดินทางจากครั้งแรกแล้วA Bการเปรียบเทียบนี้ไม่ได้บอกว่าระยะทางนี้แน่นอน มันเป็นเพียงสถานการณ์กรณีทดสอบที่มีประโยชน์เพื่อแสดงการทำงานของอัลกอริทึม

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


กรณีทดสอบ:

กรณีแรก:ในการทำซ้ำBossครั้งแรกต้องไป 100 ฟุตในขั้นแรกเพื่อให้สลิปสั่งซื้อAและAไปและทำสิ่งของเขา แต่แล้วBossต้องเดินทาง 500 ฟุตCเพื่อให้เขาสลิปสั่งซื้อของเขา จากนั้นในการทำซ้ำครั้งต่อไปและการทำซ้ำทุก ๆ ครั้งหลังจากBossนั้นจะต้องย้อนกลับไป 500 ฟุตระหว่างทั้งสอง

กรณีที่สอง:ผู้ที่Bossต้องเดินทาง 100 ฟุตในการทำซ้ำครั้งแรกไปAแต่หลังจากนั้นเขาก็อยู่ที่นั่นแล้วและรอที่Aจะกลับไปจนกว่าบิลทั้งหมดจะถูกเติมเต็ม จากนั้นBossมีการเดินทาง 500 ฟุตบนย้ำแรกที่จะCเพราะCเป็น 500 Aฟุตจาก เนื่องจากสิ่งนี้Boss( Summation, For Loop )ถูกเรียกใช้ทันทีหลังจากทำงานกับAเขาจากนั้นก็รออยู่ตรงนั้นเหมือนที่เขาทำAจนกระทั่งC'sใบคำสั่งทั้งหมดเสร็จสิ้น


ความแตกต่างในระยะทางที่เดินทาง

const n = 100000
distTraveledOfFirst = (100 + 500) + ((n-1)*(500 + 500); 
// Simplify
distTraveledOfFirst = 600 + (99999*100);
distTraveledOfFirst = 600 + 9999900;
distTraveledOfFirst =  10000500;
// Distance Traveled On First Algorithm = 10,000,500ft

distTraveledOfSecond = 100 + 500 = 600;
// Distance Traveled On Second Algorithm = 600ft;    

การเปรียบเทียบค่า Arbitrary

เราสามารถเห็นได้อย่างง่ายดายว่า 600 นั้นน้อยกว่า 10 ล้าน ตอนนี้มันไม่ถูกต้องเพราะเราไม่ทราบความแตกต่างที่แท้จริงของระยะทางระหว่างที่อยู่ของ RAM หรือ Cache หรือ Page File แต่ละครั้งที่มีการเรียกในการวนซ้ำแต่ละครั้งจะเป็นเพราะตัวแปรที่มองไม่เห็นอื่น ๆ นี่เป็นเพียงการประเมินสถานการณ์ที่ต้องระวังและมองจากสถานการณ์ที่เลวร้ายที่สุด

จากตัวเลขเหล่านี้มันจะปรากฏราวกับว่าอัลกอริทึมหนึ่งควร99%ช้ากว่าอัลกอริทึมสอง แต่นี้เป็นเพียงBoss'sบางส่วนหรือความรับผิดชอบของอัลกอริทึมและมันไม่บัญชีสำหรับคนงานที่เกิดขึ้นจริงA, B, CและDและสิ่งที่พวกเขาต้องทำในแต่ละและทุกทวนของวง ดังนั้นงานของเจ้านายมีเพียงประมาณ 15 - 40% ของงานที่ทำทั้งหมด จำนวนงานที่ทำผ่านคนงานมีผลกระทบใหญ่กว่าเล็กน้อยต่อการรักษาอัตราส่วนของความแตกต่างของอัตราความเร็วประมาณ 50-70%


การสังเกต: - ความแตกต่างระหว่างสองอัลกอริธึม

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

เรายังเห็นว่าระยะทางทั้งหมดที่เดินทางไปในกรณีที่ 1นั้นไกลกว่าในกรณีที่ 2 มากและเราสามารถพิจารณาระยะนี้ได้เดินทางปัจจัยเวลาของเราระหว่างอัลกอริธึมทั้งสอง กรณีที่ 1มีการทำงานมากขึ้นมากในการทำกว่ากรณีที่ 2ไม่

สิ่งนี้สามารถสังเกตได้จากหลักฐานของASMคำแนะนำที่ปรากฏในทั้งสองกรณี นอกเหนือจากที่ระบุไว้เกี่ยวกับกรณีเหล่านี้แล้วสิ่งนี้ไม่ได้คำนึงถึงความจริงที่ว่าในกรณีที่ 1หัวหน้าจะต้องรอให้ทั้งสองA& Cกลับไปก่อนที่เขาจะกลับไปAอีกครั้งสำหรับการทำซ้ำแต่ละครั้ง นอกจากนี้ยังไม่ได้คำนึงถึงความจริงที่ว่าหากAหรือBใช้เวลานานมากจากนั้นทั้งBossคนงานและคนงานคนอื่น ๆ กำลังรอการประหารชีวิต

ในกรณีที่ 2สิ่งเดียวที่ไม่ได้ทำงานคือBossจนกว่าผู้ปฏิบัติงานจะกลับมา ดังนั้นแม้จะมีผลกระทบต่ออัลกอริทึม



คำถามที่แก้ไขเพิ่มเติม

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

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

อาจเป็นเรื่องที่น่าสนใจที่จะชี้ให้เห็นความแตกต่างระหว่างสถาปัตยกรรมของ CPU / แคชโดยจัดทำกราฟที่คล้ายกันสำหรับ CPU เหล่านี้


เกี่ยวกับคำถามเหล่านี้

อย่างที่ฉันได้แสดงให้เห็นอย่างไม่ต้องสงสัยมีปัญหาพื้นฐานก่อนที่จะมีส่วนร่วมของฮาร์ดแวร์และซอฟต์แวร์

ขณะนี้สำหรับการจัดการหน่วยความจำและแคชพร้อมกับไฟล์หน้า ฯลฯ ซึ่งทั้งหมดทำงานร่วมกันในชุดรวมของระบบระหว่างต่อไปนี้:

  • The Architecture {ฮาร์ดแวร์, เฟิร์มแวร์, ไดรเวอร์แบบฝัง, ชุดคำสั่ง Kernels และ ASM}
  • The OS{ระบบจัดการไฟล์และหน่วยความจำไดรเวอร์และรีจิสตรี}
  • The Compiler {หน่วยการแปลและการเพิ่มประสิทธิภาพของซอร์สโค้ด}
  • และแม้แต่Source Codeตัวมันเองด้วยชุดของอัลกอริธึมที่โดดเด่น

แล้วเราจะเห็นว่ามีเป็นคอขวดที่เกิดขึ้นในขั้นตอนวิธีแรกก่อนที่เราจะนำไปใช้กับเครื่องใด ๆ กับการใด ๆ โดยพลArchitecture, OSและProgrammable Languageเมื่อเทียบกับอัลกอริทึมที่สอง มีปัญหาอยู่แล้วก่อนที่จะเกี่ยวข้องกับอินทรินสิกส์ของคอมพิวเตอร์สมัยใหม่


ผลลัพธ์ที่สิ้นสุด

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

หากคุณให้ความสนใจกับการเปรียบเทียบของBossและคนงานสองคนA& Bผู้ที่ต้องไปและดึงแพ็คเกจจากC& Dตามลำดับและพิจารณาสัญกรณ์ทางคณิตศาสตร์ของอัลกอริธึมที่เป็นปัญหา คุณสามารถดูได้โดยไม่ต้องมีส่วนร่วมของคอมพิวเตอร์ฮาร์ดแวร์และซอฟแวร์Case 2จะอยู่ที่ประมาณได้เร็วกว่า60%Case 1

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

หากDataชุดมีขนาดเล็กพอสมควรก็อาจจะดูไม่แตกต่างกันในตอนแรก แต่เนื่องจากCase 1เป็นเรื่องเกี่ยวกับ60 - 70%ช้ากว่าCase 2ที่เราสามารถมองไปที่การเจริญเติบโตของฟังก์ชั่นนี้ในแง่ของความแตกต่างในการประหารชีวิตเวลา:

DeltaTimeDifference approximately = Loop1(time) - Loop2(time)
//where 
Loop1(time) = Loop2(time) + (Loop2(time)*[0.6,0.7]) // approximately
// So when we substitute this back into the difference equation we end up with 
DeltaTimeDifference approximately = (Loop2(time) + (Loop2(time)*[0.6,0.7])) - Loop2(time)
// And finally we can simplify this to
DeltaTimeDifference approximately = [0.6,0.7]*Loop2(time)

การประมาณนี้เป็นความแตกต่างโดยเฉลี่ยระหว่างสองลูปทั้งการดำเนินการแบบอัลกอริทึมและการทำงานของเครื่องที่เกี่ยวข้องกับการปรับแต่งซอฟต์แวร์และคำสั่งเครื่อง

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

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



การแก้ไข: หลักการออกแบบวิศวกรรมซอฟต์แวร์

- ความแตกต่างระหว่างLocal StackและHeap Allocatedการคำนวณภายในการวนซ้ำและความแตกต่างระหว่างการใช้งานประสิทธิภาพและประสิทธิผล -

อัลกอริทึมทางคณิตศาสตร์ที่ฉันเสนอข้างต้นส่วนใหญ่ใช้กับลูปที่ดำเนินการกับข้อมูลที่จัดสรรในฮีป

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

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

การทำเช่นนี้กับข้อมูลที่อยู่ในสแต็กเนื่องจากมีการแคชบ่อยครั้ง แต่ไม่ใช่สำหรับข้อมูลที่ต้องมีที่อยู่หน่วยความจำของมันทุกครั้ง

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

คุณอาจมีอัลกอริทึมเดียวกันที่เกี่ยวข้องกับชุดข้อมูลเดียวกัน แต่คุณอาจต้องการการออกแบบการใช้งานหนึ่งสำหรับชุดตัวแปรสแต็กและอีกชุดสำหรับชุดตัวแปรจัดสรรฮีปเนื่องจากปัญหาข้างต้นที่เห็นได้จากO(n)ความซับซ้อนของอัลกอริทึมเมื่อทำงาน กับกอง

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

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

นี่คือตัวอย่างเทียม: สองโครงสร้างอย่างง่ายหนึ่งอัลกอริทึม

struct A {
    int data;
    A() : data{0}{}
    A(int a) : data{a}{} 
};
struct B {
    int data;
    B() : data{0}{}
    A(int b) : data{b}{}
}                

template<typename T>
void Foo( T& t ) {
    // do something with t
}

// some looping operation: first stack then heap.

// stack data:
A dataSetA[10] = {};
B dataSetB[10] = {};

// For stack operations this is okay and efficient
for (int i = 0; i < 10; i++ ) {
   Foo(dataSetA[i]);
   Foo(dataSetB[i]);
}

// If the above two were on the heap then performing
// the same algorithm to both within the same loop
// will create that bottleneck
A* dataSetA = new [] A();
B* dataSetB = new [] B();
for ( int i = 0; i < 10; i++ ) {
    Foo(dataSetA[i]); // dataSetA is on the heap here
    Foo(dataSetB[i]); // dataSetB is on the heap here
} // this will be inefficient.

// To improve the efficiency above, put them into separate loops... 

for (int i = 0; i < 10; i++ ) {
    Foo(dataSetA[i]);
}
for (int i = 0; i < 10; i++ ) {
    Foo(dataSetB[i]);
}
// This will be much more efficient than above.
// The code isn't perfect syntax, it's only psuedo code
// to illustrate a point.

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


เป็นเวลานานแล้วที่ฉันโพสต์คำตอบนี้ แต่ฉันต้องการเพิ่มความคิดเห็นด่วนที่อาจช่วยให้เข้าใจสิ่งนี้: ในการเปรียบเทียบกับ Boss ของฉันสำหรับ for loop หรือ summation หรือ iterations ผ่าน loop เราสามารถ พิจารณาว่าบอสตัวนี้เป็นการรวมกันระหว่าง Stack Frame & Stack Pointer ที่จัดการขอบเขตและตัวแปรสแต็กและการกำหนดแอดเดรสหน่วยความจำของลูป for
Francis Cugler

@PeterMortensen ฉันได้นำคำแนะนำของคุณมาพิจารณาด้วยการแก้ไขคำตอบเดิมของฉันเล็กน้อย ฉันเชื่อว่านี่คือสิ่งที่คุณแนะนำ
Francis Cugler

2

มันอาจจะเป็นภาษา C ++ และการเพิ่มประสิทธิภาพ ในคอมพิวเตอร์ของฉันฉันได้ความเร็วเกือบเท่ากัน:

หนึ่งวงวน: 1.577 มิลลิวินาที

สองลูป: 1.507 ms

ฉันรัน Visual Studio 2015 บนโปรเซสเซอร์ E5-1620 3.5 GHz พร้อม RAM 16 GB

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