web-dev-qa-db-fra.com

Pourquoi la "vectorisation" de cette simple boucle R donne-t-elle un résultat différent?

Peut-être une question très stupide.

J'essaie de "vectoriser" la boucle suivante:

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

Je pense que c'est simplement x[sig] mais le résultat ne correspond pas.

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

Qu'est-ce qui ne va pas?


Remarque

De toute évidence, à la sortie, nous voyons que la boucle for et x[sig] sont différents. Le sens de ce dernier est clair: permutation, donc beaucoup de gens ont tendance à croire que la boucle ne fait que des trucs incorrects. Mais ne soyez jamais si sûr; il peut s'agir d'un processus dynamique bien défini. Le but de ce Q & A n'est pas de juger ce qui est correct, mais d'expliquer pourquoi ils ne sont pas équivalents. Espérons que cela fournira une étude de cas solide pour comprendre la "vectorisation".

18
李哲源

réchauffer

Pour vous échauffer, considérez deux exemples plus simples.

## 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

La "vectorisation" réussit dans le 1er exemple mais pas dans le 2ème. Pourquoi?

Voici une analyse prudente. La "vectorisation" commence par le déroulement de la boucle, puis exécute plusieurs instructions en parallèle. La possibilité de "vectoriser" une boucle dépend de la dépendance des données portée par la boucle.

Dérouler la boucle dans l'exemple 1 donne

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]

L'exécution de ces instructions une par une et leur exécution simultanée donnent un résultat identique. Cette boucle peut donc être "vectorisée".

La boucle de l'exemple 2 est

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]

Malheureusement, exécuter ces instructions une par une et les exécuter simultanément ne donnerait pas un résultat identique. Par exemple, lorsque vous les exécutez un par un, x[2] est modifié dans la 1ère instruction, puis cette valeur modifiée est passée à x[3] dans la 2e instruction. Donc x[3] aurait la même valeur que x[1]. Cependant, en exécution parallèle, x[3] équivaut à x[2]. Il en résulte que cette boucle ne peut pas être "vectorisée".

Dans la théorie de la "vectorisation",

  • L'exemple 1 a une dépendance "écriture après lecture" dans les données: x[i] est modifié après sa lecture;
  • L'exemple 2 a une dépendance "lecture après écriture" dans les données: x[i] est lu après sa modification.

Une boucle avec une dépendance de données "écriture après lecture" peut être "vectorisée", contrairement à une boucle avec une dépendance de données "lecture après écriture".


en profondeur

Peut-être que beaucoup de gens sont maintenant confus. La "vectorisation" est-elle un "traitement parallèle"?

Oui. Dans les années 1960, lorsque les gens se sont demandé quel type d'ordinateur de traitement parallèle être conçu pour le calcul haute performance, Flynn a classé les idées de conception en 4 types. La catégorie "SIMD" (instruction unique, données multiples) est appelée "vectorisation", et un ordinateur avec une capacité "SIMD" est appelé "processeur vectoriel" ou "processeur matriciel".

Dans les années 60, il n'y avait pas beaucoup de langages de programmation. Les gens ont écrit Assembly (puis FORTRAN quand un compilateur a été inventé) pour programmer directement les registres CPU. Un ordinateur "SIMD" est capable de charger plusieurs données dans un registre vectoriel avec une seule instruction et de faire la même arithmétique sur ces données en même temps. Le traitement des données est donc bien parallèle. Reprenons notre exemple 1. Supposons qu'un registre vectoriel puisse contenir deux éléments vectoriels, alors la boucle peut être exécutée avec 5 itérations en utilisant le traitement vectoriel plutôt que 10 itérations comme dans le traitement scalaire.

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

Aujourd'hui, il existe de nombreux langages de programmation, comme [~ # ~] r [~ # ~] . La "vectorisation" ne fait plus clairement référence à "SIMD". [~ # ~] r [~ # ~] n'est pas un langage dans lequel nous pouvons programmer des registres CPU. La "vectorisation" dans R n'est qu'une analogie avec "SIMD". Dans un précédent Q&R: Le terme "vectorisation" signifie-t-il différentes choses dans différents contextes? J'ai essayé d'expliquer cela. La carte suivante illustre comment cette analogie est faite:

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

Ainsi, la "vectorisation" R de la boucle dans l'exemple 1 est quelque chose comme

## 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

La plupart du temps, nous le faisons

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

sans affecter explicitement le vecteur temporaire à une variable. Le bloc de mémoire temporaire créé n'est pointé par aucune variable R et est donc soumis à un garbage collection.


une image complète

Dans ce qui précède, la "vectorisation" n'est pas introduite avec l'exemple le plus simple. Très souvent, la "vectorisation" est introduite avec quelque chose comme

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 et c ne sont pas aliasés en mémoire, c'est-à-dire que les blocs de mémoire stockent les vecteurs a, b et c ne se chevauchent pas. C'est un cas idéal, car aucun alias de mémoire n'implique aucune dépendance aux données.

