【问题标题】:Fastest Way to count all-TRUE rows of a LogicalMatrix R / C++ / Rcpp计算LogicalMatrix R / C++ / Rcpp的所有TRUE行的最快方法
【发布时间】:2015-09-27 16:40:10
【问题描述】:

我需要计算一个LogicalMatrix 中所有TRUE 的行数。

因为我需要能够相对固定地执行 1 到 25 亿次,所以速度真的很重要:

我目前最好的:

我想出的最有效/最快的单进程方法是多少 Rcpp 函数 (hm2)。

我有限的分析能力表明,绝大多数时间都花在了if(r_tll == xcolls){... 上。我似乎想不出更快的不同算法(我已经尝试在找到FALSE 后立即跳出循环,但速度要慢得多)。

可以推测的细节:

我可以假设:

  1. 矩阵的行数总是少于 1000 万行。
  2. 来自上游的所有输出矩阵将具有相同数量的列(对于给定的会话/进程/线程)。
  3. 每个矩阵的列数永远不会超过 2326 个。

小例子:

m <- matrix(sample(c(T,F),50000*10, replace = T),ncol = 10L)
head(m)
#>       [,1]  [,2]  [,3]  [,4]  [,5]  [,6]  [,7]  [,8]  [,9] [,10]
#> [1,] FALSE FALSE  TRUE FALSE FALSE  TRUE  TRUE  TRUE  TRUE FALSE
#> [2,] FALSE FALSE FALSE  TRUE FALSE  TRUE FALSE FALSE FALSE  TRUE
#> [3,] FALSE  TRUE FALSE FALSE  TRUE FALSE FALSE FALSE FALSE  TRUE
#> [4,]  TRUE  TRUE FALSE  TRUE  TRUE  TRUE FALSE FALSE FALSE  TRUE
#> [5,]  TRUE FALSE FALSE FALSE  TRUE  TRUE  TRUE FALSE  TRUE  TRUE
#> [6,] FALSE FALSE FALSE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE FALSE
  // [[Rcpp::export]]
int hm(const LogicalMatrix& x){
  const int xrows = x.nrow();
  const int xcols = x.ncol();
  int n_all_true = 0;

  for(size_t row = 0; row < xrows; row++) {
    int r_ttl = 0;
    for(size_t col = 0; col < xcols; col++) {
      r_ttl += x(row,col);
    }
    if(r_ttl == xcols){
      n_all_true++;
    }
  }
  return n_all_true;
}

我不明白为什么,但在我的机器上,如果我烘焙的列数会更快(如果有人能解释为什么会这样,那就太好了):

// [[Rcpp::export]]
int hm2(const LogicalMatrix& x){
  const int xrows = x.nrow();
  // const int xcols = x.ncol();
  int n_all_true = 0;

  for(size_t row = 0; row < xrows; row++) {
    int r_ttl = 0;
    for(size_t col = 0; col < 10; col++) {
      r_ttl += x(row,col);
    }
    if(r_ttl == 10){
      n_all_true += 1;
    }
  }
  return n_all_true;
}

时机:

microbenchmark(hm(m), hm2(m), times = 1000)
#>  Unit: microseconds
#>   expr     min       lq     mean  median       uq      max neval
#>  hm(m) 597.828 599.0995 683.3482 605.397 643.8655 1659.711  1000
#> hm2(m) 236.847 237.6565 267.8787 238.748 253.5280  683.221  1000

【问题讨论】:

  • 你试过多线程吗?
  • @JamesMoore 我可以将数据分派到不同的线程,根据输入数据计算逻辑矩阵,然后计算是的——但这是一个测试用例;需要为每个线程生成 100M 个矩阵)...基本上我们可以提出的问题的大小取决于我可以执行此步骤的效率(生成这样一个矩阵的 alg 大约快 100 倍),所以这是瓶颈.在梦境中,这将比我目前在单线程上的解决方案快 100 倍。感谢您的意见!
  • 关于你为什么这个问题 - “如果我烘烤列数它会更快” - 是真的,很可能与这样一个事实有关,因为列数是一个编译时间常数,编译器可以将其纳入其优化策略。这在第一种情况下是不可能的(hm 函数),其中xcols 必须在运行时确定。
  • 如果您知道行数也是恒定的,那将节省更多时间。

