Падение производительности при включении кода, экспортированного из другого пакета Rcpp - PullRequest
0 голосов
/ 15 сентября 2018

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

У меня есть package1 , созданный через RcppArmadillo::RcppArmadillo.package.skeleton(). Единственный исходный файл пакета - package1 / src / shared.cpp , который включает функцию, которая вычисляет суммы столбцов матрицы, используя RcppArmadillo. Исходный код для shared.cpp выглядит следующим образом:

//[[Rcpp::depends(RcppArmadillo)]]
//[[Rcpp::interfaces(r, cpp)]]

#include "RcppArmadillo.h"

// [[Rcpp::export]]
arma::vec col_sums(const arma::mat& matty){
  return arma::sum(matty, 0).t();
}

Теперь предположим, что я хочу перезапустить эту функцию в другом пакете, который называется package2 . Я делаю это, редактируя Imports и LinkingTo в ОПИСАНИИ, добавляя package1 . Тогда единственным исходным файлом для этого нового пакета является package2 / src / testimport.cpp

//[[Rcpp::depends(RcppArmadillo, package1)]]

#include "RcppArmadillo.h"
#include "package1.h"

//[[Rcpp::export]]
arma::vec col_sums(const arma::mat& test){
  return arma::sum(test,0).t();
}

//[[Rcpp::export]]
arma::vec col_sums_imported(const arma::mat& test){
  return package1::col_sums(test);
}

Теперь, если я скомпилирую оба пакета и протестирую функции 3 + 1, я получу

library(magrittr)
library(rbenchmark)

nr <- 100
p <- 800

testmat <- rnorm(nr * p) %>% matrix(ncol=p)

benchmark(package2::col_sums(testmat),
          package2::col_sums_imported(testmat), 
          colSums(testmat),
          package1::col_sums(testmat),
          replications=1000)

Я ожидаю, что между package1::col_sums и package2::col_sums вообще не будет никакой разницы, но также будет минимальная или небольшая разница между этими двумя и package2::col_sums_imported, которая вызывает package1::col_sums из package2 с использованием интерфейса cpp .

Вместо этого я получаю (я также добавил colSums R для сравнения)

                                  test replications elapsed relative user.self sys.self user.child sys.child
3                     colSums(testmat)         1000   0.050    1.429     0.052    0.000          0         0
4          package1::col_sums(testmat)         1000   0.035    1.000     0.036    0.000          0         0
1        package2::col_sums(testmat)         1000   0.038    1.086     0.036    0.000          0         0
2 package2::col_sums_imported(testmat)         1000   0.214    6.114     0.100    0.108          0         0

Это 6x замедление озадачивает меня, потому что я не ожидал, что будет такая разница. Желательно ли просто скопировать источник «общих» функций в новый пакет и почему? Я чувствую, что наличие только одного источника для col_sums позволило бы мне легче распространять изменения в обоих пакетах. Или есть другая причина, почему мой код так сильно тормозит?

РЕДАКТИРОВАТЬ: в дополнение к ответу @ duckmayr, приведенному ниже, я обновил пример своего минимального пакета github, чтобы показать, как пользовательскую функцию можно использовать в package1, экспортировать в другие пакеты, импортировать в package2. Код можно найти на https://github.com/mkln/rcppeztest

Ответы [ 2 ]

0 голосов
/ 15 сентября 2018

Как уже упоминалось другими, разрешение другим пакетам вызывать ваш код C ++ из C ++ требует использования заголовочных файлов в inst/include/. Rcpp::interfaces позволяет автоматизировать создание таких файлов. Однако, как я покажу ниже, создание собственных заголовков может привести к ускорению выполнения. Я полагаю, это потому, что использование Rcpp::interfaces для создания заголовков может привести к более сложному коду заголовка.

