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

Предположим, что 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) и кеша процессора. Так что, если есть дополнительный интерес, я перефразирую вопрос:

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

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

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

График эффективности


person Johannes Gerer    schedule 17.12.2011    source источник
comment
Это может быть операционная система, которая замедляется при поиске в физической памяти каждый раз, когда вы обращаетесь к ней, и имеет что-то вроде кеша в случае вторичного доступа к тому же мембранному блоку.   -  person AlexTheo    schedule 18.12.2011
comment
Вы компилируете с оптимизацией? Похоже, много кода asm для O2 ...   -  person Luchian Grigore    schedule 18.12.2011
comment
Я спросил, что кажется аналогичным вопросом давно. Это или ответы могут содержать интересную информацию.   -  person Mark Wilkins    schedule 18.12.2011
comment
Жаль, что вы не показали кодовые адреса. Тоже критично.   -  person Hans Passant    schedule 18.12.2011
comment
Чтобы быть разборчивым, эти два фрагмента кода не эквивалентны из-за потенциально перекрывающихся указателей. В C99 есть ключевое слово restrict для таких ситуаций. Не знаю, есть ли в MSVC что-то подобное. Конечно, если бы это была проблема, то код SSE был бы неправильным.   -  person user510306    schedule 18.12.2011
comment
@ user578832 Я только что видел изменение вашего вопроса. Дайте мне время, чтобы ответить на ваш новый вопрос с 5 регионами.   -  person Mysticial    schedule 18.12.2011
comment
@ user578832 Просто к вашему сведению. К вопросу 9 правок. При следующем редактировании он перейдет в Community Wiki. Так что не делайте больше правок. (если это не то, что вы хотите)   -  person Mysticial    schedule 19.12.2011
comment
Это может иметь какое-то отношение к псевдониму памяти. В одном цикле d1[j] может быть псевдонимом a1[j], поэтому компилятор может отказаться от некоторых оптимизаций памяти. Хотя этого не произойдет, если разделить записи в памяти двумя циклами.   -  person rturrado    schedule 19.12.2011
comment
Эти графики были созданы вручную с использованием Excel, а данные получены из моего кода, который я разместил здесь (pastebin.com/ivzkuTzG). Но через некоторое время я перешел на gnuplot, потому что его можно использовать более автоматически.   -  person Johannes Gerer    schedule 20.12.2011
comment
Бьюсь об заклад, счетчики производительности L1D, L1D_CACHE_ST, L2_RQSTS и L2_DATA_RQSTS будут показательными. См. События Intel Core i7 (Nehalem).   -  person jww    schedule 05.04.2014
comment
Возможно, с restrict компилятор разделил бы циклы самостоятельно. Разделение циклов - это то, чем занимаются оптимизаторы.   -  person v.oddou    schedule 16.01.2015
comment
Это будет во многом зависеть от того, как настроены кеши ЦП и аппаратные средства предварительной выборки, а также от пропускной способности системной оперативной памяти ... На некоторых архитектурах одна может работать лучше, чем другая. Возможно, вам будет полезен мой доклад, он об этих вещах. Также затрагивается тема SIMD. youtube.com/watch?v=Nsf2_Au6KxU Кроме того, ваш код не SIMDized: компилятор генерирует только скалярные инструкции SSE. Если вы принудите компилятор к векторизации (я никогда этого не делаю) или просто используете встроенные функции (поначалу гораздо труднее выучить), это может быть еще быстрее.   -  person Sergiy Migdalskiy    schedule 31.08.2015
comment
@ user510306: Оба вывода asm просто загружают / загружают + добавляют / сохраняют. restrict мог бы включить автовекторизацию для обеих версий, а также программную конвейерную обработку (выполнение загрузки за несколько инструкций перед использованием, поэтому буфер переупорядочения не имеет резервных копий с инструкциями, ожидающими данных от загрузок.) В любом случае, с использованием restrict было бы отличной идеей, но помимо эффектов кеширования обе версии asm должны иметь одинаковую производительность.   -  person Peter Cordes    schedule 29.09.2015
comment
Я попробовал это, чтобы проиллюстрировать, что происходит: @ [ideone.com/aJgwsp] ... Попробуйте распечатать сборку для сравнения с тем, что у вас здесь. -Р   -  person C. R. Ward    schedule 12.12.2019
comment
Небольшое отступление: некоторые компиляторы могут разбивать циклы таким способом, это называется «делением цикла». en.wikipedia.org/wiki/Loop_fission_and_fusion   -  person Max Barraclough    schedule 19.05.2020
comment
Вот почему Halide так удобен: он может пробовать разные расписания, не меняя математический алгоритм, и быстро экспериментировать с другими, в отличие от полной реструктуризации кода. (Я только что видел презентацию на CPPCON-20)   -  person JDługosz    schedule 23.10.2020


