Является ли доступ и присвоение значений sys-> parts.X [i] медленнее, чем создание указателя непосредственно на sys-> parts внутри функции и выполнение parts-> X [i], например?
С точки зрения компилятора важны только побочные эффекты.Я думаю, что оба случая должны быть оптимизированы под одну и ту же инструкцию синус-компилятором с хорошей оптимизацией.Давайте проверим это:
void function(System *sys){
double dist;
int i;
for(i=0; i<sys->props.Npart; i++){
dist = pow(sys->parts.X[i],2.) + pow(sys->parts.Y[i],2.) + pow(sys->parts.Z[i],2.);
if(dist<sys->props.minDist) sys->props.minDist=dist;
}
return;
}
void function2(System *sys){
double dist;
int i;
for(i=0; i<sys->props.Npart; i++){
const struct Particles * const p = &sys->parts;
dist = pow(p->X[i],2.) + pow(p->Y[i],2.) + pow(p->Z[i],2.);
if(dist<sys->props.minDist) sys->props.minDist=dist;
}
return;
}
обе функции компилируются в одни и те же инструкции по сборке, как показано на godbolt .В этом посте я использую gcc8.2 с 64-битной архитектурой x86_64.
Имеет ли переменные, распределенные как в куче, так и в стеке в одной и той же структуре, мудрый выбор по скорости?Не теряет ли программа время, пытаясь получить доступ к этим значениям в памяти из-за этого микса?
Реальный ответ должен быть следующим: зависит от архитектуры.На x86_64 я полагаю, что не будет ощутимой разницы между доступом (не выделением) членов массива, когда:
System sys_instance;
System *sys = &sys_instance;
double Xses[NPMAX];
sys->parts.X = Xses;
double Yses[NPMAX];
sys->parts.Y = Yses;
double Zses[NPMAX];
sys->parts.Z = Zses;
и:
System *sys = alloca(sizeof(*sys));
sys->parts.X = alloca(sizeof(*sys->parts.X) * NPMAX);
sys->parts.Y = alloca(sizeof(*sys->parts.Y) * NPMAX);
sys->parts.Z = alloca(sizeof(*sys->parts.Z) * NPMAX);
и:
System *sys = malloc(sizeof(*sys));
sys->parts.X = malloc(sizeof(*sys->parts.X) * NPMAX);
sys->parts.Y = malloc(sizeof(*sys->parts.Y) * NPMAX);
sys->parts.Z = malloc(sizeof(*sys->parts.Z) * NPMAX);
или любой из этих форм.При использовании malloc
или alloca
- оба указателя приводят к тому, что с точки зрения доступа это одно и то же.Но имейте в виду, что кэш процессора и другая архитектура зависит от оптимизации.Использование malloc приведет к значительно более «медленному» распределению.
Стоит ли ожидать, что этот подход будет быстрее, чем просто использование глобальной переменной для каждой отдельной переменной, объявленной в структурах?
Даже если вы делаете:
static System sys_static;
System *sys = &sys_static;
static double X_static[NPMAX];
sys->parts.X = X_static;
static double Y_static[NPMAX];
sys->parts.Y = Y_static;
static double Z_static[NPMAX];
sys->parts.Z = Z_static;
по-прежнему для вашей функции function
передается указатель на sys и все обращения одинаковы.
В тех же редких случаях и когда не используетсяmalloc
при sys
инициализации без побочных эффектов, ваша функция объявлена статической и хороший оптимизатор, она может быть оптимизирована, и sys->props.minDist
может быть предварительно вычислен компилятором на этапе компиляции.Но я бы к этому не стремился, если только вы не хотите использовать C ++ с consteval
или constexpr
.
>
Если число X
и Y
и Z
- это то же самое, что я бы советовал с предложением @WhozCraig.
void function(System *sys){
double dist;
int i;
for(i=0; i<sys->props.Npart; i++){
const struct Particles * const p = &sys->parts[i];
dist = pow(p->X, 2.) + pow(p->Y, 2.) + pow(p->Z, 2.);
if(dist<sys->props.minDist) sys->props.minDist=dist;
}
return;
}
Это сэкономит циклы, необходимые для умножения.Также это уменьшит количество malloc, необходимое для размещения (и изменения размера) элементов.Часть sys->parts[i]
может быть рассчитана один раз для всей строки dist=
.В случае sys->parts.X[i]
sys->parts
может быть рассчитано один раз, тогда для каждого X
и Y
и Z
должно быть вычислено значение pointer + sizeof(elem) * i
.Но в случае достойного компилятора и оптимизатора это не имеет значения.Но на самом деле, этот подход привел к другой сборке, но с одинаковым количеством инструкций, см. godbolt .
Определенно я бы объявил все переменные, которые обозначают размер объекта, как имеющие size_t
type, то есть счетчик цикла i
имеет тип size_t
, а sys->propc.Npart
также будет size_t
type.Они представляют количество элементов, вот для чего используется тип size_t
.
Но я бы определенно вручную оптимизировал цикл.Вы получаете доступ к sys->props.Npart
в каждой проверке цикла.Если остаться с указателями, я бы объявил, что double *X, *Y , *Z;
ограничен друг для друга - я полагаю, вы не ожидаете, что они будут равны.
Также вы получаете доступ к sys->procp.minDist
в каждом цикле и назначаете его условно.Вам нужно указывать sys
здесь только дважды - в начале и в конце (если только у вас нет параллельного кода, который зависит от значения minDist
в середине вычисления, чего, я надеюсь, нет, потому что у вас нет средствсинхронизации в вашем текущем коде).Используйте локальную переменную и обращайтесь к sys
как можно меньше раз.
Я бы заменил вызовы pow
на присвоение переменных (так, чтобы переменная отменялась только один раз) и обычное умножение.Компиляторы могут предположить, что переменная с разыменованным значением может изменить середину цикла, если есть какие-либо назначения - давайте защитимся от этого.Однако хороший оптимизатор оптимизирует вызовы pow(..., 2.)
.
Если производительность так нужна, я бы сказал:
void function3(System * restrict sys){
double minDist = sys->props.minDist;
for (const struct Particles
* const start = &sys->parts[0],
* const stop = &sys->parts[sys->props.Npart],
* p = start; p < stop; ++p) {
const double X = p->X;
const double Y = p->Y;
const double Z = p->Z;
const double dist = X * X + Y * Y + Z * Z;
if (dist < minDist) {
minDist = dist;
}
}
sys->props.minDist = minDist;
return;
}
Это приводит к незначительному уменьшению ассемблерного кода, главным образом потому, что sys->propc.minDist
не доступен каждый раз в цикле, нет необходимости использовать и увеличивать некоторый временный счетчик.Используйте const
s, чтобы дать подсказки компилятору, что вы не будете изменять переменную.