Я пишу функцию 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;
}
Ключевая проблема, которую вы пытаетесь решить, состоит в том, что «листовая» функция в ваших вычислениях, которая будет вызываться много раз, также чаще всего не работает, учитывая проблемную область. Надежда состоит в том, что избыточная работа, а именно умножение значения на элемент массива, который во время компиляции считается равным нулю, может быть свернута как часть шага времени компиляции.
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;
}
С учетом этих ограничений я бы создал объект пользовательской функции, который хранит матрицу 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% ненулевых значений. Тем не менее, с этими двумя версиями должно быть относительно просто измерить производительность различных подходов. Я бы не ожидал, что созданный на заказ код будет значительно лучше, хотя это может улучшить вычисления. Если так, то это может работать, чтобы использовать методы мета-программирования для создания кода, но я сомневаюсь, что это было бы слишком практично.
В комментариях вы говорите, что 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. Чтобы избежать повсеместного тестирования на выход за пределы, вы можете хранить каждую диагональ и сам вектор в массивах больших размеров, в которых начальные и конечные элементы заполнены нулями.
Обратите внимание, что это также будет очень эффективно кешировать, поскольку все обращения к массивам являются линейными