Почему петли медленны в R?

Я знаю, что циклы медленнее в R, и что я должен попытаться сделать что-то в векторе.

Но почему? Почему медленные циклы и apply быстро? apply вызывает несколько подфункций - это не кажется быстрым.

Обновление: Извините, вопрос был неправильным. Я вводил в заблуждение векторизацию с помощью apply. Мой вопрос должен был быть: "Почему векторизация быстрее?"

Ответ 1

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

Посмотрите R_execClosure в eval.c (это функция, вызываемая для вызова пользовательская функция). Это почти 100 строк в длину и выполняет всевозможные операции - создание среды для выполнения, назначение аргументов в окружающей среды и т.д.

Подумайте, насколько меньше происходит, когда вы вызываете функцию в C (нажмите args on to stack, jump, pop args).

Итак, поэтому вы получаете такие тайминги (как отметил joran в комментарии, это не фактически apply, что быстрая; это внутренняя петля C в mean это быстро. apply является обычным старым R-кодом):

A = matrix(as.numeric(1:100000))

Использование цикла: 0,342 секунды:

system.time({
    Sum = 0
    for (i in seq_along(A)) {
        Sum = Sum + A[[i]]
    }
    Sum
})

Использование суммы: неизмеримо мало:

sum(A)

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

Итак, рассмотрим:

# 0.370 seconds
system.time({
    I = 0
    while (I < 100000) {
        10
        I = I + 1
    }
})

# 0.743 seconds -- double the time just adding parentheses
system.time({
    I = 0
    while (I < 100000) {
        ((((((((((10))))))))))
        I = I + 1
    }
})

(Этот пример был обнаружен Radford Neal)

Потому что ( в R является оператором и на самом деле требует поиска имени каждый раз, когда вы его используете:

> `(` = function(x) 2
> (3)
[1] 2

Или, в общем, интерпретируемые операции (на любом языке) имеют больше шагов. Конечно, эти шаги также дают преимущества: вы не могли бы сделать трюк ( в C.

Ответ 2

Не всегда бывает, что петли медленные, а apply - быстро. Там было приятное обсуждение этого вопроса в мае 2008 года в выпуске R News:

Уве Лиггс и Джон Фокс. R Help Desk: как я могу избежать этого цикла или сделать это быстрее? R News, 8 (1): 46-50, май 2008 г.

В разделе "Циклы!" (начиная с стр. 48), говорят:

Многие комментарии о состоянии R, что использование циклов - особенно плохая идея. Это не обязательно правда. В некоторых случаях сложно написать векторизованный код, или векторизованный код может потреблять огромный объем памяти.

Они также предлагают:

  • Инициализировать новые объекты до полной длины перед циклом, а чем увеличение их размера внутри цикла.
  • Не делай ничего в который может быть выполнен за пределами цикла.
  • Не избегайте циклов просто ради избежания петель.

У них есть простой пример, когда цикл for занимает 1,3 с, но apply исчерпывает память.

Ответ 3

Единственный ответ на поставленный вопрос: петли не замедляются, если вам нужно выполнить перебор по набору данных, выполняющих некоторую функцию, и эта функция или операция не векторизация. Цикл for() будет таким же быстрым, как обычно, как apply(), но, возможно, немного медленнее, чем вызов lapply(). Последняя точка хорошо рассмотрена на SO, например в этом Ответ, и применяется, если код, связанный с настройкой и использованием цикла, является значительной частью общего вычислительного бремя цикла.

Почему многие думают, что циклы for() медленны, потому что они, пользователь, пишут плохой код. В общем случае (хотя есть несколько исключений), если вам нужно расширить/развернуть объект, это также потребует копирования, поэтому у вас есть как накладные расходы на копирование, так и увеличение объекта. Это не просто ограничивается циклами, но если вы копируете/вырастите на каждой итерации цикла, конечно, цикл будет медленным, потому что вы выполняете много операций копирования/выращивания.

Общая идиома для использования циклов for() в R заключается в том, что вы выделяете требуемое хранилище до начала цикла, а затем заполняете выделенный таким образом объект. Если вы будете следовать этой идиоме, петли не будут медленными. Это то, что apply() управляет для вас, но оно просто скрыто от вида.

Конечно, если для операции, которую вы реализуете с циклом for(), существует векторизованная функция, , не делайте этого. Точно так же не использовать apply() и т.д., Если существует вексеризованная функция (например, apply(foo, 2, mean) лучше выполняется через colMeans(foo)).

Ответ 4

Как сравнение (не слишком много читайте в нем!): Я провел (очень) простой цикл в R и JavaScript в Chrome и IE 8. Обратите внимание, что Chrome делает компиляцию для собственного кода, а R с компилятором компилируется в байт-код.

# In R 2.13.1, this took 500 ms
f <- function() { sum<-0.5; for(i in 1:1000000) sum<-sum+i; sum }
system.time( f() )

# And the compiled version took 130 ms
library(compiler)
g <- cmpfun(f)
system.time( g() )

@Gavin Simpson: Кстати, это взяло 1162 мс в S-Plus...

И "тот же" код, что и JavaScript:

// In IE8, this took 282 ms
// In Chrome 14.0, this took 4 ms
function f() {
    var sum = 0.5;
    for(i=1; i<=1000000; ++i) sum = sum + i;
    return sum;
}

var start = new Date().getTime();
f();
time = new Date().getTime() - start;