web-dev-qa-db-fra.com

Un programme de transposition de matrice efficace en cache?

La manière évidente de transposer une matrice est donc d'utiliser:

  for( int i = 0; i < n; i++ )

    for( int j = 0; j < n; j++ )

      destination[j+i*n] = source[i+j*n];

mais je veux quelque chose qui profitera du blocage de la localité et du cache. Je le cherchais et je ne trouve pas de code qui ferait cela, mais on me dit que cela devrait être une modification très simple de l'original. Des idées?

Edit: J'ai une matrice 2000x2000, et je veux savoir comment changer le code en utilisant deux boucles for, divisant essentiellement la matrice en blocs que je transpose individuellement, disons blocs 2x2 ou blocs 40x40, et voir quelle taille de bloc est la plus efficace.

Edit2: Les matrices sont stockées dans l'ordre des colonnes, c'est-à-dire pour une matrice

a1 a2    
a3 a4

est stocké en tant que a1 a3 a2 a4.

29
user635832

Vous allez probablement vouloir quatre boucles - deux pour parcourir les blocs, puis deux autres pour effectuer la transposition-copie d'un seul bloc. En supposant pour simplifier une taille de bloc qui divise la taille de la matrice, quelque chose comme ça, je pense, bien que je voudrais dessiner quelques images sur le dos des enveloppes pour être sûr:

for (int i = 0; i < n; i += blocksize) {
    for (int j = 0; j < n; j += blocksize) {
        // transpose the block beginning at [i,j]
        for (int k = i; k < i + blocksize; ++k) {
            for (int l = j; l < j + blocksize; ++l) {
                dst[k + l*n] = src[l + k*n];
            }
        }
    }
}

Un autre aperçu important est qu'il existe en fait un algorithme sans cache pour cela (voir http://en.wikipedia.org/wiki/Cache-oblivious_algorithm , qui utilise ce problème exact comme exemple). La définition informelle de "cache-inconscient" est que vous n'avez pas besoin d'expérimenter le réglage des paramètres (dans ce cas, la taille de bloc) afin d'atteindre des performances de cache bonnes/optimales. La solution dans ce cas est de transposer en divisant récursivement la matrice en deux et en transposant les moitiés dans leur position correcte dans la destination.

Quelle que soit la taille du cache, cette récursivité en profite. Je suppose qu'il y a un peu de surcharge de gestion supplémentaire par rapport à votre stratégie, qui consiste à utiliser des expériences de performance pour, en fait, sauter directement au point de la récursivité auquel le cache entre vraiment en jeu, et ne pas aller plus loin. D'un autre côté, vos expériences de performances peuvent vous donner une réponse qui fonctionne sur votre machine mais pas sur les machines de vos clients.

38
Steve Jessop

J'ai eu exactement le même problème hier. Je me suis retrouvé avec cette solution:

void transpose(double *dst, const double *src, size_t n, size_t p) noexcept {
    THROWS();
    size_t block = 32;
    for (size_t i = 0; i < n; i += block) {
        for(size_t j = 0; j < p; ++j) {
            for(size_t b = 0; b < block && i + b < n; ++b) {
                dst[j*n + i + b] = src[(i + b)*p + j];
            }
        }
    }
}

C'est 4 fois plus rapide que la solution évidente sur ma machine.

Cette solution prend en charge une matrice rectangulaire avec des dimensions qui ne sont pas un multiple de la taille du bloc.

si dst et src sont la même matrice carrée, une fonction sur place devrait vraiment être utilisée à la place:

void transpose(double*m,size_t n)noexcept{
    size_t block=0,size=8;
    for(block=0;block+size-1<n;block+=size){
        for(size_t i=block;i<block+size;++i){
            for(size_t j=i+1;j<block+size;++j){
                std::swap(m[i*n+j],m[j*n+i]);}}
        for(size_t i=block+size;i<n;++i){
            for(size_t j=block;j<block+size;++j){
                std::swap(m[i*n+j],m[j*n+i]);}}}
    for(size_t i=block;i<n;++i){
        for(size_t j=i+1;j<n;++j){
            std::swap(m[i*n+j],m[j*n+i]);}}}

J'ai utilisé C++ 11 mais cela pourrait être facilement traduit dans d'autres langues.

10
Arnaud

Au lieu de transposer la matrice en mémoire, pourquoi ne pas réduire l'opération de transposition en la prochaine opération que vous allez faire sur la matrice?

7
payne

Steve Jessop a mentionné un algorithme de transposition de matrice sans mémoire cache. Pour mémoire, je souhaite partager une implémentation possible d'une cache inconsciente de transposition matricielle.

public class Matrix {
    protected double data[];
    protected int rows, columns;

    public Matrix(int rows, int columns) {
        this.rows = rows;
        this.columns = columns;
        this.data = new double[rows * columns];
    }

    public Matrix transpose() {
        Matrix C = new Matrix(columns, rows);
        cachetranspose(0, rows, 0, columns, C);
        return C;
    }

    public void cachetranspose(int rb, int re, int cb, int ce, Matrix T) {
        int r = re - rb, c = ce - cb;
        if (r <= 16 && c <= 16) {
            for (int i = rb; i < re; i++) {
                for (int j = cb; j < ce; j++) {
                    T.data[j * rows + i] = data[i * columns + j];
                }
            }
        } else if (r >= c) {
            cachetranspose(rb, rb + (r / 2), cb, ce, T);
            cachetranspose(rb + (r / 2), re, cb, ce, T);
        } else {
            cachetranspose(rb, re, cb, cb + (c / 2), T);
            cachetranspose(rb, re, cb + (c / 2), ce, T);
        }
    }
}

Plus de détails sur les algorithmes inconscients du cache peuvent être trouvés ici .

5
mariolpantunes

Multiplication matricielle vient à l'esprit, mais le problème de cache y est beaucoup plus prononcé, car chaque élément est lu [~ # ~] n [~ # ~] fois.

Avec la transposition matricielle, vous lisez en une seule passe linéaire et il n'y a aucun moyen d'optimiser cela. Mais vous pouvez traiter simultanément plusieurs lignes afin d'écrire plusieurs colonnes et de remplir ainsi des lignes de cache complètes. Vous n'aurez besoin que de trois boucles.

Ou faites-le dans l'autre sens et lisez en colonnes tout en écrivant de façon linéaire.

2
aaz

Avec une grande matrice, peut-être une grande matrice clairsemée, ce pourrait être une idée de la décomposer en petits morceaux compatibles avec le cache (disons, des sous-matrices 4x4). Vous pouvez également marquer les sous-matrices comme identité, ce qui vous aidera à créer des chemins de code optimisés.

0
Luther