Почему «векторизация» этого простого цикла R дает другой результат? - PullRequest
0 голосов
/ 01 октября 2018

Возможно, очень глупый вопрос.

Я пытаюсь "векторизовать" следующий цикл:

set.seed(0)
x <- round(runif(10), 2)
# [1] 0.90 0.27 0.37 0.57 0.91 0.20 0.90 0.94 0.66 0.63
sig <- sample.int(10)
# [1]  1  2  9  5  3  4  8  6  7 10
for (i in seq_along(sig)) x[i] <- x[sig[i]]
x
# [1] 0.90 0.27 0.66 0.91 0.66 0.91 0.94 0.91 0.94 0.63

Я думаю, что это просто x[sig], но результат не совпадает.

set.seed(0)
x <- round(runif(10), 2)
x[] <- x[sig]
x
# [1] 0.90 0.27 0.66 0.91 0.37 0.57 0.94 0.20 0.90 0.63

Что не так?


Замечание

Очевидно, из вывода мы видим, что цикл for и x[sig]разные.Смысл последнего ясен: перестановка, поэтому многие люди склонны считать, что цикл просто делает что-то не так.Но никогда не будь так уверен;это может быть какой-то четко определенный динамический процесс.Целью этих вопросов и ответов является не оценка правильности, а объяснение того, почему они не эквивалентны.Надеемся, что это послужит хорошим примером для понимания «векторизации».

Ответы [ 4 ]

0 голосов
/ 02 октября 2018

Интересно видеть, что хотя R «векторизация» отличается от «SIMD» (как хорошо объяснил OP), та же логика применяется при определении, является ли цикл «векторизуемым».Вот демонстрация с использованием примеров в самоответе OP (с небольшой модификацией).

Пример 1 с зависимостью «запись после прочтения» «vectorizable».

// "ex1.c"
#include <stdlib.h>
void ex1 (size_t n, size_t *x) {
  for (size_t i = 1; i < n; i++) x[i - 1] = x[i] + 1;
}

gcc -O2 -c -ftree-vectorize -fopt-info-vec ex1.c
#ex1.c:3:3: note: loop vectorized

Пример 2 с зависимостью «чтение после записи»: не «векторизация».

// "ex2.c"
#include <stdlib.h>
void ex2 (size_t n, size_t *x) {
  for (size_t i = 1; i < n; i++) x[i] = x[i - 1] + 1;
}

gcc -O2 -c -ftree-vectorize -fopt-info-vec-missed ex2.c
#ex2.c:3:3: note: not vectorized, possible dependence between data-refs
#ex2.c:3:3: note: bad data dependence

Использование ключевого слова C99 restrict дляКомпилятор подсказок без псевдонимов блоков памяти между тремя массивами.

// "ex3.c"
#include <stdlib.h>
void ex3 (size_t n, size_t * restrict a, size_t * restrict b, size_t * restrict c) {
  for (size_t i = 0; i < n; i++) a[i] = b[i] + c[i];
}

gcc -O2 -c -ftree-vectorize -fopt-info-vec ex3.c
#ex3.c:3:3: note: loop vectorized
0 голосов
/ 01 октября 2018

Существует более простое объяснение.В вашем цикле вы перезаписываете один элемент x на каждом шаге, заменяя его прежнее значение одним из других элементов x.Таким образом, вы получаете то, что просили.По сути, это сложная форма выборки с заменой (sample(x, replace=TRUE)) - нужно ли вам такое осложнение, зависит от того, чего вы хотите достичь.

С вашим векторизованным кодом вы просто запрашиваете определенную перестановку x (без замены), и это то, что вы получаете.Векторизованный код не делает то же самое, что и ваш цикл.Если вы хотите добиться того же результата с помощью цикла, вам сначала нужно сделать копию x:

set.seed(0)
x <- x2 <- round(runif(10), 2)
# [1] 0.90 0.27 0.37 0.57 0.91 0.20 0.90 0.94 0.66 0.63
sig <- sample.int(10)
# [1]  1  2  9  5  3  4  8  6  7 10
for (i in seq_along(sig)) x2[i] <- x[sig[i]]
identical(x2, x[sig])
#TRUE