Ответы (9)


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

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

Это означает, что все ваши обращения в каждом цикле будут выполняться одним и тем же способом кеширования. Однако процессоры Intel какое-то время имели 8-стороннюю ассоциативность кэша L1. Но на самом деле производительность не совсем одинакова. Доступ к четырехстороннему по-прежнему медленнее, чем, скажем, к двухстороннему.

РЕДАКТИРОВАТЬ: на самом деле похоже, что вы выделяете все массивы отдельно. Обычно, когда запрашиваются такие большие выделения, распределитель запрашивает свежие страницы из ОС. Следовательно, высока вероятность того, что большие выделения появятся на одном и том же смещении от границы страницы.

Вот тестовый код:

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/%7Eamplifierxe/pmw_dp/events/partial_address_alias.html


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

Регион 1:

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

Регион 2:

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

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

Регион 3:

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

Регион 4:

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

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

Регион 5:

На данный момент в кеш ничего не помещается. Итак, вы ограничены пропускной способностью памяти.


2 x Intel X5482 Harpertown @ 3,2 ГГц Intel Core i7 870 @ 2,8 ГГц  Intel Core i7 2600K @ 4,4 ГГц

person Mysticial    schedule 17.12.2011
comment
+1: Думаю, это ответ. Вопреки тому, что говорят все другие ответы, речь идет не о варианте с одним циклом, который по своей сути имеет больше промахов в кеше, а о конкретном выравнивании массивов, вызывающем промахи кеша. - person Oliver Charlesworth; 18.12.2011
comment
Этот; наиболее вероятным объяснением является задержка ложного псевдонима. - person Stephen Canon; 18.12.2011

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

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

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

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

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

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

  1. Имеет наибольшее расхождение между одно- и двухконтурной версией (почти в три раза).

  2. Это единственная точка, где однопетлевой (а именно с непрерывным распределением) лучше двухпетлевой версии. (Это вообще сделало возможным ответ Mysticial.)

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

Введите описание изображения здесь

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

Введите описание изображения здесь

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

Введите описание изображения здесь

Предложение

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

person Johannes Gerer    schedule 18.12.2011
comment
+1 Хороший анализ. Я вообще не собирался оставлять данные неинициализированными. Просто так получилось, что распределитель все равно их обнулил. Итак, значение имеют инициализированные данные. Я только что отредактировал свой ответ, указав результаты на машине с реальной архитектурой Core 2, и они намного ближе к тому, что вы наблюдаете. Другое дело, что я тестировал диапазон размеров n, и он показывает такой же разрыв в производительности для n = 80000, n = 100000, n = 200000 и т. Д. - person Mysticial; 18.12.2011

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

person Puppy    schedule 17.12.2011

Представьте, что вы работаете на машине, где 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 в ОЗУ, а затем полностью обработает их в ОЗУ. Когда начинается второй цикл, 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];
    }
    
  • cache 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];
    }
    
  • cache 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

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

person OldCurmudgeon    schedule 18.12.2011
comment
Это неверно. Ссылка на конкретный элемент массива не приводит к тому, что весь массив выгружается с диска (или из некэшированной памяти); выгружается только соответствующая страница или строка кэша. - person Brooks Moses; 19.12.2011

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

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

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

person Emilio Garavaglia    schedule 17.12.2011

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

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

person Community    schedule 30.12.2012

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

person James    schedule 17.12.2011

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

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

person Guillaume Kiz    schedule 17.08.2012

Это может быть старый C ++ и оптимизации. На моем компьютере я получил почти такую ​​же скорость:

Один цикл: 1,577 мс

Два цикла: 1,507 мс

Я запускаю Visual Studio 2015 на процессоре E5-1620 3,5 ГГц с 16 ГБ ОЗУ.

person mathengineer    schedule 11.07.2018