c++ لماذا تكون الإضافات المكونة للعنصر أسرع بكثير في الحلقات المنفصلة عن الحلقات المركبة؟




performance compiler-optimization (8)

قد يكون من C ++ القديمة والتحسينات. على جهاز الكمبيوتر الخاص بي حصلت على نفس السرعة تقريبا:

حلقة واحدة: 1.577 مللي ثانية

حلقتان: 1.507 مللي ثانية

أقوم بتشغيل Visual Studio 2015 على معالج E5-1620 3.5 غيغاهرتز مع ذاكرة الوصول العشوائي 16 غيغابايت.

لنفترض 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 (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 ثانية فقط. سؤالي هو: (يرجى الرجوع إلى سؤالي المعاد صياغته في الأسفل)

ملاحظة: لست متأكداً ، إذا كان هذا يساعد:

تبدو عملية التفكيك للحلقة الأولى بشكل أساسي كالتالي: (تتكرر هذه الكتلة خمس مرات تقريبًا في البرنامج الكامل):

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). إذا كان هناك المزيد من الاهتمام ، أعيد صياغة السؤال:

هل يمكنك تقديم بعض الإحصاءات القوية حول التفاصيل التي تؤدي إلى سلوكيات ذاكرة التخزين المؤقت المختلفة كما هو موضح في المناطق الخمس على الرسم البياني التالي؟

قد يكون من المثير للاهتمام أيضًا توضيح الاختلافات بين معماريات وحدة المعالجة المركزية / ذاكرة التخزين المؤقت ، من خلال توفير رسم بياني مشابه لوحدات المعالجة المركزية هذه.

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 .)


عند إجراء المزيد من التحليل ، أعتقد أن هذا (جزئيًا على الأقل) ناتج عن محاذاة البيانات للمؤشرات الأربعة. سيؤدي ذلك إلى حدوث مستوى معين من تعارض المصرف / الطريق في ذاكرة التخزين المؤقت.

إذا كنت قد خمنت بشكل صحيح حول كيفية تخصيص صفائفك ، فمن المحتمل أن تتم محاذاتها مع خط الصفحة .

هذا يعني أن جميع عمليات الدخول في كل حلقة ستقع على نفس طريقة التخزين المؤقت. ومع ذلك ، فقد كان معالجات انتل ذاكرة التخزين المؤقت 8-طريقة L1 لفترة من الوقت. ولكن في الواقع ، فإن الأداء ليس متجانسًا تمامًا. لا يزال الوصول إلى طرق 4 أبطأ من قول الطريقتين.

تحرير: في الواقع يبدو أنك تقوم بتخصيص كافة المصفوفات بشكل منفصل. عادة عندما يتم طلب مثل هذه المخصصات الضخمة ، سيطلب المختص صفحات جديدة من نظام التشغيل. لذلك ، توجد فرصة كبيرة أن تظهر عمليات تخصيص كبيرة في نفس الإزاحة من حدود صفحة.

إليك رمز الاختبار:

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 غيغاهرتز:

#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 ثانية مع اثنين من الحلقات. هذا يستنسخ نتائج البروتوكول الاختياري بالضبط.

  • في أول اختبارين ، يتم تخصيص المصفوفات بشكل منفصل. ستلاحظ أن جميعهم لديهم نفس المحاذاة بالنسبة للصفحة.

  • في الاختبارين الثانيين ، يتم تجميع المصفوفات معاً لكسر ذلك المحاذاة. هنا ستلاحظ كلا الحلقتين أسرع. علاوة على ذلك ، فإن الحلقة الثانية (المزدوجة) هي الآن الأبطأ كما تتوقع.

كما يشير @ Stephen Cannon في التعليقات ، هناك احتمال محتمل جدًا أن يؤدي هذا المحاذاة إلى التعرّف الخاطئ في وحدات التحميل / المخزن أو ذاكرة التخزين المؤقت. لقد بحثت في Google حول هذا الأمر ووجدت أن Intel لديها بالفعل عداد أجهزة لأسماء العناوين المستعارة الجزئية :

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

5 مناطق - تفسيرات

المنطقة 1:

هذا واحد سهل. مجموعة البيانات صغيرة جدًا لدرجة أن الأداء يتحكم في الشكل العلوي مثل التكرار والتفرع.

المنطقة 2:

