web-dev-qa-db-fra.com

Multiplication matricielle optimisée en C

J'essaie de comparer différentes méthodes de multiplication matricielle. La première est la méthode normale:

do
{
    for (j = 0; j < i; j++)
    {
        for (k = 0; k < i; k++)
        {
            suma = 0;
            for (l = 0; l < i; l++)
                suma += MatrixA[j][l]*MatrixB[l][k];
                MatrixR[j][k] = suma;
            }
        }
    }
    c++;
} while (c<iteraciones);

La seconde consiste à transposer d'abord la matrice B puis à effectuer la multiplication par lignes:

int f, co;
for (f = 0; f < i; f++) {
    for ( co = 0; co < i; co++) {
        MatrixB[f][co] = MatrixB[co][f];
    }
}

c = 0;
do
{
    for (j = 0; j < i; j++)
    {
        for (k = 0; k < i; k++)
        {
            suma = 0;
            for (l = 0; l < i; l++)
                suma += MatrixA[j][l]*MatrixB[k][l];
                MatrixR[j][k] = suma;
            }
        }
     }
     c++;
} while (c<iteraciones);

La deuxième méthode est censée être beaucoup plus rapide, car nous accédons à des emplacements de mémoire contigus, mais je n'obtiens pas d'amélioration significative des performances. Est-ce que je fais quelque chose de mal?

Je peux poster le code complet, mais je pense que ce n'est pas nécessaire.

22
Peter

Ce que tout programmeur devrait savoir sur la mémoire (lien pdf) par Ulrich Drepper a beaucoup de bonnes idées sur l’efficacité de la mémoire, mais il utilise en particulier la multiplication matricielle comme exemple de la manière dont la connaissance de la mémoire et son utilisation peuvent accélérer ce processus. Regardez l'annexe A.1 dans son article et lisez la section 6.2.1. Le tableau 6.2 dans le document montre qu'il pouvait obtenir un temps d'exécution de 10% sur une mise en œuvre naïve pour une matrice 1000x1000.

Certes, son code final est assez poilu et utilise beaucoup de choses spécifiques au système et d’ajustements à la compilation, mais néanmoins, si vous vraiment avez besoin de rapidité, la lecture de ce papier et la lecture de son implémentation en valent vraiment la peine.

24
Alok Singhal

Obtenir ce droit peut être non-trivial. Une optimisation particulièrement importante pour les grandes matrices est la mise en mosaïque de la multiplication pour conserver les éléments dans le cache. Une fois, j’ai mesuré une différence de performance de 12x, mais j’ai spécifiquement choisi une taille de matrice qui consomme des multiples de mon cache (vers 1997, donc le cache était petit).

Il y a un lot de la littérature sur le sujet. Un point de départ est:

http://en.wikipedia.org/wiki/Loop_tiling

Pour une étude plus approfondie, les références suivantes, en particulier les livres Banerjee, peuvent être utiles:

[Ban93] Banerjee, Utpal, Transformations de boucle pour les compilateurs en restructuration: les fondations, Kluwer Academic Publishers, Norwell, MA, 1993.

[Ban94] Banerjee, Utpal, Parallélisation de boucle, Kluwer Academic Publishers, Norwell, MA, 1994.

[BGS93] Bacon, David F., Susan L. Graham et Oliver Sharp, Transformations du compilateur pour l'informatique haute performance, Division de l'informatique, Université de Californie, Berkeley, Californie, Rapport technique n ° UCB/CSD-93-781.

[LRW91] Lam, Monica S., Edward E. Rothberg et Michael E. Wolf. La performance du cache et les optimisations des algorithmes bloqués, dans la 4e Conférence internationale sur le support architectural pour les langages de programmation, tenue à Santa Clara, Californie, avril 1991, 63-74.

[LW91] Lam, Monica S. et Michael E. Wolf. Une théorie de la transformation de boucle et un algorithme pour maximiser le parallélisme, dans des transactions IEEE sur des systèmes parallèles et distribués, 1991, 2 (4): 452-471.

[PW86] Padua, David A. et Michael J. Wolfe, Optimisations avancées du compilateur pour superordinateurs, Communications of the ACM, 29 (12): 1184-1201, 1986.

[Wolfe89] Wolfe, Michael J. Optimisation des supercompilateurs pour supercalculateurs, The MIT Press, Cambridge, MA, 1989.

[Wolfe96] Wolfe, Michael J., Compilateurs de hautes performances pour l'informatique parallèle, Addison-Wesley, CA, 1996.

13
Peeter Joot

ATTENTION: vous avez un BUG dans votre deuxième implémentation

