Можно ли оптимизировать этот код интеграции, чтобы он работал быстрее?

9
double trap(double func(double), double b, double a, double N) {
  double j;
  double s;
  double h = (b-a)/(N-1.0); //Width of trapezia

  double func1 = func(a);
  double func2;

  for (s=0,j=a;j<b;j+=h){
    func2 = func(j+h);
    s = s + 0.5*(func1+func2)*h;
    func1 = func2;
  }

  return s;
}

Выше приведен мой код C ++ для одномерного численного интегрирования (с использованием расширенного правила трапеции) func()между пределами с использованием трапеции .[a,б]N-1

Я на самом деле делаю 3D-интеграцию, где этот код называется рекурсивно. Я работаю с давая мне достойные результаты.Nзнак равно50

Кроме дальнейшего сокращения , кто-нибудь может предложить, как оптимизировать приведенный выше код, чтобы он работал быстрее? Или даже можете предложить более быстрый способ интеграции?N

user2970116
источник
5
Это не совсем относится к вопросу, но я бы предложил выбрать более подходящие имена переменных. Как trapezoidal_integrationвместоtrap , sumили running_totalвместо того , чтобы s(а также использовать +=вместо s = s +), trapezoid_widthили dxвместо h(или нет, в зависимости от предпочитаемого обозначения трапеций), а также изменений func1и func2отразить тот факт , что они являются ценностями, а не функции. Например func1-> previous_valueи func2-> current_valueили что-то в этом роде.
Дэвид З

Ответы:

5

Математически ваше выражение эквивалентно:

