Огромные различия в реализации?

Я пишу несколько функций для дистрибутивов и использовал нормальный дистрибутив для запуска тестов между моей реализацией и C ++ Boost.

Учитывая функцию плотности вероятности (pdf: http://www.mathworks.com/help/stats/normpdf.html)

Который я написал так:

double NormalDistribution1D::prob(double x) {
return (1 / (sigma * (std::sqrt(boost::math::constants::pi<double>()*2))))*std::exp((-1 / 2)*(((x - mu) / sigma)*((x - mu) / sigma)));
}

Чтобы сравнить мои результаты с тем, как это делается с C ++ Boost:

    boost::math::normal_distribution <> d(mu, sigma);
return boost::math::pdf(d, x);

Я не очень позитивно удивлен — мою версию взяли 44278 нано секунд, повышение
только 326.

Поэтому я немного поиграл и написал метод probboost в своем классе NormalDistribution1D-Class и сравнил все три:

void MATTest::runNormalDistribution1DTest1() {
double mu = 0;
double sigma = 1;
double x = 0;

std::chrono::high_resolution_clock::time_point tn_start = std::chrono::high_resolution_clock::now();
NormalDistribution1D *n = new NormalDistribution1D(mu, sigma);
double nres = n->prob(x);
std::chrono::high_resolution_clock::time_point tn_end = std::chrono::high_resolution_clock::now();

std::chrono::high_resolution_clock::time_point tdn_start = std::chrono::high_resolution_clock::now();
NormalDistribution1D *n1 = new NormalDistribution1D(mu, sigma);
double nres1 = n1->probboost(x);
std::chrono::high_resolution_clock::time_point tdn_end = std::chrono::high_resolution_clock::now();

std::chrono::high_resolution_clock::time_point td_start = std::chrono::high_resolution_clock::now();
boost::math::normal_distribution <> d(mu, sigma);
double dres = boost::math::pdf(d, x);
std::chrono::high_resolution_clock::time_point td_end = std::chrono::high_resolution_clock::now();

std::cout << "Mu : " << mu << "; Sigma: " << sigma << "; x" << x << std::endl;
if (nres == dres) {
std::cout << "Result" << nres << std::endl;
} else {
std::cout << "\033[1;31mRes incorrect: " << nres << "; Correct: " << dres << "\033[0m" << std::endl;
}auto duration_n = std::chrono::duration_cast<std::chrono::nanoseconds>(tn_end - tn_start).count();
auto duration_d = std::chrono::duration_cast<std::chrono::nanoseconds>(td_end - td_start).count();
auto duration_dn = std::chrono::duration_cast<std::chrono::nanoseconds>(tdn_end - tdn_start).count();

std::cout << "own boost: " << duration_dn << std::endl;
if (duration_n < duration_d) {
std::cout << "Boost: " << (duration_d) << "; own implementation: " << duration_n << std::endl;
} else {
std::cout << "\033[1;31mBoost faster: " << (duration_d) << "; than own implementation: " << duration_n << "\033[0m" << std::endl;
}
}

Результаты (компилирование и запуск метода проверки 3 раза)

собственное ускорение: 1082 Повышение быстрее: 326; чем собственная реализация: 44278

собственный буст: 774 Буст быстрее: 216; чем собственная реализация: 34291

собственное ускорение: 769 Повышение быстрее: 230; чем собственная реализация: 33456

Теперь это озадачивает меня:
Как это возможно, что метод из класса занимает в 3 раза больше, чем операторы, которые были вызваны напрямую?

Мои варианты компиляции:

g++ -O2   -c -g -std=c++11 -MMD -MP -MF "build/Debug/GNU-Linux-x86/main.o.d" -o build/Debug/GNU-Linux-x86/main.o main.cpp

g++ -O2    -o ***Classes***

0

Решение

Во-первых, вы распределяете свой объект динамически, с new:

NormalDistribution1D *n = new NormalDistribution1D(mu, sigma);
double nres = n->prob(x);

Если бы вы делали то же самое, что и с бустом, одного этого было бы достаточно, чтобы иметь такую ​​же (или сопоставимую) скорость:

NormalDistribution1D n(mu, sigma);
double nres = n.prob(x);

Теперь, я не знаю, как вы выразили свое выражение в NormalDistribution1D::prob() это важно, но я скептически отношусь к тому, что было бы важно написать это более «оптимизированным» способом, потому что подобные арифметические выражения — это то, что компиляторы могут оптимизировать очень хорошо. Может быть, это будет быстрее, если вы используете --ffast-math переключатель, который даст больше свободы оптимизации компилятору.

Кроме того, если определение double NormalDistribution1D::prob(double x) находится в другом модуле компиляции (другой файл .cpp), компилятор не сможет включить его, и это также приведет к заметным накладным расходам (возможно, в два раза медленнее или меньше). В boost почти все реализовано внутри заголовков, поэтому встроенные строки всегда будут происходить, когда компилятор кажется подходящим. Вы можете преодолеть эту проблему, если вы компилируете и связываете с помощью gcc -flto переключатель.

3

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

Вы не скомпилировали с -ffast-math вариант. Это означает, что компилятор не может (на самом деле, не должен!) Упрощать (-1 / 2)*(((x - mu) / sigma)*((x - mu) / sigma)) в форме, аналогичной той, которая используется в boost::math::pdf,

expo = (x - mu) / sigma
expo *= -x
expo /= 2
result = std::exp(expo)
result /= sigma * std::sqrt(2 * boost::math::constants::pi<double>())

Вышеуказанное заставляет компилятор выполнять быстрые (но потенциально небезопасные / неточные) вычисления, не прибегая к использованию -ffast_math,

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

2

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