Чи можна оптимізувати цей інтеграційний код, щоб він працював швидше?


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 ++ код для 1D числової інтеграції (використовуючи розширене правило трапеції) func()між межами[a,b] використовуючи N1 трапеція.

Я фактично роблю 3D-інтеграцію, де цей код називається рекурсивно. Я працюю зN=50 даючи мені гідні результати.

Крім скорочення Nдалі, чи хтось може підказати, як оптимізувати код вище, щоб він працював швидше? Або, навіть, може запропонувати швидший метод інтеграції?


5
Це не дуже важливо для питання, але я б запропонував вибрати кращі імена змінних. Як і trapezoidal_integrationзамість trap, sumабо running_totalзамість s(і також використовувати +=замість s = s +), trapezoid_widthабо dxзамість h(або ні, залежно від бажаного позначення трапецієподібного правила), і змінювати func1та func2відображати той факт, що вони є значеннями, а не функціями. Наприклад func1-> previous_valueі func2-> current_value, або щось подібне.
David Z

Відповіді:


5

Математично ваш вираз еквівалентний:

I=h(12f1+f2+f3+...+fn1+12fn)+O((ba)3fn2)

Тож ви могли це реалізувати. Як було сказано, в оцінці функції, напевно, переважає час, тому для отримання однакової точності можна скористатися кращим методом інтеграції, який вимагає менших оцінок функцій.

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

Якщо кривизна функції сильно змінюється, ви можете використовувати процедуру адаптивного кроку, яка б вибрала більший крок, коли функція рівна, і менший більш точний, коли кривизна вища.


Відійшовши і повернувшись до проблеми, я вирішив застосувати правило Сімпсона. Але чи можу я перевірити, що насправді помилка в складеному правилі Сімпсона пропорційна 1 / (N ^ 4) (не 1 / (N ^ 3), як ви розумієте у своїй відповіді)?
користувач2970116

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

3

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


3

Коли ви намагаєтеся обчислити інтеграл чисельно, ви намагаєтеся домогтися точності, яку ви хочете, з найменшими зусиллями, або, як альтернатива, намагаєтесь досягти максимально можливої ​​точності з фіксованим зусиллям. Ви, здається, запитуєте, як змусити код для одного конкретного алгоритму якомога швидше.

Це може принести вам невеликий прибуток, але воно буде мало. Існують набагато ефективніші методи чисельної інтеграції. Google для "правила Сімпсона", "Рунге-Кутта" та "Фельберга". Усі вони працюють досить схоже, оцінюючи деякі значення функції і вміло додаючи кратні значення, створюючи набагато менші помилки з однаковою кількістю оцінок функції або однакову помилку зі значно меншою кількістю оцінок.


3

Існує маса способів інтеграції, серед яких трапецієподібне правило - про найпростіший.

Якщо ви взагалі що-небудь знаєте про фактичну функцію, яку ви інтегруєте, ви можете зробити це краще, якщо ви це будете використовувати. Ідея полягає в тому, щоб мінімізувати кількість точок сітки в межах допустимих рівнів помилок.

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

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

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


1

Отже, як було зазначено в інших відповідях, це сильно залежить від того, наскільки дорога ваша функція. Оптимізація коду трапса варто лише тоді, якщо він справді є вашим вузьким місцем. Якщо це не зовсім очевидно, вам слід перевірити це шляхом профілювання коду (такі інструменти, як Intel-V-tune, Valgrind або Visual Studio, можуть це зробити).

Однак я б запропонував зовсім інший підхід: інтеграція Монте-Карло . Тут ви просто наближаєте інтеграл, вибираючи свою функцію у випадкових точках, додаючи результати. Докладні відомості див. У цьому PDF-файлі на додаток до сторінки вікі.

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

Простий випадок дуже легко здійснити (див. Pdf), просто будьте уважні, що стандартна випадкова функція в c ++ 98 є досить поганою як за продуктивністю, так і за якістю. У c ++ 11 ви можете використовувати Mersenne Twister в.

Якщо ваша функція має багато варіацій в одних областях і менше в інших, подумайте про використання стратифікованої вибірки. Я б рекомендував використовувати наукову бібліотеку GNU , а не писати власну.


1
Я фактично роблю 3D-інтеграцію, де цей код називається рекурсивно.

"рекурсивно" є ключовим. Ви або переглядаєте великий набір даних і не раз обмірковуєте безліч даних, або фактично генеруєте свій набір даних з (частково?) Функцій.

Рекурсивно оцінені інтеграції будуть смішно дорогими та смішно неточними в міру збільшення повноважень у рекурсії.

Створіть модель для інтерполяції набору даних та зробіть кускову символічну інтеграцію. Оскільки велика кількість даних згортається на коефіцієнти базових функцій, складність для більш глибокої рекурсії зростає поліноміально (і зазвичай досить низькими потужностями), а не експоненціально. І ви отримуєте "точні" результати (для отримання розумних чисельних показників потрібно все-таки розробити хороші схеми оцінювання, але все-таки це має бути досить доцільним для кращого, ніж трапецієвидна інтеграція).

Якщо ви подивитесь на оцінки помилок для трапецієподібних правил, ви побачите, що вони пов'язані з деякою похідною від залучених функцій, і якщо інтеграція / визначення виконується рекурсивно, функції не мають тенденції мати добре поведені похідні .

Якщо ваш єдиний інструмент - молоток, кожна проблема виглядає як цвях. У той час як ви ледь торкаєтесь проблеми у своєму описі, я маю підозру, що застосування трапецієподібного правила рекурсивно - це погана відповідність: ви отримуєте вибух як неточності, так і вимог до обчислень.


1

вихідний код оцінює функцію в кожній з N точок, потім додає значення вгору і множує суму на розмір кроку. Єдина хитрість полягає в тому, що значення на початку і в кінці додаються з вагою , а всі точки всередині додаються з повною вагою. насправді вони також додаються з масою але вдвічі. замість того, щоб додавати їх двічі, додайте їх лише один раз з повною вагою. множимо множення на розмір кроку поза петлею. це все, що можна зробити, щоб прискорити це, насправді.1/21/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
Будь ласка, дайте міркування щодо змін та код. Блок коду досить марний для більшості людей.
Годрик Провидник

Домовились; будь ласка, поясніть свою відповідь.
Джефф Оксберрі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.