Почему в отдельных циклах стигментные добавления намного быстрее, чем в комбинированном цикле?

Предположим, что 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 С++ 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

EDIT: Вопрос оказался нецелесообразным, так как поведение сильно зависит от размеров массивов (n) и кэша CPU. Поэтому, если есть еще больший интерес, я перефразирую вопрос:

Не могли бы вы подробно рассказать о деталях, которые приводят к различным поведениям кэша, как показано пятью областями на следующем графике?

Также может быть интересно указать различия между архитектурами CPU/cache, предоставив аналогичный график для этих процессоров.

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

enter image description here

1885
задан Johannes Gerer 17 дек. '11 в 23:40
источник поделиться
9 ответов

При дальнейшем анализе этого, я считаю, что это (по крайней мере частично) вызвано выравниванием данных четырех указателей. Это приведет к некоторому конфликту в кеше/пути.

Если я правильно понял, как вы распределяете свои массивы, они , скорее всего, будут выровнены со строкой страницы.

Это означает, что все ваши обращения в каждом цикле будут попадать в один и тот же кеш файл. Однако процессоры Intel на некоторое время имели 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;
}

Результаты тестов:

EDIT: результаты на реальной архитектуре архитектуры 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 в комментариях, существует очень вероятная вероятность того, что это выравнивание вызывает ложное сглаживание в единицах загрузки/хранения или кеш. Я задумался об этом и обнаружил, что у Intel на самом деле есть аппаратный счетчик для сглаживания парциальных адресов:

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


5 Регионы - Пояснения

Регион 1:

Это легко. Набор данных настолько мал, что на производительность преобладают накладные расходы, такие как цикл и ветвление.

Регион 2:

Здесь, когда размеры данных увеличиваются, количество относительных накладных расходов уменьшается, а производительность "насыщается". Здесь два цикла медленнее, потому что он имеет в два раза больше потоков и ветвлений.

Я не уверен, что происходит здесь... Выравнивание все равно может сыграть эффект, поскольку Agner Fog упоминает конфликты банковского кэша. (Эта ссылка касается Sandy Bridge, но идея должна быть применима к Core 2.)

Регион 3:

В этот момент данные больше не вписываются в кеш-память L1. Таким образом, производительность ограничена полосой пропускания L1 ↔ L2.

Регион 4:

Снижение производительности в одном цикле - это то, что мы наблюдаем. И, как уже упоминалось, это связано с выравниванием, которое (скорее всего) вызывает блокировки ложных псевдонимов в блоках загрузки/хранения процессора.

Однако для того, чтобы возникло ложное сглаживание, между наборами данных должен быть достаточно большой шаг. Вот почему вы не видите это в регионе 3.

Регион 5:

В этот момент ничего не помещается в кеш. Таким образом, вы связаны пропускной способностью памяти.


2 x Intel X5482 Harpertown @ 3.2 GHzIntel Core i7 870 @ 2.8 GHzIntel Core i7 2600K @ 4.4 GHz

1491
ответ дан Mysticial 18 дек. '11 в 0:17
источник поделиться

ОК, правильный ответ определенно должен что-то сделать с кешем процессора. Но использовать аргумент кеша может быть довольно сложным, особенно без данных.

Есть много ответов, которые привели к большому дискуссию, но пусть сталкиваются с этим: проблемы с кешем могут быть очень сложными и не одномерными. Они сильно зависят от размера данных, поэтому мой вопрос был несправедливым: он оказался очень интересным в графике кэша.

@Мистический ответ убедил многих людей (включая меня), вероятно, потому, что он был единственным, который, казалось, полагался на факты, но это была только одна "точка данных" истины.

Вот почему я объединил его тест (используя непрерывное или отдельное распределение) и ответ @James "Ответ".

На приведенных ниже графиках показано, что большинство ответов и особенно большинство комментариев к вопросу и ответам могут считаться полностью неправильными или истинными в зависимости от конкретного сценария и используемых параметров.