Здесь нет опасности наложения псевдонимов: x и x2 изначально относятся кта же ячейка памяти, но его изменится, как только вы измените первый элемент x2.

0 голосов
/ 02 октября 2018

Это не имеет ничего общего с псевдонимами блоков памяти (термин, с которым я никогда раньше не сталкивался).Возьмите конкретный пример перестановки и пройдитесь по назначениям, которые произойдут независимо от реализации на уровне языка C или ассемблера (или любого другого);Это присуще тому, как будет вести себя любой последовательный цикл for по сравнению с тем, как будет происходить любая «истинная» перестановка (то, что получается с x[sig]):

sample(10)
 [1]  3  7  1  5  6  9 10  8  4  2

value at 1 goes to 3, and now there are two of those values
value at 2 goes to 7, and now there are two of those values
value at 3 (which was at 1) now goes back to 1 but the values remain unchanged

... может продолжаться, но это иллюстрирует, как это будетобычно не является «истинной» перестановкой и очень редко приводит к полному перераспределению значений.Я предполагаю, что только полностью упорядоченная перестановка (из которой я думаю, что есть только одна, то есть 10:1) может привести к новому набору х, которые являются уникальными.

replicate( 100, {x <- round(runif(10), 2); 
                  sig <- sample.int(10); 
                  for (i in seq_along(sig)){ x[i] <- x[sig[i]]}; 
                  sum(duplicated(x)) } )
 #[1] 4 4 4 5 5 5 4 5 6 5 5 5 4 5 5 6 3 4 2 5 4 4 4 4 3 5 3 5 4 5 5 5 5 5 5 5 4 5 5 5 5 4
 #[43] 5 3 4 6 6 6 3 4 5 3 5 4 6 4 5 5 6 4 4 4 5 3 4 3 4 4 3 6 4 7 6 5 6 6 5 4 7 5 6 3 6 4
 #[85] 8 4 5 5 4 5 5 5 4 5 5 4 4 5 4 5

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

table( replicate( 1000000, {x <- round(runif(10), 5); 
                            sig <- sample.int(10); 
               for (i in seq_along(sig)){ x[i] <- x[sig[i]]}; 
                            sum(duplicated(x)) } ) )

     0      1      2      3      4      5      6      7      8 
     1    269  13113 126104 360416 360827 125707  13269    294 
0 голосов
/ 01 октября 2018

разминка

В качестве разминки рассмотрим два более простых примера.

## example 1
x <- 1:11
for (i in 1:10) x[i] <- x[i + 1]
x
# [1]  2  3  4  5  6  7  8  9 10 11 11

x <- 1:11
x[1:10] <- x[2:11]
x
# [1]  2  3  4  5  6  7  8  9 10 11 11

## example 2
x <- 1:11
for (i in 1:10) x[i + 1] <- x[i]
x
# [1] 1 1 1 1 1 1 1 1 1 1 1

x <- 1:11
x[2:11] <- x[1:10]
x
# [1]  1  1  2  3  4  5  6  7  8  9 10

«Векторизация» успешна в 1-м примере, но не во 2-м.Почему?

Вот разумный анализ.«Векторизация» начинается с развертывания цикла, а затем выполняет несколько инструкций параллельно.Возможность векторизации цикла зависит от зависимости данных, переносимой циклом.

Развертывание цикла в примере 1 дает

x[1]  <- x[2]
x[2]  <- x[3]
x[3]  <- x[4]
x[4]  <- x[5]
x[5]  <- x[6]
x[6]  <- x[7]
x[7]  <- x[8]
x[8]  <- x[9]
x[9]  <- x[10]
x[10] <- x[11]

Выполнение этих инструкций один за другим и выполнение их одновременнодать одинаковый результат.Таким образом, этот цикл можно «векторизовать».

Цикл в примере 2 - это

x[2]  <- x[1]
x[3]  <- x[2]
x[4]  <- x[3]
x[5]  <- x[4]
x[6]  <- x[5]
x[7]  <- x[6]
x[8]  <- x[7]
x[9]  <- x[8]
x[10] <- x[9]
x[11] <- x[10]