Outre la "dépendance des données", il existe également la "dépendance de contrôle", c'est-à-dire le traitement de "si ... sinon ..." dans la "vectorisation". Cependant, pour des raisons de temps et d'espace, je ne développerai pas cette question.


retour à l'exemple de la question

Il est maintenant temps d'étudier la boucle de la question.

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]]

Dérouler la boucle donne

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]

Il existe une dépendance des données "lecture après écriture" entre la 3e et la 5e instruction, de sorte que la boucle ne peut pas être "vectorisée" (voir Remarque 1 ).

Eh bien, qu'est-ce que x[] <- x[sig] faire? Écrivons d'abord explicitement le vecteur temporaire:

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

Puisque "[" est appelé deux fois, il y a en fait deux boucles de niveau C derrière ce code "vectorisé":

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]

Donc x[] <- x[sig] est équivalent à

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

ce qui n'est pas du tout la boucle d'origine donnée dans la question.


Remarque 1

Si l'implémentation de la boucle dans Rcpp est considérée comme une "vectorisation", alors laissez-la. Mais il n'y a aucune chance de "vectoriser" davantage la boucle C/C++ avec "SIMD".


Remarque 2

Ce Q & A est motivé par ce Q & A . OP a initialement présenté une boucle

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

Il est tentant de le "vectoriser" comme

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

mais c'est potentiellement faux. Plus tard, OP a changé la boucle en

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

ce qui élimine le problème d'alias de mémoire, car les colonnes à remplacer sont les premières num colonnes, tandis que les colonnes à rechercher sont après les premières num colonnes.


Remarque 3

J'ai reçu des commentaires sur la question de savoir si la boucle de la question fait une modification "sur place" de x. Oui, ça l'est. Nous pouvons utiliser 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>"

Ma session R a alloué un bloc de mémoire pointé par l'adresse <0x28f7340> pour x et vous pouvez voir une valeur différente lorsque vous exécutez le code. Cependant, la sortie de tracemem ne changera pas après la boucle, ce qui signifie qu'aucune copie de x n'est effectuée. Ainsi, la boucle effectue en effet une modification "sur place" sans utiliser de mémoire supplémentaire.

Cependant, la boucle ne fait pas de permutation "sur place". La permutation "sur place" est une opération plus compliquée. Non seulement les éléments de x doivent être échangés le long de la boucle, les éléments de sig doivent également être échangés (et à la fin, sig serait 1:10).

16
李哲源

Il y a une explication plus simple. Avec votre boucle, vous écrasez un élément de x à chaque étape, en remplaçant son ancienne valeur par l'un des autres éléments de x. Vous obtenez donc ce que vous avez demandé. Il s'agit essentiellement d'une forme complexe d'échantillonnage avec remplacement (sample(x, replace=TRUE)) - si vous avez besoin d'une telle complication, cela dépend de ce que vous voulez réaliser.

Avec votre code vectorisé, vous demandez juste une certaine permutation de x (sans remplacement), et c'est ce que vous obtenez. Le code vectorisé est pas faisant la même chose que votre boucle. Si vous voulez obtenir le même résultat avec une boucle, vous devez d'abord faire une copie de 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

Aucun danger d'aliasing ici: x et x2 se réfère initialement au même emplacement mémoire, mais le sien changera dès que vous modifiez le premier élément de x2.

3
lebatsnok

Cela n'a rien à voir avec l'alias de bloc de mémoire (un terme que je n'ai jamais rencontré auparavant). Prenez un exemple de permutation particulier et parcourez les affectations qui se produiraient quelle que soit l'implémentation au niveau du langage C ou Assembly (ou autre); Elle est intrinsèque à la façon dont se comporterait une boucle for séquentielle par rapport à la façon dont toute "vraie" permutation (ce que l'on obtient avec x[sig]) qui se produirait:

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

... peut continuer, mais cela illustre comment cela ne sera généralement pas une "vraie" permutation et entraînerait très rarement une redistribution complète des valeurs. Je suppose que seule une permutation complètement ordonnée (dont je pense qu'il n'y en a qu'une, c'est-à-dire 10:1) pourrait entraîner un nouvel ensemble de x uniques.

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

J'ai commencé à me demander quelle pourrait être la distribution des comptes de duplication dans une grande série. Semble assez symétrique:

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 
3
42-

Il est intéressant de voir que bien que la "vectorisation" R soit différente de "SIMD" (comme OP l'a bien expliqué), la même logique s'applique pour déterminer si une boucle est "vectorisable". Voici une démo utilisant des exemples dans la réponse automatique d'OP (avec une petite modification).

L'exemple 1 avec une dépendance "écriture après lecture" est "vectorisable".

// "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

L'exemple 2 avec la dépendance "lecture après écriture" est pas "vectorisable".

// "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

Utilisez le mot clé C99 restrict pour indiquer le compilateur sans alias de bloc de mémoire entre trois tableaux.

// "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
2
GingerCat