Ich versuche, die neuen AVX2 GATHER Anweisungen zu verwenden, um eine spärliche Matrix - Vektormultiplikation zu beschleunigen. Die Matrix befindet sich im CSR-Format (oder Yale-Format) mit einem Zeilenzeiger, der auf ein Spaltenindexarray zeigt, das wiederum die Spalten enthält. Der C-Code für eine solche Matte-vec mul ist wie folgt aussehen:AVX2 Sparse Matrix Multiplikation
for (int row = 0; row < n_rows - 1; row++) {
double rowsum = 0;
for (int col = row_ptr[row]; col < row_ptr[row + 1]; col++) {
rowsum += values[col] * x[col_indices[col]];
}
result[row] = rowsum;
}
Jetzt ist mein Ziel in diesem mit AVX2 intrinsics zu beschleunigen. Der folgende Code funktioniert mit dem neuesten Intel oder GCC, basierend auf https://blog.fox-toolkit.org/?p=174. Ich habe den Rest hier entfernt, weil meine Zeilen alle auf 4 Doubles (Spalten% 4 == 0) ausgerichtet sind (lucky me). Ich habe den Code auch mit Rest, wenn jemand interessiert ist, aber der Punkt ist, der Code ist tatsächlich etwas langsamer. Ich habe die Disassemblierung überprüft und für die obige Version werden nur FP-Anweisungen generiert und für meinen AVX2-Code erscheinen alle AVX2-Ops wie erwartet. Selbst mit kleinen Matrizen, die in den Cache passen, ist die AVX2-Version nicht gut. Ich bin verwirrt hier ...
double* value_base = &values[0];
double* x_base = &x[0];
int* index_base = &col_indices[0];
for (int row = 0; row < n_rows - 1; row++) {
int col_length = row_ptr[row + 1] - row_ptr[row];
__m256d rowsum = _mm256_set1_pd(0.);
for (int col4 = 0; col4 < col_length; col4 += 4) {
// Load indices for x vector(const __m128i*)
__m128i idxreg = _mm_load_si128((const __m128i*)index_base);
// Load 4 doubles from x indexed by idxreg (AVX2)
__m256d x_ = _mm256_i32gather_pd(x_base, idxreg, 8);
// Load 4 doubles linear from memory (value array)
__m256d v_ = _mm256_load_pd(value_base);
// FMA: rowsum += x_ * v_
rowsum = _mm256_fmadd_pd(x_, v_, rowsum);
index_base += 4;
value_base += 4;
}
__m256d s = _mm256_hadd_pd(rowsum, rowsum);
result[row] = ((double*)&s)[0] + ((double*)&s)[2];
// Alternative (not faster):
// Now we split the upper and lower AVX register, and do a number of horizontal adds
//__m256d hsum = _mm256_add_pd(rowsum, _mm256_permute2f128_pd(rowsum, rowsum, 0x1));
//_mm_store_sd(&result[row], _mm_hadd_pd(_mm256_castpd256_pd128(hsum), _mm256_castpd256_pd128(hsum)));
}
Alle Vorschläge sind willkommen.
Vielen Dank, Chris
Grundsätzlich, versammeln sich saugt. – harold
Was @harold gesagt hat: Gesammelte Lasten sind schrecklich ineffizient und werden die Leistung zunichte machen, es sei denn, du machst danach genug Berechnungen, um die Anfangskosten der Lasten zu amortisieren. Sie können sich genauso gut an den skalaren Code halten. –
Emulieren wird mit '_mm_move_sd' +' _mm_loadh_pd' erfasst. Auf Haswell ist es schneller als Hardware zu sammeln. –