К сожалению, выполнение этих инструкций одна за другой и выполнение их одновременно не даст одинакового результата.Например, когда они выполняются один за другим, x[2] изменяется в 1-й инструкции, затем это измененное значение передается в x[3] во 2-й инструкции.Так что x[3] будет иметь то же значение, что и x[1].Однако при параллельном выполнении x[3] равняется x[2].В результате этот цикл не может быть «векторизован».

В теории «векторизации»

  • Пример 1 имеет зависимость «запись после чтения» в данных: x[i] изменяется после чтения;
  • Пример 2 имеет зависимость «чтение после записи» в данных: x[i] читается после изменения.

Aцикл с зависимостью данных «запись после чтения» может быть «векторизован», а цикл с зависимостью данных «чтение после записи» не может.


в глубину

Возможно, многие люди были смущены сейчас.«Векторизация» - это «параллельная обработка»?

Да.В 1960-х годах, когда люди задавались вопросом, какой компьютер параллельной обработки предназначен для высокопроизводительных вычислений, Флинн разделил идеи дизайна на 4 типа.Категория «SIMD» (одна инструкция, несколько данных) называется «векторизацией», а компьютер с возможностью «SIMD» называется «векторным процессором» или «процессором массива».

В 1960-х годах не быломного языков программирования.Люди писали ассемблер (затем FORTRAN, когда был изобретен компилятор) для программирования регистров процессора напрямую«SIMD» компьютер может загружать несколько данных в векторный регистр с помощью одной инструкции и выполнять одинаковую арифметику с этими данными одновременно.Таким образом, обработка данных действительно параллельна.Рассмотрим наш пример 1 еще раз.Предположим, что векторный регистр может содержать два векторных элемента, тогда цикл можно выполнить с 5 итерациями, используя векторную обработку, а не 10 итераций, как при скалярной обработке.

reg <- x[2:3]  ## load vector register
x[1:2] <- reg  ## store vector register
-------------
reg <- x[4:5]  ## load vector register
x[3:4] <- reg  ## store vector register
-------------
reg <- x[6:7]  ## load vector register
x[5:6] <- reg  ## store vector register
-------------
reg <- x[8:9]  ## load vector register
x[7:8] <- reg  ## store vector register
-------------
reg <- x[10:11] ## load vector register
x[9:10] <- reg  ## store vector register

Сегодня существует много языков программирования, таких как R .«Векторизация» больше не означает «SIMD». R - это не язык, на котором мы можем программировать регистры процессора.«Векторизация» в R - это просто аналогия «SIMD».В предыдущем вопросе и ответах: Означает ли термин "векторизация" разные вещи в разных контекстах? Я пытался объяснить это.Следующая карта иллюстрирует, как проводится эта аналогия:

single (assembly) instruction    -> single R instruction
CPU vector registers             -> temporary vectors
parallel processing in registers -> C/C++/FORTRAN loops with temporary vectors

Итак, R "векторизация" цикла в примере 1 выглядит примерно так:

## the C-level loop is implemented by function "["
tmp <- x[2:11]  ## load data into a temporary vector
x[1:10] <- tmp  ## fill temporary vector into x

Большую часть времени мы простоделать

x[1:10] <- x[2:10]

без явного присвоения временного вектора переменной.На созданный временный блок памяти не указывает ни одна переменная R, и поэтому он подлежит сборке мусора.


полная картинка

В приведенном выше разделе «векторизация» не вводитсяс самым простым примером.Очень часто «векторизация» вводится с чем-то вроде

a[1] <- b[1] + c[1]
a[2] <- b[2] + c[2]
a[3] <- b[3] + c[3]
a[4] <- b[4] + c[4]

, где a, b и c не являются псевдонимами в памяти, то есть блоки памяти, хранящие векторы a,b и c не перекрываются.Это идеальный случай, так как отсутствие псевдонимов памяти не подразумевает зависимости от данных.

Помимо «зависимости от данных», существует также «зависимость управления», то есть работа с «если ... еще ...»"в" векторизации ".Однако по причине времени и пространства я не буду подробно останавливаться на этом вопросе.


