おそらく非常に愚かな質問です。
次のループを「ベクトル化」しようとしています。
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]
異なっています。後者の意味は明らかです。順列です。したがって、多くの人々は、ループが単に間違ったことをしていると信じがちです。しかし、決してそんなことは絶対にしないでください。明確に定義された動的プロセスである場合があります。このQ&Aの目的は、どちらが正しいかを判断することではなく、なぜそれらが同等でないのかを説明することです。 「ベクトル化」を理解するための堅実なケーススタディを提供できれば幸いです。
ウォームアップとして、2つの簡単な例を考えてみましょう。
## 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
「ベクトル化」は最初の例では成功しますが、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]
これらの命令を1つずつ実行し、同時に実行すると、同じ結果が得られます。したがって、このループは「ベクトル化」できます。
例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]
残念ながら、これらの命令を1つずつ実行して同時に実行しても、同じ結果にはなりません。たとえば、それらを1つずつ実行すると、1番目の命令でx[2]
が変更され、2番目の命令でこの変更された値がx[3]
に渡されます。したがって、x[3]
はx[1]
と同じ値になります。ただし、並列実行では、x[3]
はx[2]
と等しくなります。その結果、このループは「ベクトル化」できません。
「ベクトル化」理論では、
x[i]
は読み取り後に変更されます。x[i]
は、変更後に読み取られます。「読み取り後書き込み」データ依存性を持つループは「ベクトル化」できますが、「書き込み後読み取り」データ依存性を持つループはできません。
おそらく多くの人が今では混乱しているでしょう。 「ベクトル化」は「並列処理」ですか?
はい。 1960年代、高性能コンピューティング用にどのような並列処理コンピューターを設計するのかと人々が疑問に思ったとき、フリンは設計のアイデアを4つのタイプに分類しました。カテゴリ「SIMD」(単一命令、複数データ)は「ベクトル化」と呼ばれ、「SIMD」機能を備えたコンピュータは「ベクトルプロセッサ」または「アレイプロセッサ」と呼ばれます。
1960年代には、多くのプログラミング言語はありませんでした。 CPUレジスターを直接プログラムするために、人々はアセンブリー(当時はコンパイラーが発明されたときはFORTRAN)を書きました。 「SIMD」コンピュータは、単一の命令で複数のデータをベクトルレジスタにロードし、それらのデータに対して同時に同じ演算を実行できます。したがって、データ処理は確かに並列です。例1をもう一度考えてみましょう。ベクトルレジスタが2つのベクトル要素を保持できると仮定すると、ループは、スカラー処理のように10回の反復ではなく、ベクトル処理を使用して5回の反復で実行できます。
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 [〜#〜]はCPUレジスタをプログラムできる言語ではありません。 Rの「ベクトル化」は、「SIMD」に類似しています。以前のQ&Aで: 「ベクトル化」という用語は異なるコンテキストで異なることを意味しますか? これを説明しようとしました。次のマップは、この類推がどのように行われるかを示しています。
single (Assembly) instruction -> single R instruction
CPU vector registers -> temporary vectors
parallel processing in registers -> C/C++/FORTRAN loops with temporary vectors
したがって、例1のループのR "ベクトル化"は次のようなものです。
## 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
doを格納するメモリブロック重複しません。これは理想的なケースです。メモリのエイリアシングがないことは、データの依存関係がないことを意味します。
「データ依存関係」とは別に、「制御依存関係」、つまり「ベクトル化」の「if ... else ...」を処理します。ただし、時間と空間の理由から、この問題については詳しく説明しません。
ここで、問題のループを調査します。
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
"["
が2回呼び出されるため、実際にはこの「ベクトル化された」コードの背後に2つの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()
それは質問で与えられた元のループではありません。
Rcppでのループの実装が「ベクトル化」と見なされる場合は、そのままにしてください。ただし、C/C++ループを「SIMD」でさらに「ベクトル化」することはできません。
このQ&Aは、 this Q&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
列の後にあるため、メモリエイリアシングの問題がなくなります。
質問のループが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
になります)。
より簡単な説明があります。ループでは、ステップごとにx
の1つの要素を上書きし、以前の値をx
の他の要素の1つで置き換えます。だからあなたはあなたが求めたものを手に入れる。本質的に、それは置換(sample(x, replace=TRUE)
)を伴うサンプリングの複雑な形式です-そのような複雑さが必要かどうかは、達成したいものに依存します。
ベクトル化されたコードでは、x
の特定の順列(置換なし)を求めているだけで、それが得られます。ベクトル化されたコードはnotループと同じことをしています。ループで同じ結果を得るには、最初に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
の最初の要素を変更するとすぐに変更されます。
これは、メモリブロックのエイリアシングとは関係ありません(これまでに出会ったことのない用語です)。特定の順列の例を取り上げ、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
...続行できますが、これは、これが通常「真の」置換ではなく、非常にまれに値の完全な再配分をもたらすことを示しています。私は完全に順序付けられた順列のみを推測しています(そのうちの1つだけ、つまり10:1
)は、一意のxの新しいセットになる可能性があります。
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
Rの「ベクトル化」は「SIMD」とは異なりますが(OPの説明どおり)、ループが「ベクトル化可能」であるかどうかを判断するときに同じロジックが適用されます。これは、OPの自己回答の例を使用したデモです(少し変更を加えています)。
「write-after-read」依存関係を持つ例1は「ベクトル化可能」です。
// "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はnot "vectorizable"です。
// "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
キーワードを使用して、3つの配列間でメモリブロックのエイリアシングがないことをコンパイラに知らせます。
// "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