Есть ли лучший способ оптимизировать потенциальную функцию Леннарда Джонса? - PullRequest
4 голосов
/ 07 февраля 2011

На самом деле, это производная от потенциала Леннарда Джонса. Причина в том, что я пишу программу Molecular Dynamics, и по крайней мере 80% времени уходит на выполнение следующей функции, даже при самых агрессивных параметрах компилятора (gcc ** -O3).

double ljd(double r) /* Derivative of Lennard Jones Potential for Argon with 
                        respect to distance (r) */ 
{  
    double temp;  
    temp = Si/r;  
    temp = temp*temp;
    temp = temp*temp*temp;  
    return ( (24*Ep/r)*(temp-(2 * pow(temp,2))) );  
}  

Этот код взят из файла "functs.h", который я импортирую в свой основной файл. Я думал, что использование временных переменных таким образом сделает функцию быстрее, но я беспокоюсь, что их создание слишком расточительно. Должен ли я использовать статический? Также код написан параллельно с использованием openmp, поэтому я не могу объявить temp как глобальную переменную?

Определены переменные Ep и Si (с помощью #define). Я использую C только около 1 месяца. Я пытался взглянуть на ассемблерный код, сгенерированный gcc, но я был полностью потерян. \

Ответы [ 6 ]

7 голосов
/ 07 февраля 2011

Я бы избавился от звонка на pow() для начала:

double ljd(double r) /* Derivative of Lennard Jones Potential for Argon with 
                        respect to distance (r) */ 
{  
    double temp;  
    temp = Si / r;  
    temp = temp * temp;
    temp = temp * temp * temp;  
    return ( (24.0 * Ep / r) * (temp - (2.0 * temp * temp)) );  
}  
3 голосов
/ 07 февраля 2011

В моей архитектуре (intel Centrino Duo, MinGW-gcc 4.5.2 в Windows XP) неоптимизированный код с использованием pow()

static inline double ljd(double r)
{
    return 24 * Ep / Si * (pow(Si / r, 7) - 2 * pow(Si / r, 13));
}

фактически превосходит вашу версию, если предоставляется -ffast-math.

Созданная сборка (с использованием некоторых произвольных значений для Ep и Si) выглядит следующим образом:

fldl    LC0
fdivl   8(%ebp)
fld     %st(0)
fmul    %st(1), %st
fmul    %st, %st(1)
fld     %st(0)
fmul    %st(1), %st
fmul    %st(2), %st
fxch    %st(1)
fmul    %st(2), %st
fmul    %st(0), %st
fmulp   %st, %st(2)
fxch    %st(1)
fadd    %st(0), %st
fsubrp  %st, %st(1)
fmull   LC1
1 голос
/ 08 февраля 2011

Локальная переменная просто в порядке.Это ничего не стоит.Оставьте это в покое.

Как говорили другие, избавьтесь от звонка pow.Это не может быть быстрее, чем просто возвести в квадрат число, и это может быть лот медленнее.

Тем не менее, только потому, что функция активна, 80 +% времени незначит, это проблема.Это означает только , если есть что-то, что вы можете оптимизировать, оно либо там, либо в чем-то, что он называет (например, pow), либо в чем-то, что вызывает это .

Если вы попытаетесь случайная пауза , которая является методом выборки из стека, вы увидите, что эта подпрограмма на 80 +% выборок, плюс строки в ней, которые отвечают за время, плюс ее вызывающие, которые отвечают за время, и их абоненты и так далее.Все строки кода в стеке несут совместную ответственность за время.

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

1 голос
/ 08 февраля 2011

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

Также кажется, что вам лучше хранить вокруг значения 1/r, а не r.


Это пример явного использования инструкций SSE2 для реализации функции.ljd() рассчитывает два значения одновременно.

static __m128d ljd(__m128d r)
{
    static const __m128d two = { 2.0, 2.0 };
    static const __m128d si = { Si, Si };
    static const __m128d ep24 = { 24 * Ep, 24 * Ep };

    __m128d temp2, temp3;
    __m128d temp = _mm_div_pd(si, r);
    __m128d ep24_r = _mm_div_pd(ep24, r);

    temp = _mm_mul_pd(temp, temp);
    temp2 = _mm_mul_pd(temp, temp);
    temp2 = _mm_mul_pd(temp2, temp);
    temp3 = _mm_mul_pd(temp2, temp2);
    temp3 = _mm_mul_pd(temp3, two);

    return _mm_mul_pd(ep24_r, _mm_sub_pd(temp2, temp3));
}

/* Requires `out` and `in` to be 16-byte aligned */
void ljd_array(double out[], const double in[], int n)
{
    int i;

    for (i = 0; i < n; i += 2)
    {
        _mm_store_pd(out + i, ljd(_mm_load_pd(in + i)));
    }
}

Однако важно отметить, что последние версии GCC часто могут автоматически векторизовать такие функции, если вы выбираете правильную архитектуру.и включить оптимизацию.Если вы нацеливаетесь на 32-битный x86, попробуйте скомпилировать с -msse2 -O3 и отрегулировать так, чтобы входной и выходной массивы были выровнены по 16 байтов.с атрибутом типа __attribute__ ((aligned (16))), а для динамических массивов - с помощью функции posix_memalign().

1 голос
/ 07 февраля 2011

Ах, это возвращает меня к некоторым воспоминаниям ... Я сделал MD с Леннардом Джонсом много лет назад.

В моем сценарии (не огромные системы) было достаточно заменить pow() нанесколько умножений, как предполагает другой ответ.Я также ограничил диапазон соседей, эффективно обрезая потенциал до r ~ 3.5 и применяя некоторую стандартную термодинамическую коррекцию впоследствии.

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

1 голос
/ 07 февраля 2011

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

const double Si6 = /*whatever pow(Si,6) is*/;
const double Si_value = /*whatever Si is*/; /* need _value as Si is a register name! */
const double Ep24 = /*whatever 24.Ep is*/;

double ljd (double r)
{
  double result;
  __asm
  {
    fld qword ptr [r]
    fld st(0)
    fmul st(0),st(0)
    fld st(0)
    fmul st(0),st(0)
    fmulp st(1),st(0)
    fld qword ptr [Si6]
    fdivrp st(1),st(0)
    fld st(0)
    fld1
    fsub st(0),st(1)
    fsubrp st(1),st(0)
    fmulp st(1),st(0)
    fld qword ptr [Ep24]
    fmulp st(1),st(0)
    fdivrp st(1),st(0)
    fstp qword ptr [result]
  }

  return result;
}

Эта версия будет немного отличаться от опубликованной версии. Компилятор, вероятно, будет записывать промежуточные результаты в ОЗУ в исходном коде. Это приведет к потере точности, поскольку (Intel) FPU работает на внутренней скорости 80 бит, тогда как двойной тип - только 64 бит. Приведенный выше ассемблер не потеряет точности в промежуточных результатах, все это делается на 80 битах. Только конечный результат округляется до 64 бит.

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