Почему моя средняя функция Rcpp медленнее, чем R?

Я хочу создать функцию C++, которая повышает каждый элемент в x до power и принимает среднее значение. Я создал три версии:

  • power_mean_R: R решение -- mean(x^power)
  • power_mean_C: C++ решение
  • power_mean_C_2arg: 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…

Я новичок в Rcpp, поэтому любые общие отзывы также полезны, даже power_mean_C кажется немного медленнее, чем я ожидал

Jeff Bezos 10.12.2020 22:25

1. Функция R уже настолько эффективна, насколько это возможно, и уже кричит вам о векторизации. 2. Вы можете немного выиграть с меньшим количеством проверок безопасности, но это не рекомендуется. 3. Вы можете выиграть больше через OpenMP, но это требует больше работы. Короче говоря, это ориентир для легкой победы.

Dirk Eddelbuettel 10.12.2020 22:29

pow(x[i], 2) скорее всего медленнее, чем x[i]*x[i]

Öö Tiib 10.12.2020 22:31

Также обратите внимание, что ^ и mean фактически являются просто обертками вокруг функций .Primitive и .Internal, которые являются функциями C/C++.

DanY 10.12.2020 22:33

Даниэль, я обсуждал («обсудил»?) эту подобную тему с Дирком (много лет назад), и он «победил» и с тех пор неоднократно подтверждал свою правоту. Хотя я понял поговорку «Преждевременная оптимизация — корень всех зол», я не особо применял ее на практике (ну), думая, что смогу все исправить, перейдя сразу к Rcpp. Короче говоря, Rcpp великолепен, но он не может реально улучшить такие вещи в R. И под «такими вещами» я имею в виду .Primitive функции, которые выдержали испытание временем и действительно хорошо написаны/векторизированы.

r2evans 10.12.2020 22:39

(Мой следующий вопрос к вам: почему вы пытаетесь скомпилировать и улучшить эту функциональность? Существует множество других математических вещей, с которыми R справляется «хорошо» и может быть выполнен (иногда «намного») быстрее в Rcpp, но не ванильная сила/среднее значение.)

r2evans 10.12.2020 22:41

Я использую эту функцию для агрегирования групп в большой таблице данных: DT[, lapply(.SD, function(x) {mean(x ^ (1 / power))}), by = group]

Jeff Bezos 10.12.2020 22:44

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

Dirk Eddelbuettel 11.12.2020 05:14

Я бы предложил создать новый конкретный вопрос об операции, которую вы пытаетесь оптимизировать. Если я прав, ваш подход ({mean(x ^ (1 / power))}) не использует оптимизацию data.tables для расчета mean, так что, возможно, есть место для улучшений.

minem 11.12.2020 09:37
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
9
217
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Есть несколько вещей, которые мешают вашей функции С++ с примерами, которые вы привели.

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() на самом деле не является проблемой ОП.

Dirk Eddelbuettel 11.12.2020 05:12

Другие вопросы по теме