числовой — генерация функции C ++ из списка аргументов

Я пишу функцию f для использования в интеграторе Runge Kutta.

output RungeKutta(function f, initial conditions IC, etc.)

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

В этом случае функция f зависит от фиксированного списка параметров вектора p, где p является разреженным и фиксируется до компиляции кода. Быть конкретным,

double function f(vector<double> x) {
return x dot p;
}

поскольку p редкий, принимая скалярное произведение в f не самый эффективный. Hard-кодирование x dot p кажется, что путь, но р может быть очень длинным (1000).

Какие у меня варианты?

Пишет другую программу (принимая p в качестве входных данных) для создания файла .cpp мой единственный вариант?

Спасибо за комментарии. Вот более конкретный пример для дифференциального уравнения.

dy / dx = f_p (x)

Один пример для f_p (x):

р = [0, 1, 0]; х = [х1, х2, х3]

double f_p(vector<double> x) {
return x2;  // This is what I meant by hard-coding
}

вместо:

   double f(vector<double> p, vector<double> x) {
double r = 0;
for (i=0; i < p.length(); i++) {
r += p[i]*x[i];
}
return r;
}

4

Решение

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

C ++ имеет языковые средства для решения этой проблемы, а именно метапрограммирование шаблонов. Шаблоны C ++ очень мощные (т. Е. Завершенные по Тьюрингу) и допускают такие вещи, как рекурсивные вычисления на основе констант времени компиляции.

Ниже приведен пример реализации вашего примера с использованием шаблонов и специализации шаблонов (вы также можете найти работающий пример, который я создал здесь http://ideone.com/BDtBt7). Основная идея кода заключается в создании типа со статической функцией, которая возвращает результирующий точечный продукт входного вектора значений и массива постоянной времени компиляции. Статическая функция рекурсивно вызывает экземпляры самой себя, передавая более низкое значение индекса при перемещении по массивам ввода / константы элементов. Это также зависит от того, является ли значение в массиве констант времени компиляции, которое оценивается, равным нулю. Если это так, мы можем пропустить его вычисление и перейти к следующему значению в рекурсии. Наконец, есть базовый случай, который останавливает рекурсию, как только мы достигли первого элемента в массиве.

#include <array>
#include <iostream>
#include <vector>

constexpr std::array<double, 5> p = { 1.0, 0.0, 3.0, 5.0, 0.0 };

template<size_t index, bool isZero>
struct DotProductCalculator
{
static double Calculate(const std::vector<double>& xArg)
{
return (xArg[index] * p[index])
+ DotProductCalculator<index - 1, p[index - 1] == 0.0>::Calculate(xArg);
}
};

template<>
struct DotProductCalculator<0, true>
{
static double Calculate(const std::vector<double>& xArg)
{
return 0.0;
}
};

template<>
struct DotProductCalculator<0, false>
{
static double Calculate(const std::vector<double>& xArg)
{
return xArg[0] * p[0];
}
};

template<size_t index>
struct DotProductCalculator<index, true>
{
static double Calculate(const std::vector<double>& xArg)
{
return 0.0 + DotProductCalculator<index - 1, p[index - 1] == 0.0>::Calculate(xArg);
}
};

template<typename ArrayType>
double f_p_driver(const std::vector<double>& xArg, const ArrayType& pAsArgument)
{
return DotProductCalculator<std::tuple_size<ArrayType>::value - 1,
p[std::tuple_size<ArrayType>::value -1] == 0.0>::Calculate(xArg);
}

int main()
{
std::vector<double> x = { 1.0, 2.0, 3.0, 4.0, 5.0 };
double result = f_p_driver(x, p);
std::cout << "Result: " << result;
return 0;
}
3

Другие решения

С учетом этих ограничений я бы создал объект пользовательской функции, который хранит матрицу p и вычисляет операцию в своем операторе вызова функции. Я бы реализовал две версии функции: одну, которая предварительно обрабатывает матрицу при построении, чтобы «знать», где находятся ненулевые элементы, и другую, которая просто выполняет операции, как указано, принимая, что многие вычисления просто приводят к 0, Указанное количество ненулевых элементов в 10% кажется слишком плотным для того, чтобы усложнение не использовало разреженности, чтобы окупиться.

Игнорируя это p является матрицей и использует ее как вектор, версия без предварительной обработки будет выглядеть примерно так:

class dotProduct {
std::vector<double> p;
public:
dotProduct(std::vector<double> const& p): p(p) {}
double operator()(std::vector<double> const& x) const {
return std::inner_product(p.begin(), p.end(), x.begin());
}
};
// ...
... RungeKutta(dotProduct(p), initial conditions IC, etc.);

При использовании C ++ 11 вместо этого может использоваться лямбда-функция:

... RungeKutta([=](std::vector<double> const& x) {
return std::inner_product(p.begin(), p.end(), x.begin());
}, intitial conditions IC, etc.);

Для предварительной обработки вы должны сохранить std::vector<std::pair<double, std::size_t>> указание, какие индексы действительно нужно умножить:

class sparseDotProduct {
typedef std::vector<std::pair<double, std::size_t>> Vector;
Vector p;
public:
sparsedotProduct(std::vector<double> const& op) {
for (std::size_t i(0), s(op.size()); i != s; ++i) {
if (op[i]) {
p.push_back(std::make_pair(op[i], i));
}
}
}
double operator()(std::vector<double> const& x) {
double result(0);
for (Vector::const_iterator it(p.begin()), end(p.end()); it != end; ++it) {
result += it->first * x[it->second];
}
return result;
}
};

Использование этого функционального объекта точно такое же, хотя может быть разумным сохранить этот объект, если p не меняется

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

2

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

| a1 b1  0  0  0  0  0 d1 |
| c1 a2 b2  0  0  0  0  0 |
| 0  c2 a3 b3  0  0  0  0 |
| 0  0  c3 a4 b4  0  0  0 |
| 0  0  0  c4 a5 b5  0  0 |
| 0  0  0  0  c5 a6 b6  0 |
| 0  0  0  0  0  c6 a7 b7 |
| e1 0  0  0  0  0  c7 a8 |

Наиболее эффективным способом хранения таких матриц, как правило, является сохранение диагоналей в виде массивов / векторов, поэтому:

A = [a1, a2, a3, a4, a5, a6, a7, a8]
B = [b1, b2, b3, b4, b5, b6, b7]
C = [c1, c2, c3, c4, c5, c6, c7]
D = [d1]
E = [e1]

Умножение строки-вектора X = [x1, x2, x3, x4, x5, x6, x7, x8] по вышеуказанной матрице таким образом становится:

Y = X . M

Y[0] =               X[0] * A[0] + X[1] * C[0] + X[7] * E[0]
Y[1] = X[0] * B[0] + X[1] * A[1] + X[2] * C[1]

и т.п.

или в целом:

Y[i] = X[i-7] * D[i] + X[i-1] * B[i] + X[i] * A[i] + X[i+1] * C[i] + X[i+7] * E[i]

Где доступ к массиву вне диапазона (< 0 или же >= 8 следует рассматривать как оценку до 0. Чтобы избежать повсеместного тестирования на выход за пределы, вы можете хранить каждую диагональ и сам вектор в массивах больших размеров, в которых начальные и конечные элементы заполнены нулями.

Обратите внимание, что это также будет очень эффективно кешировать, поскольку все обращения к массивам являются линейными

1
По вопросам рекламы [email protected]