Эффективное использование структуры структур при переходе в функцию и доступ к значениям, хранящимся в C - PullRequest
0 голосов
/ 17 ноября 2018

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

#define NPMAX 50000

typedef struct Particles{
    double *X, *Y, *Z;
} Particles;

typedef struct Properties{
    int Npart;
    double Box[3];
    double minDist;
} Properties;

typedef struct System{
    Properties props;
    Particles parts;
} System;

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;
}

Следующим основным методом:

int main(){
    System sys;
    sys.parts.X = (double *)malloc(sizeof(double) * NPMAX);
    sys.parts.Y = (double *)malloc(sizeof(double) * NPMAX);
    sys.parts.Z = (double *)malloc(sizeof(double) * NPMAX);

    //... some code to populate sys->parts.X, Y, and Z ... 

    sys.props.Npart = 1000;
    sys.props.Box[0] = 10.; //etc.
    sys.props.minDist = 9999.;

    function(&sys);

    // some file I/O

    return;

}

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

  • Доступ и присвоение значений sys->parts.X[i] медленнее, чем создание указателя непосредственно на sys->parts внутри функции и выполнение, например, parts->X[i]?

  • Являются ли переменные, размещенные как в куче, так и в стеке в одной структуре, мудрым выбором по скорости? Из-за этого микса программа теряет время, пытаясь получить доступ к этим значениям в памяти?

  • Стоит ли ожидать, что этот подход будет быстрее, чем просто использование глобальной переменной для каждой отдельной переменной, объявленной в структурах?

У меня есть доступ к компиляторам Intel в дополнение к gcc, и я компилирую с флагом -O3.

Ответы [ 2 ]

0 голосов
/ 18 ноября 2018

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

0 голосов
/ 18 ноября 2018

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

// collect computations first
double dist[NPMAX];
// process 8 64-bit floating-points at a time
int n = sys->props.Npart & ~7;
for(int i = 0; i < n; i += 8){
    _m512d xsq = _mm512_sqrt_pd(&sys->parts.X[i]);
    _m512d ysq = _mm512_sqrt_pd(&sys->parts.Y[i]);
    _m512d zsq = _mm512_sqrt_pd(&sys->parts.Z[i]);
    dist[i] = xsq + ysq + zsq;
}
// deal with remainders (if any)
for (int i = n; i < sys->props.Npart; i++)
    dist[i] = sqrt(sys->parts.X[i]) + sqrt(sys->parts.Y[i]) + sqrt(sys->parts.Z[i]);

// then find lowest
for (int i = 0; i < sys->props.Npart; i++)
    if(dist[i] < sys->props.minDist) sys->props.minDist = dist[i];
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...