Термин "векторизация" означает разные вещи в разных контекстах?
Исходя из того, что я читал ранее, векторизация является формой распараллеливания, известной как SIMD. Это позволяет процессорам одновременно выполнять одну и ту же инструкцию (например, сложение) в массиве.
Однако, я запутался, читая "Отношения между векторизованным и девекторизованным кодом" относительно производительности векторизации Джулии и Р. В посте утверждается, что де-векторизованный код Julia (с помощью циклов) быстрее, чем векторизованный код в Julia и R, потому что:
Это смущает некоторых людей, которые не знакомы с внутренностями R. Поэтому стоит отметить, как можно улучшить скорость кода R. Процесс повышения производительности довольно прост: один начинается с де-векторизованного R-кода, затем заменяет его векторизованным R-кодом и, наконец, реализует этот векторизованный R-код в де-векторизованном C-коде. Этот последний шаг, к сожалению, невидим для многих пользователей R, которые поэтому думают о векторизации как о механизме повышения производительности. Векторизация сама по себе не помогает сделать код быстрее. Что делает векторизацию в R эффективной, так это то, что она обеспечивает механизм для переноса вычислений в C, где скрытый слой девекторизации может творить чудеса.
В нем утверждается, что R превращает векторизованный код, написанный на R, в де-векторизованный код на C. Если векторизация происходит быстрее (как форма распараллеливания), то почему R де-векторизовывает код и почему это плюс?
2 ответа
"Векторизация" в R - это обработка вектора в представлении интерпретатора R. Взять на себя функцию cumsum
В качестве примера. На входе интерпретатор R видит, что вектор x
передается в эту функцию. Однако работа затем передается на язык C, который интерпретатор R не может анализировать / отслеживать. Пока C делает работу, R просто ждет. К тому времени, когда интерпретатор R возвращается к работе, вектор уже обработан. Таким образом, по мнению R, он выпустил одну инструкцию, но обработал вектор. Это аналогия концепции SIMD - "одна инструкция, несколько данных".
Не только cumsum
функция, которая принимает вектор и возвращает вектор, рассматривается как "векторизация" в R, функции, такие как sum
это берет вектор и возвращает скаляр также "векторизация".
Проще говоря: всякий раз, когда R вызывает некоторый скомпилированный код для цикла, это "векторизация". Если вам интересно, почему этот вид "векторизации" полезен, то это потому, что цикл, написанный на скомпилированном языке, работает быстрее, чем цикл, написанный на интерпретируемом языке. Цикл C переводится на машинный язык, который понимает процессор. Однако, если ЦПУ хочет выполнить цикл R, ему нужна помощь интерпретатора R, чтобы прочитать его, итерация за итерацией. Это похоже на то, что если вы знаете китайский (самый сложный человеческий язык), вы можете быстрее ответить кому-то, кто говорит на китайском; в противном случае вам понадобится переводчик, чтобы сначала переводчик с китайского переводил вам предложение за предложением на английском, затем вы отвечали на английском, и переводчик возвращал его на китайский предложение за предложением. Эффективность общения в значительной степени снижается.
x <- runif(1e+7)
## R loop
system.time({
sumx <- 0
for (x0 in x) sumx <- sumx + x0
sumx
})
# user system elapsed
# 1.388 0.000 1.347
## C loop
system.time(sum(x))
# user system elapsed
# 0.032 0.000 0.030
Имейте в виду, что "векторизация" в R - это всего лишь аналогия с SIMD, но не реальная. Настоящая SIMD использует векторные регистры ЦП для вычислений, следовательно, является настоящим параллельным вычислением через параллелизм данных. R не является языком, на котором вы можете программировать регистры процессора; для этого вам нужно написать скомпилированный код или ассемблерный код.
"Векторизации" R не важно, как в действительности выполняется цикл, написанный на скомпилированном языке; в конце концов, это за пределами знания интерпретатора Р. Относительно того, будет ли этот скомпилированный код выполняться с SIMD, читайте: Использует ли R SIMD при выполнении векторизованных вычислений?
Подробнее о "векторизации" в R
Я не пользователь Юлии, но Богумил Каминьски продемонстрировал впечатляющую особенность этого языка: слияние петель. Юлия может сделать это, потому что, как он указывает, "векторизация в Юлии реализована в Юлии", а не вне языка.
Это показывает обратную сторону векторизации R: скорость часто приходится платить за использование памяти. Я не говорю, что у Джулии не будет этой проблемы (поскольку я не использую ее, я не знаю), но это определенно верно для R.
Вот пример: Самый быстрый способ вычисления линейных точечных произведений между двумя тощими высокими матрицами в R. rowSums(A * B)
является векторизацией в R, так как оба "*"
а также rowSums
закодированы на языке Си как цикл. Тем не менее, R не может объединить их в один цикл C, чтобы избежать создания временной матрицы C = A * B
в оперативную память
Другим примером является правило утилизации R или любые вычисления, основанные на таком правиле. Например, когда вы добавляете скаляр a
в матрицу A
от A + a
что на самом деле происходит, так это a
сначала реплицируется, чтобы быть матрицей B
который имеет такое же измерение с A
т.е. B <- matrix(a, nrow(A), ncol(A))
Затем вычисляется сложение между двумя матрицами: A + B
, Очевидно, генерация временной матрицы B
нежелательно, но извините, вы не можете сделать это лучше, если не напишите свою собственную функцию C для A + a
и назовите его в R. Это описано как "такое слияние возможно только в случае явного осуществления" в ответе Богумила Каминьского.
Чтобы справиться с эффектами памяти многих временных результатов, R имеет сложный механизм, называемый "сборка мусора". Это помогает, но память все еще может взорваться, если вы генерируете действительно большой временный результат где-то в вашем коде. Хорошим примером является функция outer
, Я написал много ответов, используя эту функцию, но она особенно недружелюбна.
Возможно, я был не по теме в этом редактировании, поскольку я начинаю обсуждать побочный эффект "векторизации". Используйте это с осторожностью.
- Помните об использовании памяти; может быть более эффективная векторизация реализации, более эффективная для памяти. Например, как упомянуто в связанном потоке о построчных точечных произведениях между двумя матрицами,
c(crossprod(x, y))
лучше, чемsum(x * y)
, - Будьте готовы использовать пакеты CRAN R с скомпилированным кодом. Если вы обнаружите, что существующие векторизованные функции в R ограничены для выполнения вашей задачи, найдите в CRAN возможные пакеты R, которые могут это сделать. Вы можете задать вопрос с узким местом кодирования в переполнении стека, и кто-то может указать вам правильную функцию в нужном пакете.
- Будьте счастливы написать свой собственный скомпилированный код.
Я думаю, что стоит отметить, что пост, на который вы ссылаетесь, не охватывает всю текущую функциональность векторизации в Julia.
Важно то, что векторизация в Julia реализована в Julia, в отличие от R, где она реализована вне языка. Это подробно объясняется в этом посте: https://julialang.org/blog/2017/01/moredots.
Следствие того, что Юлия может выполнять слияние любой последовательности транслируемых операций в один цикл. В других языках, которые обеспечивают векторизацию, такое объединение возможно, только если оно явно реализовано.
В итоге:
- В Julia вы можете ожидать, что векторизованный код работает так же быстро, как цикл.
- Если вы выполняете последовательность векторизованных операций, то в целом вы можете ожидать, что Джулия будет быстрее, чем R, так как это поможет избежать выделения промежуточных результатов вычислений.
РЕДАКТИРОВАТЬ:
После комментария к 李哲源 приведен пример, показывающий, что Юлия может избежать любых выделений, если вы хотите увеличить все элементы вектора x
от 1
:
julia> using BenchmarkTools
julia> x = rand(10^6);
julia> @benchmark ($x .+= 1)
BenchmarkTools.Trial:
memory estimate: 0 bytes
allocs estimate: 0
--------------
minimum time: 819.230 μs (0.00% GC)
median time: 890.610 μs (0.00% GC)
mean time: 929.659 μs (0.00% GC)
maximum time: 2.802 ms (0.00% GC)
--------------
samples: 5300
evals/sample: 1
В коде .+=
выполняет сложение на месте (добавление $
перед выражением требуется только для бенчмаркинга, в обычном коде это будет x .+= 1
). И мы видим, что не было выделено памяти.
Если мы сравним это с возможной реализацией в R:
> library(microbenchmark)
> x <- runif(10^6)
> microbenchmark(x <- x + 1)
Unit: milliseconds
expr min lq mean median uq max neval
x <- x + 1 2.205764 2.391911 3.999179 2.599051 5.061874 30.91569 100
мы видим, что это не только экономит память, но и ускоряет выполнение кода.