язнак равночас(12е1+е2+е3+,,,+еN-1+12еN)+О((б-a)3е"N2)

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

Квадратура Гаусса в наши дни немного больше, чем игрушка; полезно только если вам требуется очень мало оценок. Если вы хотите что-то простое в реализации, вы можете использовать правило Симпсона, но я бы не пошел дальше, чем заказать без веской причины.1/N3

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

Davidmh
источник
Уйдя и вернувшись к проблеме, я решил реализовать правило Симпсона. Но могу ли я проверить, что на самом деле ошибка в составном правиле Симпсона пропорциональна 1 / (N ^ 4) (не 1 / (N ^ 3), как вы подразумеваете в своем ответе)?
user2970116
1
У вас есть формулы для а также . Первый использует коэффициенты а второй . 1/N31/N45/12,13/12,1,1...1,1,13/12,15/121/3,4/3,2/3,4/3 ...
Davidmh
9

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

В зависимости от свойств func (), также возможно, что вы могли бы получить более точную оценку интеграла с меньшим количеством оценок функций, используя более сложную формулу интеграции.

Брайан Борхерс
источник
1
Верно. Если ваша функция гладкая, вы, как правило, можете получить менее 50 оценок функций, если, например, используете квадратурное правило Гаусса-4 только на 5 интервалах.
Вольфганг Бангерт
7

Возможный? Да. Полезно? Нет. Оптимизации, которые я собираюсь перечислить здесь, вряд ли дадут более чем незначительную долю процента разницы во времени выполнения. Хороший компилятор может уже сделать это для вас.

Во всяком случае, глядя на ваш внутренний цикл:

    for (s=0,j=a;j<b;j+=h){
        func2 = func(j+h);
        s = s + 0.5*(func1+func2)*h;
        func1 = func2;
    }

На каждой итерации цикла вы выполняете три математические операции, которые можно вывести за пределы: сложение j + h, умножение на 0.5и умножение на h. Первое, что вы можете исправить, запустив переменную итератора в a + h, а другие, вычленив умножения:

    for (s=0, j=a+h; j<=b; j+=h){
        func2 = func(j);
        s += func1+func2;
        func1 = func2;
    }
    s *= 0.5 * h;

Хотя я хотел бы отметить, что при этом из-за ошибки округления с плавающей запятой можно пропустить последнюю итерацию цикла. (Это было также проблемой в вашей первоначальной реализации.) Чтобы обойти это, используйте unsigned intили size_tсчетчик:

    size_t n;
    for (s=0, n=0, j=a+h; n<N; n++, j+=h){
        func2 = func(j);
        s += func1+func2;
        func1 = func2;
    }
    s *= 0.5 * h;

Как говорит ответ Брайана, ваше время лучше потратить на оптимизацию оценки функции func. Если точность этого метода достаточна, я сомневаюсь, что вы найдете что-то быстрее для того же N. (Хотя вы могли бы запустить некоторые тесты, чтобы увидеть, например, позволяет ли Runge-Kutta снизить уровень Nшума, чтобы полная интеграция занимала меньше времени, не жертвуя точностью.)

Дэвид З
источник
4

Есть несколько изменений, которые я бы порекомендовал улучшить для вычисления:

  • Для производительности и точности используйте std::fma(), который выполняет слитое умножение-сложение .
  • Для производительности отложите умножение площади каждой трапеции на 0,5 - вы можете сделать это один раз в конце.
  • Избегайте повторного добавления h, которое может накапливать ошибки округления.

Кроме того, я бы внес несколько изменений для ясности:

  • Дайте функции более описательное имя.
  • Поменяйте местами порядок aиb в сигнатуре функции.
  • Переименовать Nn, hdx, jx2, saccumulator .
  • Изменить nнаint .
  • Объявите переменные в более узком объеме.
#include <cmath>

double trapezoidal_integration(double func(double), double a, double b, int n) {
    double dx = (b - a) / (n - 1);   // Width of trapezoids

    double func_x1 = func(a);
    double accumulator = 0;

    for (int i = 1; i <= n; i++) {
        double x2 = a + i * dx;      // Avoid repeated floating-point addition
        double func_x2 = func(x2);
        accumulator = std::fma(func_x1 + func_x2, dx, accumulator); // Fused multiply-add
        func_x1 = func_x2;
    }

    return 0.5 * accumulator;
}
200_success
источник
3

Если ваша функция является полиномом, возможно взвешенным какой-либо функцией (например, гауссианом), вы можете выполнить точное интегрирование в 3d непосредственно с кубатурной формулой (например, http://people.sc.fsu.edu/~jburkardt/c_src/ stroud / stroud.html ) или с разреженной сеткой (например, http://tasmanian.ornl.gov/ ). Эти методы просто задают набор точек и весов, на которые умножается значение функции, поэтому они очень быстрые. Если ваша функция достаточно гладкая, чтобы ее можно было аппроксимировать полиномами, тогда эти методы могут дать очень хороший ответ. Формулы специализируются на типе функции, которую вы интегрируете, поэтому может потребоваться некоторое копание, чтобы найти правильную.

Роналду Карпио
источник
3

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

Это может дать вам небольшой выигрыш, но это будет мало. Существуют гораздо более эффективные методы численного интегрирования. Google для "правила Симпсона", "Рунге-Кутта" и "Фельберг". Все они работают примерно одинаково, оценивая некоторые значения функции и умно добавляя кратные значения этих значений, создавая гораздо меньшие ошибки с тем же числом оценок функции или ту же ошибку с гораздо меньшим числом оценок.

gnasher729
источник
3

Есть много способов сделать интеграцию, из которых правило трапеции является самым простым.

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

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

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

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

Майк Данлавей
источник
1

Итак, как было указано в других ответах, это сильно зависит от того, насколько дорогая ваша функция. Оптимизация вашего кода trapz того стоит, только если это действительно ваше узкое место. Если это не совсем очевидно, вы должны проверить это путем профилирования вашего кода (такие инструменты, как Intel V-tune, Valgrind или Visual Studio могут это сделать).

Однако я бы предложил совершенно другой подход: интеграция Монте-Карло . Здесь вы просто аппроксимируете интеграл путем выборки вашей функции в случайных точках, добавляя результаты. Смотрите этот PDF в дополнение к странице вики для деталей.

Это работает очень хорошо для данных больших размеров, как правило, намного лучше, чем квадратурные методы, используемые в 1-й интеграции.

Простой случай очень прост в реализации (см. Pdf), но будьте осторожны, так как стандартная случайная функция в c ++ 98 довольно плоха как по производительности, так и по качеству. В C ++ 11 вы можете использовать Mersenne Twister в.

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

LKlevin
источник
1
Я на самом деле делаю 3D-интеграцию, где этот код называется рекурсивно.

«рекурсивно» является ключом. Вы либо просматриваете большой набор данных и рассматриваете множество данных более одного раза, либо фактически генерируете свой набор данных из (кусочно?) Функций.

Рекурсивно оцениваемые интеграции будут смехотворно дорогими и смехотворно неточными, поскольку полномочия в рекурсии увеличиваются.

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

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

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

Дэвид
источник
1

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

    double trap(double func(double), double b, double a, double N){
double j, s;
double h = (b-a)/(N-1.0); //Width of trapezia

double s = 0;
j = a;
for(i=1; i<N-1; i++){
  j += h;
  s += func(j);
}
s += (func(a)+func(b))/2;

return s*h;
}
Аксакал почти наверняка бинарный
источник
1
Пожалуйста, дайте обоснование ваших изменений и кода. Блок кода довольно бесполезен для большинства людей.
Годрик Провидец
Согласовано; Пожалуйста, объясните свой ответ.
Джефф Оксберри