Je n'arrête pas de me prendre la tête avec ça. J'ai un algorithme basé sur l'ESS pour multiplier la matrice A
par matrice B
. Je dois également mettre en œuvre les opérations pour lesquelles A, B ou les deux sont transposés. J'ai fait une implémentation naïve, le code de la matrice 4x4 représenté ci-dessous (qui est une opération SSE assez standard, je pense), mais le code de la matrice 4x4 n'a pas été implémenté. A*B^T
L'opération prend environ deux fois plus de temps que l'opération A*B
. L'implémentation d'ATLAS renvoie des valeurs similaires pour A*B
et des résultats presque identiques pour la multiplication par une transposition, ce qui me laisse penser qu'il existe un moyen efficace de procéder.
MM-Multiplication :
m1 = (mat1.m_>>2)<<2;
n2 = (mat2.n_>>2)<<2;
n = (mat1.n_>>2)<<2;
for (k=0; k<n; k+=4) {
for (i=0; i<m1; i+=4) {
// fetch: get 4x4 matrix from mat1
// row-major storage, so get 4 rows
Float* a0 = mat1.el_[i]+k;
Float* a1 = mat1.el_[i+1]+k;
Float* a2 = mat1.el_[i+2]+k;
Float* a3 = mat1.el_[i+3]+k;
for (j=0; j<n2; j+=4) {
// fetch: get 4x4 matrix from mat2
// row-major storage, so get 4 rows
Float* b0 = mat2.el_[k]+j;
Float* b1 = mat2.el_[k+1]+j;
Float* b2 = mat2.el_[k+2]+j;
Float* b3 = mat2.el_[k+3]+j;
__m128 b0r = _mm_loadu_ps(b0);
__m128 b1r = _mm_loadu_ps(b1);
__m128 b2r = _mm_loadu_ps(b2);
__m128 b3r = _mm_loadu_ps(b3);
{ // first row of result += first row of mat1 * 4x4 of mat2
__m128 cX1 = _mm_add_ps(_mm_mul_ps(_mm_load_ps1(a0+0), b0r), _mm_mul_ps(_mm_load_ps1(a0+1), b1r));
__m128 cX2 = _mm_add_ps(_mm_mul_ps(_mm_load_ps1(a0+2), b2r), _mm_mul_ps(_mm_load_ps1(a0+3), b3r));
Float* c0 = this->el_[i]+j;
_mm_storeu_ps(c0, _mm_add_ps(_mm_add_ps(cX1, cX2), _mm_loadu_ps(c0)));
}
{ // second row of result += second row of mat1 * 4x4 of mat2
__m128 cX1 = _mm_add_ps(_mm_mul_ps(_mm_load_ps1(a1+0), b0r), _mm_mul_ps(_mm_load_ps1(a1+1), b1r));
__m128 cX2 = _mm_add_ps(_mm_mul_ps(_mm_load_ps1(a1+2), b2r), _mm_mul_ps(_mm_load_ps1(a1+3), b3r));
Float* c1 = this->el_[i+1]+j;
_mm_storeu_ps(c1, _mm_add_ps(_mm_add_ps(cX1, cX2), _mm_loadu_ps(c1)));
}
{ // third row of result += third row of mat1 * 4x4 of mat2
__m128 cX1 = _mm_add_ps(_mm_mul_ps(_mm_load_ps1(a2+0), b0r), _mm_mul_ps(_mm_load_ps1(a2+1), b1r));
__m128 cX2 = _mm_add_ps(_mm_mul_ps(_mm_load_ps1(a2+2), b2r), _mm_mul_ps(_mm_load_ps1(a2+3), b3r));
Float* c2 = this->el_[i+2]+j;
_mm_storeu_ps(c2, _mm_add_ps(_mm_add_ps(cX1, cX2), _mm_loadu_ps(c2)));
}
{ // fourth row of result += fourth row of mat1 * 4x4 of mat2
__m128 cX1 = _mm_add_ps(_mm_mul_ps(_mm_load_ps1(a3+0), b0r), _mm_mul_ps(_mm_load_ps1(a3+1), b1r));
__m128 cX2 = _mm_add_ps(_mm_mul_ps(_mm_load_ps1(a3+2), b2r), _mm_mul_ps(_mm_load_ps1(a3+3), b3r));
Float* c3 = this->el_[i+3]+j;
_mm_storeu_ps(c3, _mm_add_ps(_mm_add_ps(cX1, cX2), _mm_loadu_ps(c3)));
}
}
// Code omitted to handle remaining rows and columns
}
Pour la multiplication MT (matrice multipliée par la matrice transposée), j'ai stocké b0r à b3r avec les commandes suivantes et j'ai modifié les variables de la boucle en conséquence :
__m128 b0r = _mm_set_ps(b3[0], b2[0], b1[0], b0[0]);
__m128 b1r = _mm_set_ps(b3[1], b2[1], b1[1], b0[1]);
__m128 b2r = _mm_set_ps(b3[2], b2[2], b1[2], b0[2]);
__m128 b3r = _mm_set_ps(b3[3], b2[3], b1[3], b0[3]);
Je soupçonne que le ralentissement est en partie dû à la différence entre l'extraction d'une ligne à la fois et le fait de devoir stocker 4 valeurs à chaque fois pour obtenir la colonne, mais j'ai l'impression que l'autre façon de procéder, l'extraction de lignes de B et la multiplication par la colonne de As, ne fera que déplacer le coût vers le stockage de 4 colonnes de résultats.
J'ai également essayé d'insérer les lignes de B en tant que lignes et d'utiliser ensuite la fonction _MM_TRANSPOSE4_PS(b0r, b1r, b2r, b3r);
pour effectuer la transposition (je pensais qu'il y avait des optimisations supplémentaires dans cette macro), mais il n'y a pas d'amélioration réelle.
A priori, je pense que cela devrait être plus rapide... les produits points impliqués seraient une ligne par une ligne, ce qui semble intrinsèquement plus efficace, mais en essayant de faire les produits points directement, on se retrouve à devoir faire la même chose pour stocker les résultats.
Qu'est-ce qui m'échappe ?
Ajouté : Pour clarifier, j'essaie de ne pas transposer les matrices. Je préfère itérer le long des matrices. Le problème, d'après ce que je sais, est que la commande _mm_set_ps est beaucoup plus lente que _mm_load_ps.
J'ai également essayé une variante dans laquelle j'ai stocké les 4 lignes de la matrice A, puis j'ai remplacé les 4 segments entre crochets contenant 1 chargement, 4 multiplications et 2 additions par 4 instructions de multiplication et 3 instructions d'addition. hadds
mais sans grand résultat. Le timing reste le même (et oui, j'ai essayé avec une instruction de débogage pour vérifier que le code avait changé dans ma compilation de test. Cette déclaration de débogage a été supprimée avant le profilage, bien sûr) :
{ // first row of result += first row of mat1 * 4x4 of mat2
__m128 cX1 = _mm_hadd_ps(_mm_mul_ps(a0r, b0r), _mm_mul_ps(a0r, b1r));
__m128 cX2 = _mm_hadd_ps(_mm_mul_ps(a0r, b2r), _mm_mul_ps(a0r, b3r));
Float* c0 = this->el_[i]+j;
_mm_storeu_ps(c0, _mm_add_ps(_mm_hadd_ps(cX1, cX2), _mm_loadu_ps(c0)));
}
{ // second row of result += second row of mat1 * 4x4 of mat2
__m128 cX1 = _mm_hadd_ps(_mm_mul_ps(a1r, b0r), _mm_mul_ps(a1r, b1r));
__m128 cX2 = _mm_hadd_ps(_mm_mul_ps(a1r, b2r), _mm_mul_ps(a1r, b3r));
Float* c0 = this->el_[i+1]+j;
_mm_storeu_ps(c0, _mm_add_ps(_mm_hadd_ps(cX1, cX2), _mm_loadu_ps(c0)));
}
{ // third row of result += third row of mat1 * 4x4 of mat2
__m128 cX1 = _mm_hadd_ps(_mm_mul_ps(a2r, b0r), _mm_mul_ps(a2r, b1r));
__m128 cX2 = _mm_hadd_ps(_mm_mul_ps(a2r, b2r), _mm_mul_ps(a2r, b3r));
Float* c0 = this->el_[i+2]+j;
_mm_storeu_ps(c0, _mm_add_ps(_mm_hadd_ps(cX1, cX2), _mm_loadu_ps(c0)));
}
{ // fourth row of result += fourth row of mat1 * 4x4 of mat2
__m128 cX1 = _mm_hadd_ps(_mm_mul_ps(a3r, b0r), _mm_mul_ps(a3r, b1r));
__m128 cX2 = _mm_hadd_ps(_mm_mul_ps(a3r, b2r), _mm_mul_ps(a3r, b3r));
Float* c0 = this->el_[i+3]+j;
_mm_storeu_ps(c0, _mm_add_ps(_mm_hadd_ps(cX1, cX2), _mm_loadu_ps(c0)));
}
Mise à jour : droite, et en déplaçant le chargement des lignes de a0r
a a3r
dans les accolades, afin d'éviter les perturbations du registre, a également échoué.