for (f = 0; f < i; f++) {
    for (co = 0; co < i; co++) {
        MatrixB[f][co] = MatrixB[co][f];
    }
}

Quand vous faites f = 0, c = 1

        MatrixB[0][1] = MatrixB[1][0];

vous écrasez MatrixB[0][1] et perdez cette valeur! Lorsque la boucle atteint f = 1, c = 0

        MatrixB[1][0] = MatrixB[0][1];

la valeur copiée est la même que celle qui existait déjà.

7
pmg

Si la matrice n'est pas assez grande ou si vous ne répétez pas les opérations un nombre élevé de fois, vous ne verrez pas de différences appréciables.

Si la matrice est, par exemple, 1 000 x 1 000, vous constaterez des améliorations, mais je dirais que si elle est inférieure à 100 x 100, vous ne devriez pas vous en inquiéter.

En outre, toute «amélioration» peut être de l'ordre de la milliseconde, à moins que vous ne travailliez avec des matrices extrêmement volumineuses ou que l'opération ne se répète pas des milliers de fois.

Enfin, si vous modifiez l'ordinateur que vous utilisez pour un ordinateur plus rapide, les différences seront encore plus étroites!

4
Pablo Rodriguez

pas si spécial mais meilleur:

    c = 0;
do
{
    for (j = 0; j < i; j++)
    {
        for (k = 0; k < i; k++)
        {
            sum = 0; sum_ = 0;
            for (l = 0; l < i; l++) {
                MatrixB[j][k] = MatrixB[k][j];
                sum += MatrixA[j][l]*MatrixB[k][l];
                l++;
                MatrixB[j][k] = MatrixB[k][j];
                sum_ += MatrixA[j][l]*MatrixB[k][l];

                sum += sum_;
            }
            MatrixR[j][k] = sum;
        }
     }
     c++;
} while (c<iteraciones);
1
user3089939

La complexité de calcul de la multiplication de deux matrices N * N est O (N ^ 3). Les performances seront considérablement améliorées si vous utilisez l'algorithme O (N ^ 2.73) qui a probablement été adopté par MATLAB. Si vous avez installé MATLAB, essayez de multiplier deux matrices 1024 * 1024. Sur mon ordinateur, MATLAB le complète en 0.7s, mais la mise en oeuvre C\C++ de l’algorithme naïf comme le vôtre prend 20s. Si vous vous souciez vraiment de la performance, reportez-vous aux algorithmes moins complexes. J'ai entendu dire qu'il existe un algorithme O (N ^ 2.4), mais qu'il a besoin d'une très grande matrice pour que d'autres manipulations puissent être négligées.

1
user2277473

Vous ne devriez pas écrire la multiplication matricielle. Vous devriez dépendre de bibliothèques externes. En particulier, vous devez utiliser la routine GEMM de la bibliothèque BLAS. GEMM fournit souvent les optimisations suivantes

Blocage

La multiplication matricielle efficace repose sur le blocage de votre matrice et l'exécution de plusieurs multiplications bloquées plus petites. Idéalement, la taille de chaque bloc est choisie pour s'intégrer parfaitement dans le cache améliorant considérablement les performances

Réglage

La taille de bloc idéale dépend de la hiérarchie de la mémoire sous-jacente (quelle est la taille du cache?). En conséquence, les bibliothèques doivent être ajustées et compilées pour chaque machine spécifique. Ceci est fait, entre autres, par la mise en œuvre ATLAS de BLAS.

Optimisation du niveau d'assemblage

La multiplication de matrice est si courante que les développeurs l’optimiseront à la main . En particulier, ceci est fait dans GotoBLAS.

Calcul hétérogène (GPU)

Matrix Multiply est un processus très intensif en FLOP/calcul, ce qui en fait un candidat idéal pour être exécuté sur des GPU. cuBLAS et MAGMA sont de bons candidats pour cela. 

En bref, l'algèbre linéaire dense est un sujet bien étudié. Les gens consacrent leur vie à l'amélioration de ces algorithmes. Vous devriez utiliser leur travail. ça va les rendre heureux.

1
MRocklin

Pouvez-vous publier des données comparant vos 2 approches pour une gamme de tailles de matrice? Il se peut que vos attentes soient irréalistes et que votre deuxième version soit plus rapide, mais vous n’avez pas encore fait les mesures.

N'oubliez pas, lors de la mesure du temps d'exécution, d'inclure le temps nécessaire pour transposer la matriceB.

Vous pouvez également essayer d’essayer de comparer les performances de votre code avec celles de l’opération équivalente de votre bibliothèque BLAS. Cela peut ne pas répondre directement à votre question, mais cela vous donnera une meilleure idée de ce à quoi vous pouvez vous attendre de votre code.