Прежде чем я продолжу и продемонстрирую «более простой» подход, который приводит к более быстрому времени выполнения, я должен отметить, что хотя это работает для меня (и я использовал подход, который я продемонстрирую ниже несколько раз без проблем), тем больше «комплексный» подход, принятый Rcpp::interfaces, частично используется для сопоставления с утверждениями в Раздел 5.4.3. Руководства по написанию расширений R . (В частности, биты, имеющие отношение к R_GetCCallable, вы увидите ниже). Итак, улучшите время выполнения с помощью кода, который я предлагаю ниже, на свой страх и риск. 1,2

Простой заголовок для совместного использования кода для col_sums может выглядеть следующим образом:

#ifndef RCPP_package3
#define RCPP_package3

#include <RcppArmadillo.h>

namespace package3 {
    inline arma::vec col_sums(const arma::mat& test){
      return arma::sum(test,0).t();
    }
}

#endif

Однако заголовок, созданный Rcpp::interfaces, выглядит следующим образом:

// Generated by using Rcpp::compileAttributes() -> do not edit by hand
// Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393

#ifndef RCPP_package1_RCPPEXPORTS_H_GEN_
#define RCPP_package1_RCPPEXPORTS_H_GEN_

#include <RcppArmadillo.h>
#include <Rcpp.h>

namespace package1 {

    using namespace Rcpp;

    namespace {
        void validateSignature(const char* sig) {
            Rcpp::Function require = Rcpp::Environment::base_env()["require"];
            require("package1", Rcpp::Named("quietly") = true);
            typedef int(*Ptr_validate)(const char*);
            static Ptr_validate p_validate = (Ptr_validate)
                R_GetCCallable("package1", "_package1_RcppExport_validate");
            if (!p_validate(sig)) {
                throw Rcpp::function_not_exported(
                    "C++ function with signature '" + std::string(sig) + "' not found in package1");
            }
        }
    }

    inline arma::vec col_sums(const arma::mat& matty) {
        typedef SEXP(*Ptr_col_sums)(SEXP);
        static Ptr_col_sums p_col_sums = NULL;
        if (p_col_sums == NULL) {
            validateSignature("arma::vec(*col_sums)(const arma::mat&)");
            p_col_sums = (Ptr_col_sums)R_GetCCallable("package1", "_package1_col_sums");
        }
        RObject rcpp_result_gen;
        {
            RNGScope RCPP_rngScope_gen;
            rcpp_result_gen = p_col_sums(Shield<SEXP>(Rcpp::wrap(matty)));
        }
        if (rcpp_result_gen.inherits("interrupted-error"))
            throw Rcpp::internal::InterruptedException();
        if (Rcpp::internal::isLongjumpSentinel(rcpp_result_gen))
            throw Rcpp::LongjumpException(rcpp_result_gen);
        if (rcpp_result_gen.inherits("try-error"))
            throw Rcpp::exception(Rcpp::as<std::string>(rcpp_result_gen).c_str());
        return Rcpp::as<arma::vec >(rcpp_result_gen);
    }

}

#endif // RCPP_package1_RCPPEXPORTS_H_GEN_

Итак, я создал два дополнительных пакета через

library(RcppArmadillo)
RcppArmadillo.package.skeleton(name = "package3", example_code = FALSE)
RcppArmadillo.package.skeleton(name = "package4", example_code = FALSE)

Затем в package3/inst/include я добавил package3.h, содержащий приведенный выше код «простого заголовка» (я также добавил одноразовый cpp-файл «Hello World» в src/). В package4/src/ я добавил следующее:

#include <package3.h>

// [[Rcpp::export]]
arma::vec col_sums(const arma::mat& test){
  return arma::sum(test,0).t();
}

// [[Rcpp::export]]
arma::vec simple_header_import(const arma::mat& test){
  return package3::col_sums(test);
}

, а также добавление package3 к LinkingTo в файле DESCRIPTION.

Затем, после установки новых пакетов, я сравнил все функции друг с другом:

library(rbenchmark)

set.seed(1)
nr <- 100
p <- 800
testmat <- matrix(rnorm(nr * p), ncol = p)