вернуться к примеру в вопросе

Теперь пришло время исследовать цикл в вопросе.

set.seed(0)
x <- round(runif(10), 2)
sig <- sample.int(10)
# [1]  1  2  9  5  3  4  8  6  7 10
for (i in seq_along(sig)) x[i] <- x[sig[i]]

Развертывание цикла дает

x[1]  <- x[1]
x[2]  <- x[2]
x[3]  <- x[9]   ## 3rd instruction
x[4]  <- x[5]
x[5]  <- x[3]   ## 5th instruction
x[6]  <- x[4]
x[7]  <- x[8]
x[8]  <- x[6]
x[9]  <- x[7]
x[10] <- x[10]

Между 3-й и 5-й инструкциями существует зависимость данных «чтение после записи», поэтому цикл нельзя «векторизовать» (см. Замечание 1 ).

Что же тогдаx[] <- x[sig] делает?Давайте сначала явно выпишем временный вектор:

tmp <- x[sig]
x[] <- tmp

Поскольку "[" вызывается дважды, за этим "векторизованным" кодом на самом деле находятся два цикла уровня C:

tmp[1]  <- x[1]
tmp[2]  <- x[2]
tmp[3]  <- x[9]
tmp[4]  <- x[5]
tmp[5]  <- x[3]
tmp[6]  <- x[4]
tmp[7]  <- x[8]
tmp[8]  <- x[6]
tmp[9]  <- x[7]
tmp[10] <- x[10]

x[1]  <- tmp[1]
x[2]  <- tmp[2]
x[3]  <- tmp[3]
x[4]  <- tmp[4]
x[5]  <- tmp[5]
x[6]  <- tmp[6]
x[7]  <- tmp[7]
x[8]  <- tmp[8]
x[9]  <- tmp[9]
x[10] <- tmp[10]

Таким образом, x[] <- x[sig] эквивалентно

for (i in 1:10) tmp[i] <- x[sig[i]]
for (i in 1:10) x[i] <- tmp[i]
rm(tmp); gc()

, что совсем не соответствует первоначальному циклу, указанному в вопросе.


Замечание 1

При реализации циклав Rcpp рассматривается как "векторизация", тогда пусть это будет.Но нет никакой возможности в дальнейшем «векторизовать» цикл C / C ++ с помощью «SIMD».


Замечание 2

Эти вопросы и ответы мотивированы этими вопросамиA .OP первоначально представил цикл

for (i in 1:num) {
  for (j in 1:num) {
    mat[i, j] <- mat[i, mat[j, "rm"]]
  }
}

Заманчиво «векторизовать» его как

mat[1:num, 1:num] <- mat[1:num, mat[1:num, "rm"]]

, но это потенциально неправильно.Позже OP изменил цикл на

for (i in 1:num) {
  for (j in 1:num) {
    mat[i, j] <- mat[i, 1 + num + mat[j, "rm"]]
  }
}

, что устраняет проблему псевдонимов памяти, поскольку заменяемые столбцы - это первые num столбцы, а столбцы, которые нужно искать, - после первого num.столбцы.


Замечание 3

Я получил несколько комментариев относительно того, выполняет ли цикл в вопросе "11-местную" модификацию x.Да, это.Мы можем использовать tracemem:

set.seed(0)
x <- round(runif(10), 2)
sig <- sample.int(10)
tracemem(x)
#[1] "<0x28f7340>"
for (i in seq_along(sig)) x[i] <- x[sig[i]]
tracemem(x)
#[1] "<0x28f7340>"

Мой сеанс R выделил блок памяти, указанный адресом <0x28f7340> для x, и вы можете увидеть другое значение при запуске кода.Однако вывод tracemem не изменится после цикла, что означает, что копия x не создается.Таким образом, цикл действительно выполняет модификацию «на месте» без использования дополнительной памяти.

Однако цикл не выполняет перестановку «на месте».Перестановка на месте - более сложная операция.Меняются не только элементы x вдоль цикла, но и элементы sig также должны быть поменяны местами (и, в конце концов, sig будет 1:10).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...