Предположим, что 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
.)
restrict
для таких ситуаций. Не знаю, есть ли в MSVC что-то подобное. Конечно, если бы это была проблема, то код SSE был бы неправильным. - person user510306   schedule 18.12.2011d1[j]
может быть псевдонимомa1[j]
, поэтому компилятор может отказаться от некоторых оптимизаций памяти. Хотя этого не произойдет, если разделить записи в памяти двумя циклами. - person rturrado   schedule 19.12.2011L1D
,L1D_CACHE_ST
,L2_RQSTS
иL2_DATA_RQSTS
будут показательными. См. События Intel Core i7 (Nehalem). - person jww   schedule 05.04.2014restrict
компилятор разделил бы циклы самостоятельно. Разделение циклов - это то, чем занимаются оптимизаторы. - person v.oddou   schedule 16.01.2015restrict
мог бы включить автовекторизацию для обеих версий, а также программную конвейерную обработку (выполнение загрузки за несколько инструкций перед использованием, поэтому буфер переупорядочения не имеет резервных копий с инструкциями, ожидающими данных от загрузок.) В любом случае, с использованиемrestrict
было бы отличной идеей, но помимо эффектов кеширования обе версии asm должны иметь одинаковую производительность. - person Peter Cordes   schedule 29.09.2015