Подтвердить что ты не робот

Может ли моя петля быть оптимизирована?

Ниже мой внутренний цикл, который выполняется несколько тысяч раз, с размерами ввода от 20 до 1000 или более. Эта часть кода занимает 99 - 99,5% времени выполнения. Есть ли что-нибудь, что я могу сделать, чтобы помочь выжать из этого больше производительности?

Я не собираюсь переместить этот код на что-то вроде использования древовидных кодов (Barnes-Hut), но для оптимизации фактических вычислений, происходящих внутри, поскольку одни и те же вычисления происходят в алгоритме Барнса-Хат.

Любая помощь приветствуется!

Изменить: я работаю в 64-разрядной версии Windows 7 с выпуском Visual Studio 2008 на Core 2 Duo T5850 (2,16 ГГц)

typedef double real;

struct Particle
{
    Vector pos, vel, acc, jerk;
    Vector oldPos, oldVel, oldAcc, oldJerk;
    real mass;
};

class Vector
{
private:
    real vec[3];

public:
    // Operators defined here
};

real Gravity::interact(Particle *p, size_t numParticles)
{
    PROFILE_FUNC();
    real tau_q = 1e300;

    for (size_t i = 0; i < numParticles; i++)
    {
        p[i].jerk = 0;
        p[i].acc = 0;
    }

    for (size_t i = 0; i < numParticles; i++)
    {
        for (size_t j = i+1; j < numParticles; j++)
        {
            Vector r = p[j].pos - p[i].pos;
            Vector v = p[j].vel - p[i].vel;
            real r2 = lengthsq(r);
            real v2 = lengthsq(v);

            // Calculate inverse of |r|^3
            real r3i = Constants::G * pow(r2, -1.5);

            // da = r / |r|^3
            // dj = (v / |r|^3 - 3 * (r . v) * r / |r|^5
            Vector da = r * r3i;
            Vector dj = (v - r * (3 * dot(r, v) / r2)) * r3i;

            // Calculate new acceleration and jerk
            p[i].acc += da * p[j].mass;
            p[i].jerk += dj * p[j].mass;
            p[j].acc -= da * p[i].mass;
            p[j].jerk -= dj * p[i].mass;

            // Collision estimation
            // Metric 1) tau = |r|^2 / |a(j) - a(i)|
            // Metric 2) tau = |r|^4 / |v|^4
            real mij = p[i].mass + p[j].mass;
            real tau_est_q1 = r2 / (lengthsq(da) * mij * mij);
            real tau_est_q2 = (r2*r2) / (v2*v2);

            if (tau_est_q1 < tau_q)
                tau_q = tau_est_q1;
            if (tau_est_q2 < tau_q)
                tau_q = tau_est_q2;
        }
    }

    return sqrt(sqrt(tau_q));
}
4b9b3361

Ответ 1

  • Ввести вызовы lengthsq().

  • Измените pow (r2, -1.5) на 1/(r2 * sqrt (r2)), чтобы снизить стоимость вычисления r ^ 1.5

  • Используйте скаляры (p_i_acc и т.д.) внутри самой внутренней петли, а не p [i].acc, чтобы собрать результат. Компилятор может не знать, что p [i] не сглажен с p [j], и это может принудительно адресовать p [i] на каждой итерации цикла без необходимости.

4а. Попробуйте заменить if (...) tau_q = с помощью

    tau_q=minimum(...,...)

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

4b. [РЕДАКТИРОВАТЬ, чтобы разделить 4а и 4b друг от друга]. Возможно, вы захотите сохранить tau _.. q2 вместо tau_q и сравнить с r2/v2, а не r2 * r2/v2 * v2. Затем вы избегаете делать два умножения для каждой итерации во внутреннем цикле, в торговле для одной операции возведения в квадрат, чтобы вычислить tau..q2 в конце. Для этого собирайте минимум tau_q1 и tau_q2 (не в квадрате) отдельно и принимайте минимум этих результатов в одной скалярной операции по завершении цикла]

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