标签: c++ r performance matrix rcpp


【解决方案1】:

使用 OpenMP(我现在看到的问题与要求单线程解决方案的问题相反)仍然可以快 30%,并且代码更改最少,至少在我的 4 核 Xeon 上是这样。我有一种感觉,逻辑 AND 减少可能会做得更好,但会留到另一天:

library(Rcpp)
library(microbenchmark)

m_rows <- 10L
m_cols <- 50000L
rebuild = FALSE

cppFunction('int hm(const LogicalMatrix& x)
{
  const int xrows = x.nrow();
  const int xcols = x.ncol();
  int n_all_true = 0;

  for(size_t row = 0; row < xrows; row++) {
    int r_ttl = 0;
    for(size_t col = 0; col < xcols; col++) {
      r_ttl += x(row,col);
    }
    if(r_ttl == xcols){
      n_all_true++;
    }
  }
  return n_all_true;
}', rebuild = rebuild)

hm3 <- function(m) {
  nc <- ncol(m)
  sum(rowSums(m) == nc)
}

cppFunction('int hm_jmu(const LogicalMatrix& x)
{
  const int xrows = x.nrow();
  const int xcols = x.ncol();
  int n_all_true = 0;

  for(int row = 0; row < xrows; row++) {
    int r_ttl = 0;
    for(int col = 0; col < xcols; col++) {
      r_ttl += x(row,col);
    }
    if(r_ttl == xcols){
      n_all_true++;
    }
  }
  return n_all_true;
}', rebuild = rebuild)

macroExpand <- function(NCOL) {
  paste0('int hm_npjc(const LogicalMatrix& x)
{
  const int xrows = x.nrow();
  int n_all_true = 0;

  for(int row = 0; row < xrows; row++) {
  int r_ttl = 0;
  for(int col = 0; col < ',NCOL,'; col++) {
  r_ttl += x(row,col);
  }
  if(r_ttl == ',NCOL,'){
  n_all_true++;
  }
  }
  return n_all_true;
  }')
}

macroExpand_omp <- function(NCOL) {
  paste0('int hm_npjc_omp(const LogicalMatrix& x)
{
  const int xrows = x.nrow();
  int n_all_true = 0;

  #pragma omp parallel for reduction(+:n_all_true)
  for(int row = 0; row < xrows; row++) {
  int r_ttl = 0;
  for(int col = 0; col < ',NCOL,'; col++) {
  r_ttl += x(row,col);
  }
  if(r_ttl == ',NCOL,'){
  n_all_true++;
  }
  }
  return n_all_true;
  }')
}

cppFunction(macroExpand(m_rows), rebuild = rebuild)
cppFunction(macroExpand_omp(m_rows),  plugins = "openmp", rebuild = rebuild)

cppFunction('int hm_omp(const LogicalMatrix& x) {
const int xrows = x.nrow();
  const int xcols = x.ncol();
  int n_all_true = 0;

  #pragma omp parallel for reduction(+:n_all_true) schedule(static)
  for(size_t row = 0; row < xrows; row++) {
    int r_ttl = 0;
    for(size_t col = 0; col < xcols; col++) {
      r_ttl += x(row,col);
    }
    if(r_ttl == xcols){
      n_all_true++;
    }
  }
  return n_all_true;
}',  plugins = "openmp", rebuild = rebuild)

# using != as inner loop control - no difference, using pre-increment in n_all_true, no diff, static vs dynamic OpenMP, attempted to direct clang and gcc to unroll loops: didn't seem to work

set.seed(21)
m <- matrix(sample(c(TRUE, FALSE), m_cols * m_rows, replace = T), ncol = m_rows)
print(microbenchmark(hm(m), hm3(m), hm_jmu(m), hm_npjc(m),
                     hm_omp(m), hm_npjc_omp(m),
                     times = 1000))

我使用的是 GCC 4.9。 Clang 3.7 类似的结果。 给予: Unit: microseconds expr min lq mean median uq max neval hm(m) 614.074 640.9840 643.24836 641.462 642.9920 976.694 1000 hm3(m) 2705.066 2768.3080 2948.39388 2775.992 2786.8625 43424.060 1000 hm_jmu(m) 591.179 612.3590 625.84484 612.881 613.8825 6874.428 1000 hm_npjc(m) 62.958 63.8965 64.89338 64.346 65.0550 144.487 1000 hm_omp(m) 91.892 92.6050 165.21507 93.758 98.8230 10026.583 1000 hm_npjc_omp(m) 43.129 43.6820 129.15842 44.458 47.0860 17636.875 1000

OpenMP 的魔力只是在编译和链接时包含 -fopenmp(由 Rcpp 处理,plugin="openmp"),并且 #pragma omp parallel for reduction(+:n_all_true) schedule(static) 在这种情况下,外部循环是并行化的,结果是一个和,所以归约语句告诉编译器分解问题,并将每个部分的和归约为一个和。 schedule(static) 描述了编译器和/或运行时将如何在线程之间分配循环。在这种情况下,内环和外环的宽度都是已知的,所以首选static;例如,如果内部循环大小变化很大,那么dynamic 可能会更好地平衡线程之间的工作。

可以明确告诉 OpenMP 您希望每个线程进行多少次循环迭代,但通常最好让编译器决定。

另一方面,我努力使用编译器标志,例如 -funroll-loops 来替换内部循环宽度的丑陋但快速的硬编码(这不是问题的通用解决方案)。我测试了这些都无济于事:见https://github.com/jackwasey/optimization-comparison

【讨论】:

  • 这里的曝光率很高!如果您以如何更改为 openMP 以及它的作用来引导您的答案,那就更棒了! (就像演练只是 hm_omp / hm_npjc_omp
【解决方案2】:

这是你的函数,以及通过cppFunction 编译它的输出:

require(Rcpp)
cppFunction('int hm(const LogicalMatrix& x)
{
  const int xrows = x.nrow();
  const int xcols = x.ncol();
  int n_all_true = 0;

  for(size_t row = 0; row < xrows; row++) {
    int r_ttl = 0;
    for(size_t col = 0; col < xcols; col++) {
      r_ttl += x(row,col);
    }
    if(r_ttl == xcols){
      n_all_true++;
    }
  }
  return n_all_true;
}')
# file.*.cpp: In function ‘int hm(const LogicalMatrix&)’:
# file.*.cpp:12:29: warning: comparison between signed and unsigned integer expressions [-Wsign-compare]
#    for(size_t row = 0; row < xrows; row++) {
#                              ^
# file.*.cpp:14:31: warning: comparison between signed and unsigned integer expressions [-Wsign-compare]
#      for(size_t col = 0; col < xcols; col++) {
#                                ^

注意警告。通过对rowcol 使用int 而不是size_t,我可以获得一些改进。除此之外,我找不到太大的改进空间。

这是我的代码、基准和可重现的示例:

require(Rcpp)
require(microbenchmark)

cppFunction('int hm_jmu(const LogicalMatrix& x)
{
  const int xrows = x.nrow();
  const int xcols = x.ncol();
  int n_all_true = 0;

  for(int row = 0; row < xrows; row++) {
    int r_ttl = 0;
    for(int col = 0; col < xcols; col++) {
      r_ttl += x(row,col);
    }
    if(r_ttl == xcols){
      n_all_true++;
    }
  }
  return n_all_true;
}')

hm3 <- function(m) {
  nc <- ncol(m)
  sum(rowSums(m) == nc)
}

set.seed(21)
m <- matrix(sample(c(T,F),50000*10, replace = T),ncol = 10L)
microbenchmark(hm(m), hm3(m), hm_jmu(m), times=1000)
# Unit: microseconds
#       expr      min        lq   median        uq       max neval
#      hm(m)  578.844  594.1460  607.357  636.4410   858.347  1000
#     hm3(m) 6389.014 6452.9595 6476.197 6735.5465 33720.870  1000
#  hm_jmu(m)  409.920  415.0395  424.401  449.0075   650.127  1000

【讨论】:

  • 很好的发现——我没有收到这些警告(见我的回答);您是否设置了一些标志/另一个版本的 Rcpp?
  • @npjc:我设置了-Wall 标志。
  • @hannahheres:我还发现,一旦有超过一百个左右的列,使用break; 在第一个FALSE 时循环列的算法会更快。
【解决方案3】:

我很好奇为什么“烘焙”被定义为const 会有所作为;所以我玩弄了这个想法。

以前:

library(Rcpp)
library(microbenchmark)
cppFunction('int hm(const LogicalMatrix& x)
            {
            const int xrows = x.nrow();
            const int xcols = x.ncol();
            int n_all_true = 0;

            for(size_t row = 0; row < xrows; row++) {
            int r_ttl = 0;
            for(size_t col = 0; col < xcols; col++) {
            r_ttl += x(row,col);
            }
            if(r_ttl == 10){
            n_all_true++;
            }
            }
            return n_all_true;
            }')

hm3 <- function(m) {
  nc <- ncol(m)
  sum(rowSums(m) == nc)
}

cppFunction('int hm_jmu(const LogicalMatrix& x)
{
  const int xrows = x.nrow();
  const int xcols = x.ncol();
  int n_all_true = 0;

  for(int row = 0; row < xrows; row++) {
  int r_ttl = 0;
  for(int col = 0; col < xcols; col++) {
  r_ttl += x(row,col);
  }
  if(r_ttl == xcols){
  n_all_true++;
  }
  }
  return n_all_true;
  }')

烘烤列数

我只是在这里使用 Joshua 的 sol'n,但会生成量身定制的函数 by code-gen 在我的机器上运行良好。 这对我来说似乎很老套,但我 我想我还是会发帖:

macroExpand <- function(NCOL) {
paste0('int hm_npjc(const LogicalMatrix& x)
{
  const int xrows = x.nrow();
  int n_all_true = 0;

  for(int row = 0; row < xrows; row++) {
  int r_ttl = 0;
  for(int col = 0; col < ',NCOL,'; col++) {
  r_ttl += x(row,col);
  }
  if(r_ttl == ',NCOL,'){
  n_all_true++;
  }
  }
  return n_all_true;
  }')
}

cppFunction(macroExpand(10L))

结果:

set.seed(21)
m <- matrix(sample(c(T,F),50000*10, replace = T),ncol = 10L)
microbenchmark(hm(m), hm3(m), hm_jmu(m), hm_npjc(m), times=1000)
#> Unit: microseconds
#>        expr      min        lq      mean    median        uq       max
#>       hm(m)  596.808  600.1870  722.5140  629.1750  709.3875  1680.379
#>      hm3(m) 2189.164 2353.6700 2972.1463 2509.4630 2956.7675 49930.471
#>   hm_jmu(m)  574.137  576.5160  678.6475  600.4775  665.2800  2240.988
#>  hm_npjc(m)   81.978   83.1855  102.7646   89.2160  101.0400   380.884
#>  neval
#>   1000
#>   1000
#>   1000
#>   1000

我想指出,我真的不明白为什么编译器不在这里优化到相同的解决方案;如果有人对此有见解,那就太棒了。

出处

devtools::session_info()
#> Session info --------------------------------------------------------------
#>  setting  value                       
#>  version  R version 3.2.2 (2015-08-14)
#>  system   x86_64, darwin13.4.0        
#>  ui       RStudio (0.99.691)          
#>  language (EN)                        
#>  collate  en_CA.UTF-8                 
#>  tz       America/Los_Angeles         
#>  date     2015-09-27
#> Packages ------------------------------------------------------------------
#>  package        * version    date       source                         
#>  clipr            0.1.1      2015-09-04 CRAN (R 3.2.0)                 
#>  colorspace       1.2-6      2015-03-11 CRAN (R 3.2.0)                 
#>  devtools         1.9.1      2015-09-11 CRAN (R 3.2.0)                 
#>  digest           0.6.8      2014-12-31 CRAN (R 3.2.0)                 
#>  evaluate         0.8        2015-09-18 CRAN (R 3.2.0)                 
#>  formatR          1.2.1      2015-09-18 CRAN (R 3.2.0)                 
#>  ggplot2          1.0.1      2015-03-17 CRAN (R 3.2.0)                 
#>  gtable           0.1.2      2012-12-05 CRAN (R 3.2.0)                 
#>  htmltools        0.2.6      2014-09-08 CRAN (R 3.2.0)                 
#>  knitr            1.10.5     2015-05-06 CRAN (R 3.2.0)                 
#>  magrittr         1.5        2014-11-22 CRAN (R 3.2.0)                 
#>  MASS             7.3-43     2015-07-16 CRAN (R 3.2.2)                 
#>  memoise          0.2.1      2014-04-22 CRAN (R 3.2.0)                 
#>  microbenchmark * 1.4-2      2014-09-28 CRAN (R 3.2.0)                 
#>  munsell          0.4.2      2013-07-11 CRAN (R 3.2.0)                 
#>  plyr             1.8.3      2015-06-12 CRAN (R 3.2.0)                 
#>  proto            0.3-10     2012-12-22 CRAN (R 3.2.0)                 
#>  Rcpp           * 0.12.1     2015-09-10 CRAN (R 3.2.0)                 
#>  reprex           0.0.0.9001 2015-09-26 Github (jennybc/reprex@1d6584a)
#>  reshape2         1.4.1      2014-12-06 CRAN (R 3.2.0)                 
#>  rmarkdown        0.7        2015-06-13 CRAN (R 3.2.0)                 
#>  rstudioapi       0.3.1      2015-04-07 CRAN (R 3.2.0)                 
#>  scales           0.3.0      2015-08-25 CRAN (R 3.2.0)                 
#>  stringi          0.5-5      2015-06-29 CRAN (R 3.2.0)                 
#>  stringr          1.0.0      2015-04-30 CRAN (R 3.2.0)

【讨论】:

    【解决方案4】:

    如何利用TRUE 对许多数字运算符强制转换为1 的事实,然后将其全部向量化为已在 C 中编程的函数,例如

    set.seed(100)
    m <- matrix(sample(c(TRUE, FALSE), 50000*10, replace = TRUE), ncol = 10L)
    sum(rowSums(m) == ncol(m))
    ## [1] 47
    
    microbenchmark::microbenchmark(sum(rowSums(m) == ncol(m)))
    ## Unit: milliseconds
    ##                       expr      min       lq     mean   median       uq     max neval
    ## sum(rowSums(m) == ncol(m)) 1.715399 1.840763 1.873422 1.861552 1.905841 2.02524   100
    

    参见R Inferno 第 3 章。

    直接比较的编辑答案:

    (这里我将两个 C++ 函数粘贴到桌面上名为 test.cpp 的文件中,并带有通常的 Rcpp 标头信息)

    require(Rcpp)
    sourceCpp("~/Desktop/test.cpp")
    
    set.seed(100)
    m <- matrix(sample(c(TRUE, FALSE), 50000*10, replace = TRUE), ncol = 10L)
    
    hm3 <- function(m) {
        nc <- ncol(m)
        sum(rowSums(m) == nc)
    }
    
    microbenchmark::microbenchmark(hm(m), hm2(m), hm3(m), times = 1000)
    ## Unit: milliseconds
    ##   expr      min       lq     mean   median       uq        max neval
    ##  hm(m) 4.996005 5.036732 5.169672 5.089707 5.194580   9.961581  1000
    ## hm2(m) 5.031222 5.074990 5.228239 5.128106 5.242909  10.109776  1000
    ## hm3(m) 1.626933 1.878014 2.205195 1.922608 2.014012 226.894190  1000
    

    我在此注意到,对 R Inferno 的引用并不真正合适,因为它不适用于 C++,但它仍然是一个生活的口头禅。 :-)

    【讨论】:

    • 你能对我的解决方案进行基准测试吗...在我的机器上这要慢得多。
    • 这比 OP 的两个解决方案慢 5-10 倍。而使用.rowSums 只比rowSums 好一点。
    • 在我的系统上更快,请参阅我现在添加的直接比较。使用快速的 iMac 和 OS X。
    • 嗯,这很奇怪 - 在我的系统(也是 iMac)上,hm3 也比 hm2 慢约 10 倍。
    • microbenchmark 中的最大时间肯定更高,但times = 1000 的中值时间将非常具有代表性。奇怪的是你的结果不同 - 我无法解释那个。
    【解决方案5】:

    偶然发现这个工作在一个类似的问题上。我们可以通过在第一列初始化r_ttl 并消除if(r_ttl == xcols) 检查来提高性能:

    # initialize on the first column
    cppFunction('int hm2(const LogicalMatrix& x)
    {
      const int xrows = x.nrow();
      const int xcols = x.ncol();
      int n_all_true = 0;
    
      for(int row = 0; row < xrows; row++) {
        int r_ttl = x(row,0);
        for(int col = 1; col < xcols; col++) {
          r_ttl += x(row,col);
        }
        if(r_ttl == xcols){
          n_all_true++;
        }
      }
      return n_all_true;
    }')
    
    # use *= to eliminate the if statement
    cppFunction('int hm3(const LogicalMatrix& x)
    {
      const int xrows = x.nrow();
      const int xcols = x.ncol();
      int n_all_true = 0;
    
      for(int row = 0; row < xrows; row++) {
        int r_ttl = 1;
        for(int col = 0; col < xcols; col++) {
          r_ttl *= x(row,col);
        }
        n_all_true += r_ttl;
      }
      return n_all_true;
    }')
    
    # both modifications
    cppFunction('int hm4(const LogicalMatrix& x) {
      const int xrows = x.nrow();
      const int xcols = x.ncol();
      int n_all_true = 0;
    
      for(int row = 0; row < xrows; row++) {
        int r_ttl = x(row,0);
        for(int col = 1; col < xcols; col++) {
          r_ttl *= x(row,col);
        }
        n_all_true += r_ttl;
      }
      return n_all_true;
    }')
    
    m <- matrix(sample(c(T,F),50000*10, replace = T),ncol = 10L)
    
    microbenchmark::microbenchmark(hm_jmu = hm_jmu(m),
                                   hm2 = hm2(m),
                                   hm3 = hm3(m),
                                   hm4 = hm4(m),
                                   check = "equal",
                                   times = 1e4)
    
    # Unit: microseconds
    #   expr   min    lq     mean median    uq    max neval
    # hm_jmu 198.3 200.8 218.5362  208.3 212.7 5855.9 10000
    #    hm2 169.3 170.9 184.8722  171.4 180.0 5775.4 10000
    #    hm3 192.7 196.1 209.7465  196.8 206.3 1056.2 10000
    #    hm4 161.9 163.1 176.2370  163.5 171.9 1119.2 10000
    

    比 Joshua Ulrich 的更大改进提高了大约 20%。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-10-24
      • 1970-01-01
      • 1970-01-01
      • 2011-06-22
      • 2010-09-07
      • 1970-01-01
      • 2019-06-25
      • 2013-04-17
      相关资源
      最近更新 更多