Обратите внимание, что мой первоначальный вопрос был n = 100.000. Эта точка (случайно) проявляет особое поведение:

  • Он обладает наибольшим несоответствием между одной и двумя версиями цикла (почти в три раза)

  • Это единственная точка, где однопетная (а именно, непрерывное распределение) превосходит двухпетлевую версию. (Это сделало возможным Mystical ответ).

Результат с использованием инициализированных данных:

Enter image description here

Результат с использованием неинициализированных данных (это то, что тестировалось Mystical):

Enter image description here

И это трудно объяснить: Инициализированные данные, которые выделяются один раз и повторно используются для каждого следующего тестового примера с разным размером вектора:

Enter image description here

Предложение

Каждый низкоуровневый вопрос, связанный с производительностью, на Qaru должен быть необходим для предоставления информации MFLOPS для всего диапазона размеров данных, соответствующих кешу! Это пустая трата времени для того, чтобы думать о ответах и ​​особенно обсуждать их с другими без этой информации.

185
ответ дан Johannes Gerer 18 дек. '11 в 4:29
источник поделиться

Второй цикл включает в себя намного меньше активности кэша, поэтому процессору легче поддерживать требования к памяти.

58
ответ дан Puppy 17 дек. '11 в 23:47
источник поделиться

Представьте, что вы работаете на машине, где n было правильным значением для того, чтобы было возможно одновременно удерживать два из ваших массивов в памяти, но общая доступная память через кэширование диска все еще была достаточной чтобы держать все четыре.

Предполагая простую политику кэширования LIFO, этот код:

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

сначала приведет к загрузке a1 и b1 в ОЗУ, а затем будет полностью работать в ОЗУ. Когда начинается второй цикл, c1 и d1 затем будут загружаться с диска в ОЗУ и работать.

другой цикл

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

отобразит два массива и страницу в двух других каждый раз вокруг цикла. Это, очевидно, будет намного медленнее.

Вероятно, вы не видите кэширование дисков в своих тестах, но вы, вероятно, видите побочные эффекты какой-либо другой формы кеширования.


Кажется, здесь немного путаницы/непонимания, поэтому я попытаюсь немного придумать пример.

Скажите n = 2, и мы работаем с байтами. В моем сценарии мы имеем всего 4 байта кэша, а остальная часть нашей памяти значительно медленнее (скажем, в 100 раз больше доступа).