هنا ، مع زيادة أحجام البيانات ، ينخفض ​​مقدار الحمل النسبي وينخفض ​​الأداء "التشبع". هنا اثنين من الحلقات أبطأ لأنه يحتوي على ضعف أكبر حلقة وتفرع النفقات العامة.

لست متأكدًا مما يحدث هنا بالضبط ... لا يزال من الممكن أن يؤدي المواءمة تأثيرًا كما يشير آجنر فوغ إلى تعارضات مصرفية مخبأة. (هذا الرابط يدور حول Sandy Bridge ، ولكن يجب أن تظل الفكرة قابلة للتطبيق على Core 2.)

المنطقة 3:

عند هذه النقطة ، لا تتناسب البيانات في ذاكرة التخزين المؤقت L1. بحيث يتم تغطيتها بالأداء من خلال النطاق الترددي L1 <-> L2 للذاكرة الوسيطة.

المنطقة 4:

انخفاض الأداء في حلقة واحدة هو ما نلاحظه. وكما ذكرنا ، فإن هذا يرجع إلى المحاذاة التي تسبب (على الأرجح) أكشاك التعرج الخاطئة في وحدات تحميل / تخزين المعالج.

ومع ذلك ، حتى يحدث التعرج الخاطئ ، يجب أن يكون هناك خطوة كبيرة بما فيه الكفاية بين مجموعات البيانات. هذا هو السبب في أنك لا ترى هذا في المنطقة 3.

المنطقة 5:

عند هذه النقطة ، لا شيء يناسب ذاكرة التخزين المؤقت. لذا فأنت مقيد بعرض نطاق الذاكرة.


ليس بسبب رمز مختلف ، ولكن بسبب التخزين المؤقت: ذاكرة الوصول العشوائي أبطأ من سجلات وحدة المعالجة المركزية وذاكرة التخزين المؤقت داخل وحدة المعالجة المركزية لتجنب كتابة ذاكرة الوصول العشوائي في كل مرة يتم تغيير متغير. لكن ذاكرة التخزين المؤقت ليست كبيرة مثل ذاكرة الوصول العشوائي ، وبالتالي ، فإنها لا ترسم سوى جزء صغير منها.

يعدّل الرمز الأول عناوين الذاكرة البعيدة بتناوبها في كل حلقة ، مما يتطلب باستمرار إبطال ذاكرة التخزين المؤقت.

لا تتناوب الشفرة الثانية: فهي تتدفق فقط على العناوين المجاورة مرتين. يؤدي ذلك إلى إكمال كل المهام في ذاكرة التخزين المؤقت ، وإبطالها فقط بعد بدء الحلقة الثانية.


لا يمكنني تكرار النتائج التي نوقشت هنا.

لا أعرف ما إذا كان الكود القياسي الضعيف هو السبب ، أم ماذا ، ولكن الطريقتين تكونان ضمن 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٪. إذهب واستنتج.

الأجهزة هي XPS 8500 من Dell مع الجيل 3 من Core i7 @ 3.4 غيغاهرتز وذاكرة 8 جيجابايت. ل 2 ^ 16 إلى 2 ^ 24 ، باستخدام ثمانية حلقات ، كان الوقت التراكمي 44.987 و 40.965 على التوالي. Visual C ++ 2010 ، الأمثل تماما.

ملاحظة: لقد غيرت الحلقات إلى العد إلى الصفر ، وكانت الطريقة المجمعة بشكل أسرع هامشيًا. خدش رأسي. لاحظ تحجيم الصفيف وحلقة حلقة جديدة.

// 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 ثانية لكل منهما.


تتضمن الحلقة الثانية نشاطًا أقل بكثير لذاكرة التخزين المؤقت ، لذا يسهل على المعالج مواكبة متطلبات الذاكرة.


تخيل أنك تعمل على جهاز حيث n كانت القيمة الصحيحة فقط لأنه من الممكن فقط الاحتفاظ بصفيفين من صفائفك في الذاكرة في وقت واحد ، ولكن إجمالي الذاكرة المتوفرة ، عبر التخزين المؤقت للقرص ، كان لا يزال كافياً لاحتواء كل أربعة.

على افتراض سياسة التخزين المؤقت LIFO بسيطة ، هذا الكود:

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

سيؤدي أولاً إلى تحميل b و b في ذاكرة الوصول العشوائي ومن ثم العمل بشكل كامل في ذاكرة الوصول العشوائي. عندما تبدأ الحلقة الثانية ، يتم تحميل c و d من القرص إلى RAM وتشغيلها.