1

Les améliorations importantes que vous obtiendrez dépendront de:

  1. La taille de la cache
  2. La taille d'une ligne de cache
  3. Le degré d'associativité du cache

Pour les petites tailles de matrice et les processeurs modernes, il est très probable que les données de MatrixA et MatrixB soient presque entièrement conservées dans le cache après la première utilisation.

1
Andreas Brinck

Juste quelque chose à essayer (mais cela ne ferait la différence que pour les grandes matrices): séparez votre logique d’addition de la logique de multiplication dans la boucle interne, comme suit:

for (k = 0; k < i; k++)
{
    int sums[i];//I know this size declaration is illegal in C. consider 
            //this pseudo-code.
    for (l = 0; l < i; l++)
        sums[l] = MatrixA[j][l]*MatrixB[k][l];

    int suma = 0;
    for(int s = 0; s < i; s++)
       suma += sums[s];
}

En effet, vous finissez par bloquer votre pipeline lorsque vous écrivez dans suma. Certes, une grande partie de cela est pris en compte dans le changement de nom de registre, etc., mais avec ma compréhension limitée du matériel, si je voulais extraire chaque once de performance du code, je le ferais parce que maintenant, vous n'avez pas à caler le pipeline pour attendre une écriture à suma. Étant donné que la multiplication coûte plus cher que l’addition, vous voulez laisser la machine la paralléliser autant que possible. Par conséquent, enregistrer vos stands pour l’addition signifie que vous passerez moins de temps à attendre dans la boucle d’addition que dans la boucle de multiplication.

Ceci est juste ma logique. D'autres personnes ayant plus de connaissances dans le domaine peuvent être en désaccord.

1
San Jacinto

Très vieille question, mais voici ma mise en œuvre actuelle pour mes projets opengl:

typedef float matN[N][N];

inline void matN_mul(matN dest, matN src1, matN src2)
{
    unsigned int i;
    for(i = 0; i < N^2; i++)
    {
        unsigned int row = (int) i / 4, col = i % 4;
        dest[row][col] = src1[row][0] * src2[0][col] +
                         src1[row][1] * src2[1][col] +
                         ....
                         src[row][N-1] * src3[N-1][col];
    }
}

Où N est remplacé par la taille de la matrice. Donc, si vous multipliez les matrices 4x4, alors vous utilisez:

typedef float mat4[4][4];    

inline void mat4_mul(mat4 dest, mat4 src1, mat4 src2)
{
    unsigned int i;
    for(i = 0; i < 16; i++)
    {
        unsigned int row = (int) i / 4, col = i % 4;
        dest[row][col] = src1[row][0] * src2[0][col] +
                         src1[row][1] * src2[1][col] +
                         src1[row][2] * src2[2][col] +
                         src1[row][3] * src2[3][col];
    }
}

Cette fonction minimise principalement les boucles, mais le module peut être contraignant. Sur mon ordinateur, cette fonction s’applique environ 50% plus vite que la multiplication par trois de la boucle. 

Les inconvénients:

  • Beaucoup de code nécessaire (ex. Différentes fonctions pour mat3 x mat3, mat5 x mat5 ...)

  • Tweaks nécessaires pour la multiplication irrégulière (ex. Mat3 x mat4) .....

0
Jas

De manière générale, la transposition de B devrait finit par être beaucoup plus rapide que la mise en œuvre naïve, mais au détriment de la perte d’une autre mémoire NxN. Je viens de passer une semaine à explorer l'optimisation de la multiplication matricielle, et à ce jour, le gagnant absolu est le suivant:

for (int i = 0; i < N; i++)
    for (int k = 0; k < N; k++)
        for (int j = 0; j < N; j++)
            if (likely(k)) /* #define likely(x) __builtin_expect(!!(x), 1) */
                C[i][j] += A[i][k] * B[k][j];
            else
                C[i][j] = A[i][k] * B[k][j];

C'est encore mieux que la méthode de Drepper mentionnée dans un commentaire précédent, car cela fonctionne de manière optimale, quelles que soient les propriétés de cache du processeur sous-jacent. L'astuce consiste à réorganiser les boucles afin que les trois matrices soient accessibles par ordre de rangées majeures.

0
Mike Benden

Si vous travaillez sur de petits nombres, l'amélioration que vous mentionnez est négligeable. En outre, les performances varient en fonction du matériel sur lequel vous exécutez. Mais si vous travaillez sur des nombres en millions, cela aura un effet. En venant au programme, pouvez-vous coller le programme que vous avez écrit.

0
Roopesh Majeti