Я хочу создать функцию C++
, которая повышает каждый элемент в x
до power
и принимает среднее значение. Я создал три версии:
R
решение -- mean(x^power)
C++
решениеC++
решение с дополнительным power
аргументомДополнительный аргумент power
, кажется, резко замедляет функцию до такой степени, что она медленнее, чем реализация R. Это реальность использования Rcpp
или я могу что-то улучшить в своем коде?
#library(Rcpp)
cppFunction(
'double power_mean_C_2arg(NumericVector x, double power) {
int n = x.size();
double total = 0;
for(int i=0; i<n; ++i) {
total += pow(x[i], power);
}
return total / n;
}'
)
cppFunction(
'double power_mean_C(NumericVector x) {
int n = x.size();
double total = 0;
for(int i=0; i<n; ++i) {
total += pow(x[i], 2);
}
return total / n;
}'
)
power_mean_R <- function(x, power) {
mean(x^power)
}
bench::mark(
R = power_mean_R(1:100, p = 2),
C = power_mean_C(1:100),
C2arg = power_mean_C_2arg(x = 1:100, p = 2)
)
expression min median `itr/sec` mem_alloc `gc/sec` n_itr n_gc total_time result memory
<bch:expr> <bch:> <bch:> <dbl> <bch:byt> <dbl> <int> <dbl> <bch:tm> <list> <list>
1 R 5.91µs 6.91µs 112386. 1.27KB 0 10000 0 89ms <dbl … <Rpro…
2 C 2.87µs 3.54µs 231281. 3.32KB 23.1 9999 1 43.2ms <dbl … <Rpro…
3 C2arg 6.02µs 6.89µs 116187. 3.32KB 0 10000 0 86.1ms <dbl … <Rpro…
1. Функция R уже настолько эффективна, насколько это возможно, и уже кричит вам о векторизации. 2. Вы можете немного выиграть с меньшим количеством проверок безопасности, но это не рекомендуется. 3. Вы можете выиграть больше через OpenMP, но это требует больше работы. Короче говоря, это ориентир для легкой победы.
pow(x[i], 2) скорее всего медленнее, чем x[i]*x[i]
Также обратите внимание, что ^
и mean
фактически являются просто обертками вокруг функций .Primitive
и .Internal
, которые являются функциями C/C++.
Даниэль, я обсуждал («обсудил»?) эту подобную тему с Дирком (много лет назад), и он «победил» и с тех пор неоднократно подтверждал свою правоту. Хотя я понял поговорку «Преждевременная оптимизация — корень всех зол», я не особо применял ее на практике (ну), думая, что смогу все исправить, перейдя сразу к Rcpp
. Короче говоря, Rcpp великолепен, но он не может реально улучшить такие вещи в R. И под «такими вещами» я имею в виду .Primitive
функции, которые выдержали испытание временем и действительно хорошо написаны/векторизированы.
(Мой следующий вопрос к вам: почему вы пытаетесь скомпилировать и улучшить эту функциональность? Существует множество других математических вещей, с которыми R справляется «хорошо» и может быть выполнен (иногда «намного») быстрее в Rcpp, но не ванильная сила/среднее значение.)
Я использую эту функцию для агрегирования групп в большой таблице данных: DT[, lapply(.SD, function(x) {mean(x ^ (1 / power))}), by = group]
Вы действительно начинаете с чего-то вроде самостоятельно вырытой ямы. data.table
довольно лихо оптимизирован, и в целом вы не станете лучше, просто добавив в микс (наивную) функцию Rcpp.
Я бы предложил создать новый конкретный вопрос об операции, которую вы пытаетесь оптимизировать. Если я прав, ваш подход ({mean(x ^ (1 / power))}
) не использует оптимизацию data.table
s для расчета mean
, так что, возможно, есть место для улучшений.
Есть несколько вещей, которые мешают вашей функции С++ с примерами, которые вы привели.
1:100
— это последовательность ALTREP, для которой существует оптимизированный метод суммирования, который работает намного быстрее. В приведенных ниже экстремальных примерах это более чем в 6 миллионов раз быстрее. Очевидно, что вектор не является альтрепом на всем протяжении, но это плохая идея — сравнивать последовательности альтрепов.
billion <- c(1L, 2:1e9)
billalt <- 1:1e9
identical(billion, billalt)
#> [1] TRUE
bench::mark(sum(billion), sum(billalt))
#> # A tibble: 2 x 10
#> expression min mean median max
#> <chr> <bch:tm> <bch:tm> <bch:tm> <bch:tm>
#> 1 sum(billi~ 614564900.000ns 614564900.000ns 614564900.000ns 614564.900us
#> 2 sum(billa~ 100.000ns 312.530ns 200.000ns 23.300us
#> # ... with 5 more variables: `itr/sec` <dbl>, mem_alloc <bch:byt>, n_gc <dbl>,
#> # n_itr <int>, total_time <bch:tm>
Created on 2020-12-11 by the reprex package (v0.3.0)
Во-вторых, 1:100
— это целочисленный вектор, но ваша функция Rcpp принимает числовой вектор, поэтому данные должны быть приведены к типу double
, прежде чем будут выполнены какие-либо операции. Для такого маленького вектора это, вероятно, будет значительной частью накладных расходов.
Ваш тестовый вектор очень мал, поэтому накладные расходы, такие как сохранение Rcpp случайных начальных значений, будут доминировать в различиях.
Ни пункт 1, ни пункт 2 два на самом деле не имеют никакого отношения к разнице между тремя методами OP. Более того, пункт 2 — это предположение, которое вы могли бы проверить, поскольку есть переключатель, чтобы не сохранять значения ГСЧ. Наконец, согласно его последнему комментарию, sum()
на самом деле не является проблемой ОП.
Я новичок в
Rcpp
, поэтому любые общие отзывы также полезны, дажеpower_mean_C
кажется немного медленнее, чем я ожидал