Чому петлі повільні в R?


86

Я знаю, що цикли повільні Rі що я повинен намагатися робити речі у векторизованому порядку.

Але чому? Чому петлі повільні, а applyшвидкі? applyвикликає кілька підфункцій - це не здається швидко.

Оновлення: Вибачте, питання було неправильно поставленим. Я плутав векторизацію з apply. Моє питання мало бути,

"Чому векторизація швидша?"


3
У мене склалося враження, що "застосовувати - це набагато швидше, ніж для циклів" у R - це трохи міф . Нехай system.timeвійни у ​​відповідях розпочнуться ...
Джоран

1
Багато хорошої інформації тут по темі: stackoverflow.com/questions/2275896/…
Чейз

7
Для запису: Застосувати НЕ векторизацію. Apply - це структура циклу з різними (як у: no) побічними ефектами. Див. Дискусію на посилання @Chase.
Джоріс Мейс,

4
Цикли в S ( S-Plus ?) Були традиційно повільними. Це не стосується R ; як таке, ваше запитання насправді не є актуальним. Я не знаю, яка ситуація з S-Plus сьогодні.
Гевін Сімпсон,

4
мені незрозуміло, чому питання було дуже голосоване проти - це питання дуже поширене серед тих, хто приїжджає до Р з інших областей, і його слід додати до часто заданих питань.
patrickmdnet

Відповіді:


69

Цикли в R повільні з тієї самої причини, що будь-яка мова, що інтерпретується, повільна: кожна операція несе в собі багато зайвого багажу.

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

Подумайте, скільки менше трапляється, коли ви викликаєте функцію в C (натискайте аргументи на стек, переходите, вискакуйте аргументи).

Отож саме тому ви отримуєте такі терміни (як зазначив Джоран у коментарі, насправді 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
    }
})

(Цей приклад виявив Редфорд Ніл )

Оскільки (в R є оператором, і насправді потрібно пошук імені кожного разу, коли ви його використовуєте:

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

Або взагалі, інтерпретовані операції (будь-якою мовою) мають більше кроків. Звичайно, ці кроки також дають переваги: ​​ви не змогли б зробити цей (трюк у C.


10
То який сенс у останньому прикладі? Не робіть дурниць у R і чекайте, що це зробить їх швидко?
Чейз

6
@Chase Я думаю, це один із способів сказати це. Так, я мав на увазі, що мова, як C, не матиме різниці в швидкості з вкладеними дужками, але R не оптимізує та не компілює.
Оуен,

1
Також () або {} в тілі циклу - усі ці речі включають пошук імен. Або взагалі, в R, коли ви пишете більше, перекладач робить більше.
Оуен

1
Я не впевнений, який момент ви намагаєтесь зробити з for()циклами? Вони взагалі не роблять одне і те ж. for()Цикл ітерації по кожному елементу Aі їх підсумовування. apply()Виклик проходить весь вектор A[,1](ваш Aмає один стовпець) в vectorised функції mean(). Я не бачу, як це допомагає дискусії і просто плутає ситуацію.
Gavin Simpson

3
@Owen Я погоджуюсь із вашим загальним пунктом, і він є важливим; ми не використовуємо R, оскільки він б’є рекорди швидкості, ми використовуємо його, оскільки він простий у використанні та дуже потужний. Ця сила пов’язана з ціною тлумачення. Просто було незрозуміло, що ви намагалися показати на прикладі for()проти apply(). Я думаю, вам слід видалити цей приклад, оскільки, хоча підсумовування - це велика частина обчислення середнього, все, що ваш приклад насправді показує, - це швидкість векторизованої функції mean(), над С-подібною ітерацією над елементами.
Gavin Simpson

78

Не завжди буває, що цикли є повільними і applyшвидкими. Про це прекрасно обговорюється у випуску R News за травень 2008 року :

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

У розділі "Петлі!" (починаючи зі стор. 48), вони кажуть:

Багато коментарів щодо R стверджують, що використання циклів є особливо поганою ідеєю. Це не обов'язково правда. У деяких випадках важко написати векторизований код, або векторизований код може зайняти величезну кількість пам'яті.

Далі вони пропонують:

  • Ініціалізуйте нові об'єкти на повну довжину перед циклом, а не збільшуйте їх розмір у циклі.
  • Не робіть речей у циклі, які можна робити поза циклом.
  • Не уникайте петель, просто щоб уникнути петель.

Вони мають простий приклад, коли forцикл займає 1,3 секунди, але в нього applyзакінчується пам’ять.


35

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

Чому багато людей думають, що for()цикли є повільними, це тому, що вони, користувач, пишуть поганий код. Загалом (хоча є кілька винятків), якщо вам потрібно розширити / виростити об'єкт, це теж буде включати копіювання, тому у вас є накладні витрати на копіювання та зростання об'єкта. Це обмежується не лише циклами, але якщо ви копіюєте / росте на кожній ітерації циклу, звичайно, цикл буде повільним, тому що у вас є багато операцій копіювання / зростання.

Загальна ідіома використання for()циклів у R полягає у тому, що ви розподіляєте необхідне сховище до початку циклу, а потім заповнюєте виділений таким чином об’єкт. Якщо слідувати цій ідіомі, цикли не будуть повільними. Це те, що apply()вдається вам, але воно просто приховане від очей.

Звичайно, якщо для операції, яку ви виконуєте з for()циклом, існує векторизована функція , не робіть цього . Так само не використовуйте apply()і т.д., якщо існує векторизована функція (наприклад apply(foo, 2, mean), краще виконувати через colMeans(foo)).


9

Просто для порівняння (не читайте занадто багато!): Я провів (дуже) простий цикл for у 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: До речі, у S-Plus знадобилося 1162 мс ...

І той самий код, що і 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;
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.