Предполагая довольно тупую политику кэширования, если байт не находится в кеше, поместите его туда и получите следующий байт, пока мы на нем, вы получите сценарий примерно так:

  • С

    for(int j=0;j<n;j++){
     a1[j] += b1[j];
    }
    for(int j=0;j<n;j++){
     c1[j] += d1[j];
    }
    
  • cache a1[0] и a1[1], затем b1[0] и b1[1] и установите a1[0] = a1[0] + b1[0] в кеш - в кеше теперь четыре байта, a1[0], a1[1] и b1[0], b1[1]. Стоимость = 100 + 100.

  • установить a1[1] = a1[1] + b1[1] в кеш. Стоимость = 1 + 1.
  • Повторите для c1 и `d1.
  • Общая стоимость = (100 + 100 + 1 + 1) * 2 = 404

  • С

    for(int j=0;j<n;j++){
     a1[j] += b1[j];
     c1[j] += d1[j];
    }
    
  • cache a1[0] и a1[1], затем b1[0] и b1[1] и установите a1[0] = a1[0] + b1[0] в кеш - в кеше теперь четыре байта, a1[0], a1[1] и b1[0], b1[1]. Стоимость = 100 + 100.

  • Извлеките a1[0], a1[1], b1[0], b1[1] из кеша и кеша c1[0] и c1[1], затем d1[0] и d1[1] и установите c1[0] = c1[0] + d1[0] в кеш. Стоимость = 100 + 100.
  • Я подозреваю, что вы начинаете видеть, куда я иду.
  • Общая стоимость = (100 + 100 + 100 + 100) * 2 = 800

Это классический сценарий трэша.

34
ответ дан OldCurmudgeon 18 дек. '11 в 4:36
источник поделиться

Это не из-за другого кода, а из-за кэширования: RAM медленнее, чем регистры процессора, а кэш-память находится внутри ЦП, чтобы избежать записи ОЗУ каждый раз, когда изменяется переменная. Но кеш невелик, так как RAM, следовательно, он отображает только его часть.

Первый код изменяет адреса удаленной памяти, чередуя их в каждом цикле, тем самым требуя постоянно отбрасывать кеш.

Второй код не чередуется: он просто перемещается по смежным адресам дважды. Это заставляет все задания быть завершенными в кеше, аннулируя его только после запуска второго цикла.

25
ответ дан Emilio Garavaglia 17 дек. '11 в 23:49
источник поделиться

Я не могу воспроизвести результаты, обсуждаемые здесь.

Я не знаю, виноват ли плохой тестовый код или что, но два метода находятся на расстоянии 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 С++ 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 секунде.

14
ответ дан RocketRoy 30 дек. '12 в 4:34
источник поделиться

Это потому, что у процессора не так много промахов в кеше (где ему приходится ждать, когда данные массива будут поступать из чипов RAM). Было бы интересно настроить размер массивов постоянно, чтобы вы превышали размеры кеша уровня 1 (L1), а затем кеш уровня 2 (L2), вашего процессора и рассчитайте время, затрачиваемое на выполнение кода, против размеров массивов. Граф не должен быть прямой, как и следовало ожидать.

13
ответ дан James 17 дек. '11 в 23:52
источник поделиться

Первый цикл чередует запись в каждой переменной. Второй и третий только делают небольшие прыжки размера элемента.

Попробуйте написать две параллельные линии из 20 крестов с ручкой и листом, разделенными 20 сантиметрами. Попробуйте один раз закончить одну, а затем другую линию и попробуйте еще раз, нажимая крест в каждой строке попеременно.

11
ответ дан Guillaume Kiz 17 авг. '12 в 18:23
источник поделиться

Оригинальный вопрос

Почему один цикл намного медленнее, чем два цикла?


Оценка проблемы

Код 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 вариантах циклов for и его измененном вопросе о поведении кэшей, а также многие другие отличные ответы и полезные комментарии; Я хотел бы попробовать и сделать что-то другое здесь, используя другой подход к этой ситуации и проблеме.


Подход

Учитывая две петли и всю дискуссию о кеше и обработке страниц, я хотел бы сделать другой подход, чтобы взглянуть на это с другой точки зрения. Тот, который не включает файлы кэша и страницы, а также кавычки для распределения памяти, на самом деле этот подход вообще не касается реального оборудования или программного обеспечения.


Перспектива

Посмотрев на код некоторое время, стало совершенно очевидно, что это проблема и что ее генерирует. Давайте раскроем это на алгоритмическую задачу и рассмотрим ее с точки зрения использования математических обозначений, затем применим аналогию с математическими задачами, а также с алгоритмами.


Что мы знаем

Мы знаем, что его цикл будет работать 100 000 раз. Мы также знаем, что a1, b1, c1 и d1 являются указателями на 64-битную архитектуру. В С++ на 32-битной машине все указатели составляют 4 байта, а на 64-битной машине - 8 байтов, поскольку указатели имеют фиксированную длину. Мы знаем, что у нас есть 32 байта для размещения в обоих случаях. Единственное отличие состоит в том, что мы выделяем 32 байта или 2 набора из 2-8 байтов на каждой итерации, где во втором случае мы выделяем 16 байтов для каждой итерации для обоих независимых циклов. Таким образом, оба цикла по-прежнему равны 32 байтам в общих распределениях. С этой информацией отпустите и покажите общую математику, алгоритм и аналогию с ней. Мы знаем количество раз, когда один и тот же набор или группа операций должны выполняться в обоих случаях. Мы знаем объем памяти, который должен быть выделен в обоих случаях. Мы можем утверждать, что общая рабочая нагрузка распределений между обоими случаями будет примерно одинаковой.


Что мы не знаем

Мы не знаем, сколько времени потребуется для каждого случая, если только мы не установим счетчик и не проверим тест метки. Однако эталонные оценки уже были включены в исходный вопрос, а также из некоторых ответов и комментариев, и мы можем видеть существенную разницу между этими двумя, и это все рассуждение этого вопроса по этой проблеме и для ответа на него начните с.


Пусть исследует

Уже очевидно, что многие уже сделали это, посмотрев на распределение кучи, проверив тесты метки, глядя на ОЗУ, файлы кэша и страницы. Также были рассмотрены конкретные точки данных и конкретные индексы итераций, и в различных беседах об этой конкретной проблеме много людей начинают задавать вопросы о других связанных с этим вещах. Итак, как мы начинаем рассматривать эту проблему, используя математические алгоритмы и применяя к ней аналогию? Мы начинаем с нескольких утверждений! Затем мы построим наш алгоритм оттуда.


Наши утверждения:

  • Мы дадим нашему циклу и его итерациям суммирование, начинающееся с 1 и заканчивающееся на 100000 вместо того, чтобы начинаться с 0, как в циклах, так как нам не нужно беспокоиться о схеме индексирования адресации памяти, поскольку мы просто интересуется самим алгоритмом.
  • В обоих случаях у нас есть 4 функции для работы и 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);  }

2-й случай: - два сложения, но каждый из них имеет свой собственный вызов функции.

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, где оба Sum1 и Sum2 содержат только F1(). Это также будет очевидно и в дальнейшем, когда мы начнем делать вывод о том, что существует некоторый оптимизатор, который происходит из второго алгоритма.

Итерации через первый случай Sum вызывает f(a), который добавит к нему self f(b), затем он вызовет f(c), который сделает то же самое, но добавит f(d) к себе для каждого 100000 iterations. Во втором случае мы имеем 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 область, где 2-й алгоритм имеет 2 последовательных области.

В первом случае при каждом прохождении вызова 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 футов между ними.

Второй случай: The 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;    

Сравнение произвольных значений

Нетрудно видеть, что 600 намного меньше 10 миллионов. Теперь это неточно, потому что мы не знаем фактической разницы в расстоянии между тем, какой адрес ОЗУ или из какого кеша или файла страницы каждый вызов на каждой итерации будет вызван многими другими невидимыми переменными, но это просто оценку ситуации, о которой нужно знать и пытаться взглянуть на нее из наихудшего сценария.

Таким образом, по этим числам было бы почти похоже, что алгоритм должен быть на 99% медленнее алгоритма 2; однако это только часть или ответственность алгоритмов и не учитывает фактических работников A, B, C и D и что они должны делать на каждом и каждая итерация петли. Таким образом, работа боссов составляет лишь около 15-40% от общей выполняемой работы. Таким образом, основная часть работы, выполняемой работниками, имеет несколько большее влияние на сохранение отношения разницы скоростей к примерно 50-70%


Наблюдение:. Различия между двумя алгоритмами

В этой ситуации структура процесса работы выполняется, и она показывает, что Случай 2 более эффективен как из той частичной оптимизации, которая имеет аналогичную декларацию и определение функции где только переменные отличаются по имени. И мы также видим, что общее расстояние, пройденное в случае 1, намного дальше, чем в случае 2, и мы можем считать, что это расстояние преодолело наш временной коэффициент между двумя алгоритмами. Случай 1 имеет гораздо большую работу, чем Случай 2. Это было также видно из свидетельства ASM, которое было показано между обоими случаями. Даже с тем, что уже было сказано об этих случаях, он также не учитывает тот факт, что в случае 1 боссу придется ждать, пока оба A и C вернутся, прежде чем он может вернуться к A снова на следующей итерации, но также не учитывает тот факт, что если A или B занимает очень большое время, тогда как Boss, так и другие работники (работники) также ждут в режиме ожидания. В Case 2 единственное, что находится в режиме ожидания, - это Boss, пока рабочий не вернется. Таким образом, даже это влияет на алгоритм.


Вывод:

Случай 1 - классическая проблема интерполяции, которая оказывается неэффективной. Я также считаю, что это была одна из главных причин того, почему многие машинные архитектуры и разработчики в конечном итоге создали и разработали многоядерные системы, способные выполнять многопоточные приложения, а также параллельное программирование.

Таким образом, даже глядя на это из этого подхода, даже не связавшись с тем, как Hardware, OS и Compiler работают вместе, чтобы распределять кучи, которые связаны с работой с RAM, кешем, файлами страниц и т.д.; математика позади этого уже показывает, какой из этих двух является лучшим решением от использования вышеприведенной аналогии, где Boss или Summations являются теми For Loops, которые должны были перемещаться между Рабочими A и B. Легко видеть, что Случай 2 не меньше, чем на 1/2, если не немного больше, чем Случай 1 из-за разницы пройденного расстояния и времени. И эта математика выстраивается практически практически и идеально с обоими метками скамьи, а также величиной разницы в размере инструкций по сборке.



Измененный вопрос OPs

EDIT: вопрос оказался несущественным, так как поведение сильно зависит от размеров массивов (n) и кэша CPU. Поэтому, если есть еще больший интерес, я перефразирую вопрос:

Не могли бы вы подробно рассказать о деталях, которые приводят к различным типам поведения кэша, как показано пятью областями на следующем графике?

Также может быть интересно указать различия между архитектурами CPU/cache, предоставив аналогичный график для этих процессоров.


Относительно этих вопросов

Как я продемонстрировал, несомненно, существует основная проблема еще до того, как будет задействовано аппаратное и программное обеспечение. Теперь, что касается управления памятью и кэшированием вместе с файлами страниц и т.д., Которые все работают вместе в интегрированном наборе систем: The Architecture {Hardware, Firmware, некоторые встроенные драйверы, ядра и инструкции ASM), The OS {Системы управления файлами и памятью, Драйверы и реестр}, The Compiler {Единицы перевода и оптимизация исходного кода} и даже сам Source Code с его набором (ами) отличительных алгоритмов; мы уже видим, что существует узкое место, которое происходит в первом алгоритме, прежде чем мы даже применим его к любой машине с любыми произвольными Architecture, OS и Programmable Language по сравнению со вторым алгоритмом. Таким образом, уже существовала проблема, прежде чем вовлекать внутреннюю среду современного компьютера.


Конечные результаты

Тем не менее; это не означает, что эти новые вопросы не важны, потому что они сами, и они действительно играют роль в конце концов. Они влияют на процедуры и общую производительность, и это видно на разных графиках и оценках у многих, кто дал свои ответы и комментарии (комментарии). Если вы обратите внимание на аналогию с Boss и двумя рабочими A и B, которым приходилось искать и извлекать пакеты из C и D соответственно и учитывая математические обозначения этих двух рассматриваемых алгоритмов вы можете видеть, что без участия компьютера Case 2 примерно на 60% быстрее, чем Case 1, и когда вы смотрите на графики и диаграммы после того, как эти алгоритмы были применены к исходному коду, скомпилированы и оптимизированы и выполнены через ОС для выполнения операций на данном оборудовании вы даже видите немного большую деградацию между различиями в этих алгоритмах.

Теперь, если набор "Данные" довольно мал, сначала может показаться не так уж плохим, но поскольку 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 он должен был проехать максимальное расстояние только один раз при переходе от A до C.

Итак, старайтесь, чтобы Boss сосредоточился на том, чтобы делать две одинаковые вещи одновременно и подталкивать их туда и обратно, а не фокусироваться на подобных последовательных задачах, заставит его рассердиться до конца дня, потому что он должен был путешествовать и работать в два раза больше. Поэтому не теряйте масштаб ситуации, позволяя вашему боссу попасть в интерполированное узкое место, потому что босс-супруг и дети не оценили бы это.

3
ответ дан Francis Cugler 30 янв. '17 в 17:00
источник поделиться

Другие вопросы по меткам или Задайте вопрос