Помимо этого вам нужно рассмотреть возможность разворачивания цикла, b) векторизация (с использованием инструкций SIMD, ассемблера с ручным кодированием или с использованием компилятора Intel, который, как предполагается, очень хорош в этом [но у меня нет опыта работы с ним], и c) переход многоядерных (с использованием OpenMP).

Ответ 2

Эта строка real r3i = Constants::G * pow(r2, -1.5); будет больно. Любой вид поддержки sqrt или конкретной платформы с квадратным корнем помог бы.

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

Что-то, что приходит на ум, - вы сохраняете достаточную точность с вашим calc? Взятие вещей к 4-й мощности и 4-й корень действительно разбивают ваши доступные биты через блендер under/overflow. Я был бы уверен, что ваш ответ действительно ваш ответ, когда он завершен.

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

Другая мысль. Поскольку это, по-видимому, связано с гравитацией, есть ли способ отбросить вашу тяжелую математику на основе дистанционной проверки? В принципе, проверка квадрата/радиуса радиуса для борьбы с O (n ^ 2) поведением вашего цикла. Если вы удалите 1/2 частица, она будет работать быстрее на x4.

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

Ответ 3

  • Во-первых, вам необходимо профайл кода. Метод для этого будет зависеть от того, какой процессор и ОС вы используете.

  • Вы можете подумать, можете ли вы использовать float, а не double.

  • Если вы используете gcc, убедитесь, что используете -O2 или, возможно, -O3.

  • Вы также можете попробовать хороший компилятор, например, Intel ICC (если он работает на x86?).

  • Опять же, предполагая, что это (Intel) x86, если у вас есть 64-разрядный процессор, тогда создайте 64-разрядный исполняемый файл, если вы еще не сделали этого - дополнительные регистры могут сделать заметную разницу (около 30%),.

Ответ 4

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

Ответ 5

Легкая вещь сначала: переместите все "старые" переменные в другой массив. Вы никогда не обращаетесь к ним в своем основном цикле, поэтому вы трогаете вдвое больше памяти, чем вам действительно нужно (и, следовательно, получаете в два раза больше промахов в кеше). Вот недавний пост в блоге по теме: http://msinilo.pl/blog/?p=614. И, конечно, вы могли prefetch несколько частиц вперед, например. p [j + k], где k - некоторая константа, которая проведет некоторое экспериментирование.


Если вы также перемещаете массив, вы можете хранить такие вещи:

struct ParticleData
{
    Vector pos, vel, acc, jerk;
};

ParticleData* currentParticles = ...
ParticleData* oldParticles = ...
real* masses = ...

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


Если вы захотите сделать код немного уродливым, вы сможете улучшить оптимизацию SIMD, сохранив вещи в "транспонированном" формате, например

struct ParticleData
{
    // data_x[0] == pos.x, data_x[1] = vel.x, data_x[2] = acc.x, data_x[3] = jerk.x
    Vector4 data_x;

    // data_y[0] == pos.y, data_y[1] = vel.y, etc.
    Vector4 data_y;

    // data_z[0] == pos.z, data_y[1] = vel.z, etc.
    Vector4 data_z;
};

где Vector4 представляет собой либо одноточечный, либо два двумерных вектора двойной точности. Этот формат распространен в трассировке лучей для одновременного тестирования нескольких лучей; он позволяет выполнять операции, такие как точечные продукты, более эффективно (без перетасовки), а также означает, что ваши нагрузки на память могут быть выровнены по 16 байт. Это определенно занимает несколько минут, чтобы обернуть вашу голову, хотя:)

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

Ответ 6

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

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

Ответ 7

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

Пример SSE:

http://bitbucket.org/ademiller/nbody/src/tip/NBody.DomainModel.Native/LeapfrogNativeIntegratorImpl.cpp

Новый алгоритм кэширования, пояснения и пример кода:

http://software.intel.com/en-us/articles/a-cute-technique-for-avoiding-certain-race-conditions/