الحلقة الأخرى

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

سوف يخرج صفحتين وصفحة في الآخرتين في كل مرة حول الحلقة . من الواضح أن هذا سيكون أبطأ بكثير .

ربما لا ترى التخزين المؤقت للقرص في اختباراتك ولكنك ربما ترى الآثار الجانبية لبعض أشكال التخزين المؤقت الأخرى.

يبدو أن هناك بعض الارتباك / سوء الفهم هنا ، لذا سأحاول وضع القليل باستخدام مثال.

قل n = 2 ونحن نعمل مع بايت. في السيناريو الخاص بي ، لدينا 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.
  • كرر لـ c و d .
  • التكلفة الإجمالية = (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

هذا هو السيناريو الكلاسيكي thrash مخبأ.


السبب هو أن وحدة المعالجة المركزية لا تحتوي على الكثير من ذاكرة التخزين المؤقت (حيث يجب الانتظار لبيانات الصفيف من شرائح RAM). سيكون من المثير للاهتمام بالنسبة لك ضبط حجم المصفوفات باستمرار بحيث تتجاوز أحجام ذاكرة التخزين المؤقت المستوى 1 (L1) ، ثم ذاكرة التخزين المؤقت من المستوى الثاني (L2) ، من وحدة المعالجة المركزية ورسم الوقت المستغرق للشفرة لتنفيذ ضد أحجام المصفوفات. يجب ألا يكون الرسم البياني خطًا مستقيمًا كما تتوقع.


حسنا ، الجواب الصحيح بالتأكيد أن تفعل شيئا مع ذاكرة التخزين المؤقت وحدة المعالجة المركزية. ولكن استخدام حجة ذاكرة التخزين المؤقت يمكن أن يكون صعبًا للغاية ، خاصة بدون بيانات.

هناك العديد من الإجابات ، التي أدت إلى الكثير من النقاش ، ولكن دعنا نواجه الأمر: يمكن أن تكون مشكلات ذاكرة التخزين المؤقت معقدة للغاية وليست ذات بُعد واحد. إنهم يعتمدون بشكل كبير على حجم البيانات ، لذا كان سؤالي غير عادل: تبيّن أنه في نقطة مهمة جدًا في الرسم البياني للذاكرة المؤقتة.

لقد أقنعت جواب Mysticial الكثير من الناس (بما فيهم أنا) ، ربما لأنها كانت الوحيدة التي بدت معتمدة على الحقائق ، لكنها كانت "نقطة بيانات" واحدة فقط للحقيقة.

هذا هو السبب في أنني جمعت اختباره (باستخدام تخصيص مستمر مقابل منفصل) ونصيحةJames's Answer.

توضح الرسوم البيانية أدناه ، أن معظم الإجابات وخصوصًا غالبية التعليقات على الأسئلة والأجوبة يمكن اعتبارها خاطئة أو صحيحة تمامًا وفقًا للسيناريو المحدد والمعلمات المستخدمة.

لاحظ أن سؤالي الأول كان في n = 100.000 . هذه النقطة (عن طريق الصدفة) تُظهر سلوكًا خاصًا:

  1. وهو يمتلك أكبر تباين بين الإصدار الواحد والثاني من الحلقات (تقريبا ثلاثة عوامل)

  2. إنها النقطة الوحيدة ، حيث تتفوق الحلقة الواحدة (وبالتحديد التخصيص المستمر) على النسخة ثنائية الحلقة. (هذا جعل إجابة Mysticial ممكنة ، على الإطلاق.)

النتيجة باستخدام البيانات المبدئية:

النتيجة باستخدام بيانات غير مهيأة (وهذا هو ما تم اختباره Mysticial):

وهذا أمر يصعب توضيحه: البيانات التي تتم تهيئتها والتي يتم تخصيصها مرة واحدة وإعادة استخدامها لكل حالة اختبار مختلفة لحجم متجه مختلف:

اقتراح

يجب مطالبة كل سؤال متعلق بالأداء منخفض المستوى على لتوفير معلومات MFLOPS لمجموعة كاملة من أحجام البيانات ذات الصلة ذاكرة التخزين المؤقت! إنه مضيعة لوقت الجميع للتفكير في الأجوبة وخاصة مناقشة هذه الأسئلة مع الآخرين دون هذه المعلومات.





vectorization