benchmark(original = package1::col_sums(testmat),
          first_copy = package2::col_sums(testmat),
          complicated_import = package2::col_sums_imported(testmat),
          second_copy = package4::col_sums(testmat),
          simple_import = package4::simple_header_import(testmat),
          replications = 1e3,
          columns = c("test", "relative", "elapsed", "user.self", "sys.self"),
          order = "relative")


                test relative elapsed user.self sys.self
2         first_copy    1.000   0.174     0.174    0.000
4        second_copy    1.000   0.174     0.173    0.000
5      simple_import    1.000   0.174     0.174    0.000
1           original    1.126   0.196     0.197    0.000
3 complicated_import    6.690   1.164     0.544    0.613

В то время как более «сложная» функция заголовка была в 6 раз медленнее, «более простая» - нет.


1. Тем не менее, автоматический код, сгенерированный Rcpp::interfaces, на самом деле включает некоторые функции, которые могут оказаться для вас излишними, помимо проблемы R_GetCCallable, хотя они могут быть желательны и в некоторых других контекстах необходимы.

2. Регистрация функций всегда переносима, и авторы пакетов получают инструкции об этом в руководстве Writing R Extensions, но для внутреннего / организационного / и т. Д. Использование Я считаю, что подход, представленный здесь, должен работать, если все задействованные пакеты собраны из исходного кода. См. этот раздел R пакетов Хэдли Уикхема для некоторого обсуждения, а также раздел руководства по написанию расширений R, связанный выше.

0 голосов
/ 15 сентября 2018

Мне на ум приходят три вещи:

  1. Проводит ли rbenchmark цикл "разогрева"?Если нет, то первый вызов package1::col_sums платит цену , вызывая функцию R .Это может составлять 0,1 с системного времени.

  2. Функция возвращает объект Armadillo.Но при вызове через R это должно быть преобразовано в объект R и обратно .Я не уверен, насколько легки эти преобразования, или если копии данных сделаны в (некоторых) случаях.

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

В целом, если вы хотите поделиться такими кратковременными функциями, вы должны преобразовать их в «только заголовок» и поместить их вinst/include/, как предложено F.Privé в комментариях.Тем не менее, таким образом вы будете делиться только исходным кодом, но не объектным кодом, т.е. package2 придется перекомпилировать при изменении функции в package1.

Мне было любопытно посмотреть, насколько эффективно вызывать функцию, экспортируемую через R. Поэтому я добавил в тестовый пакет простую тестовую функцию:

//[[Rcpp::interfaces(r, cpp)]]
#include <thread>
#include <chrono>

#include <Rcpp.h>

// [[Rcpp::export]]
int mysleep(int msec) {
  std::this_thread::sleep_for (std::chrono::microseconds(msec));
  return msec;
}

Затем я сравнил вызов этой функции.прямо или косвенно как экспортируемая функция для времен сна 50, 500 и 5000 мкс.Среднее время выполнения, о котором сообщает bench::mark:

            50µs  500µs     5ms   mem_alloc
direct     153µs  688µs  5.37ms      2.47KB 
indirect   163µs  705µs  5.39ms      4.95KB

Для меня это выглядит так, как будто вызов такой простой функции косвенно добавляет всего лишь несколько 10 мкс служебных данных на эту довольно медленную машину.Однако мы уже видим, что объем выделяемой памяти удваивается.Если мы посмотрим на вашу функцию, которая возвращает более сложную структуру, мы получим:

  expression   min  mean median      max `itr/sec` mem_alloc  n_gc n_itr
  <chr>      <bch> <bch> <bch:> <bch:tm>     <dbl> <bch:byt> <dbl> <int>
1 direct     141µs 148µs  145µs 830.14µs     6737.    10.4KB     0  3342
2 imported   344µs 703µs  832µs   1.17ms     1423.   644.2KB     7   628 

Объем памяти, выделенный при косвенном вызове, более чем в 60 раз больше!Для меня это объясняет падение производительности.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...