Почему функция range () медленнее, чем комбинация min и max?

#r #performance #range #max #min

#r #Производительность #диапазон #макс #min

Вопрос:

Я наткнулся на range функцию R. Это, безусловно, полезный инструмент и делает код более читаемым, но его скорость можно удвоить, заменив его простым однострочным включением min и max .

Я провел несколько тестов, и «плохая» производительность функции range меня удивила. Для сравнения я написал функцию с именем range2 , которая использует min и max (см. Код). Кроме скорости, есть ли какие-либо причины, по которым эта функция существует, если ее можно превзойти простым однострочным текстом, который также легко читается?

 require(microbenchmark)

range2 <- function(x) c(min(x),max(x))  

n <- 1000000
x <- rnorm(n)
microbenchmark(range(x), range2(x))
#Unit: milliseconds
#  expr      min       lq     mean   median       uq     max neval cld
# range(x) 4.696101 4.734751 5.321603 4.796301 4.814751 23.0646   100   b
#range2(x) 2.477602 2.516101 2.542540 2.535051 2.544052  3.7636   100  a 

n <- 10000000
x <- rnorm(n)
microbenchmark(range(x), range2(x))
# Unit: milliseconds
#  expr     min      lq     mean   median       uq      max neval cld
# range(x) 47.3246 47.9498 58.27992 55.25795 61.98205 146.5100   100   b
#range2(x) 24.7063 25.5021 25.59192 25.55245 25.63515  27.1088   100  a
  

Конечно, это было бы не первое узкое место, от которого хотелось бы избавиться, поскольку мы говорим о миллисекундах в векторе с 10 000 000 записей, но я ожидал, что range это будет быстрее. Моя простая интуиция была:

range просматривает данные один раз и ищет минимум и максимум одновременно, тогда как моя range2 функция просматривает данные два раза: один раз, чтобы найти минимум, и один раз, чтобы найти максимум.

Может быть, кто-нибудь может рассказать о реализации. Может быть, причина в том, что min и max реализованы на C, а range нет?

Добавление: Я уже говорил об этом со своим другом, и он просто ускорил эту функцию, реализовав ее на C через:

 #include <Rcpp.h>
#include <float.h>
using namespace Rcpp;
// [[Rcpp::export]]
NumericVector range3(NumericVector x) {
  int xs = x.size();
  double minValue = FLT_MAX;
  double maxValue = FLT_MIN;
  for (int i =0; i < xs; i  ) {
    if (x[i] < minValue) minValue = x[i];
    if (x[i] > maxValue) maxValue = x[i];
  }
  Rcpp::NumericVector result(2);
  result[0] = minValue;
  result[1] = maxValue;
  return resu<
}
  

и это дает следующие тесты:

 n <- 10000000
x <- rnorm(n)
microbenchmark(range(x), range2(x) ,range3(x))
#Unit: milliseconds
#      expr     min       lq     mean  median       uq      max neval cld
#  range(x) 47.8583 48.30355 58.12575 55.3135 62.10295 149.9648   100   c
# range2(x) 24.8211 25.53615 25.90920 25.6176 25.79175  42.4659   100  b 
# range3(x) 13.2458 13.30385 13.47175 13.3797 13.65410  14.3487   100 a
  

Комментарии:

1. Из range.default видно, что она выполняет несколько дополнительных проверок перед вызовом c(min(x), max(x)) самой себя. Она не оптимизирована по скорости. Это просто удобная функция. Кажется маловероятным, что эти миллисекундные различия могут быть источником узкого места в производительности.

2. Привет, вот как вы просматриваете исходный код range.default : getAnywhere(range.default)

3. Определение функции также должно отображаться, если вы просто запустите строку range.default

4. Спасибо за ответы. Это просто хороший пример того, что некоторые знания о данных, с которыми вы работаете, полезны, поскольку вы можете избежать проверок, которые не нужны.

5. У меня нет представления о R, но есть предположение: Внутренние x[i]<minValue и x[i]>maxValue проверки выдадут результат true только для первых нескольких элементов данных. После этого будут выдаваться только «выбросы» true . Итак, мне было бы интересно увидеть сравнение этих функций на основе данных, которые являются 1. постоянными, 2. возрастающими, 3. убывающими (да, мы можем услышать слова «ошибка прогнозирования ветвления», пробирающиеся здесь …)

Ответ №1:

Вот исходный код для range.default (запустите R 3.6.1)

  > range.default
function (..., na.rm = FALSE, finite = FALSE) 
{
    x <- c(..., recursive = TRUE)
    if (is.numeric(x)) {
        if (finite) 
            x <- x[is.finite(x)]
        else if (na.rm) 
            x <- x[!is.na(x)]
        c(min(x), max(x))
    }
    else {
        if (finite) 
            na.rm <- TRUE
        c(min(x, na.rm = na.rm), max(x, na.rm = na.rm))
    }
}
  

Вы можете видеть, что она выполняет несколько дополнительных проверок перед c(min(x), max(x)) вызовом самой себя. Она не оптимизирована по скорости. Это просто удобная функция. Кажется маловероятным, что эти миллисекундные различия могут быть источником узкого места в производительности.