http://bitbucket.org/ademiller/nbody/src/tip/NBody.DomainModel.Native.Ppl/LeapfrogNativeParallelRecursiveIntegratorImpl.cpp

Вы также можете найти следующую колоду, которую я дал в Seattle Code Camp:

http://www.ademiller.com/blogs/tech/2010/04/seattle-code-camp/

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

Ответ 8

Помимо простого добавления/вычитания/деления/умножения, pow() - единственная тяжеловесная функция, которую я вижу в теле цикла. Это, вероятно, довольно медленно. Можете ли вы прекомпретировать его или избавиться от него или заменить его чем-то более простым?

Что real? Это может быть поплавок?

Кроме того, вам придется перейти на MMX/SSE/сборку.

Ответ 9

Вы могли бы воспользоваться знаменитым алгоритмом алгоритм быстрого обратного квадратного корня?

float InvSqrt(float x)
{
    union {
        float f;
        int i;
    } tmp;
    tmp.f = x;
    tmp.i = 0x5f3759df - (tmp.i >> 1);
    float y = tmp.f;
    return y * (1.5f - 0.5f * x * y * y);
}

Он возвращает достаточно точное представление 1/r ** 2 (первая итерация метода Ньютона с умным начальным предположением). Он широко используется для компьютерной графики и разработки игр.

Ответ 10

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

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

Рассмотрим разбиение нашей структуры частиц на пару структур. Ваш первый цикл через данные до reset всех значений acc и jerk может быть эффективным memset, если вы это сделали. Тогда у вас было бы по существу два массива (или векторы), где частица "n" хранится в индексе "n" каждого из массивов.

Ответ 11

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

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

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

Ответ 12

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

Вы можете использовать разворот цикла.

также помните несколько с меньшими частями проблемы: -

  for (size_t i = 0; i < numParticles; i++)
    {
        for (size_t j = i+1; j < numParticles; j++)
        {

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

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

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

но на самом деле нужно знать, где его стоить больше всего

Ответ 13

Вы должны повторно использовать реалы и векторы, которые вы всегда используете. Стоимость создания Vector или Real может быть тривиальной, но не если numParticles очень велика, особенно с вашим, казалось бы, циклом O ((n ^ 2)/2).

Vector r;
Vector v;
real r2;
real v2;
Vector da;
Vector dj;
real r3i;
real mij;
real tau_est_q1;
real tau_est_q2;
for (size_t i = 0; i < numParticles; i++)
    {
        for (size_t j = i+1; j < numParticles; j++)
        {
            r = p[j].pos - p[i].pos;
            v = p[j].vel - p[i].vel;
            r2 = lengthsq(r);
            v2 = lengthsq(v);

            // Calculate inverse of |r|^3
            r3i = Constants::G * pow(r2, -1.5);

            // da = r / |r|^3
            // dj = (v / |r|^3 - 3 * (r . v) * r / |r|^5
            da = r * r3i;
            dj = (v - r * (3 * dot(r, v) / r2)) * r3i;

            // Calculate new acceleration and jerk
            p[i].acc += da * p[j].mass;
            p[i].jerk += dj * p[j].mass;
            p[j].acc -= da * p[i].mass;
            p[j].jerk -= dj * p[i].mass;

            // Collision estimation
            // Metric 1) tau = |r|^2 / |a(j) - a(i)|
            // Metric 2) tau = |r|^4 / |v|^4
            mij = p[i].mass + p[j].mass;
            tau_est_q1 = r2 / (lengthsq(da) * mij * mij);
            tau_est_q2 = (r2*r2) / (v2*v2);

            if (tau_est_q1 < tau_q)
                tau_q = tau_est_q1;
            if (tau_est_q2 < tau_q)
                tau_q = tau_est_q2;
        }
    }

Ответ 14

Вы можете заменить любое вхождение:

a = b/c
d = e/f

с

icf = 1/(c*f)
a = bf*icf
d = ec*icf

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

Если вы используете другие схемы интеграции (например, Runge-Kutta), у вас будет меньше шагов времени, но я подозреваю, что вы уже знаете это.