Який найшвидший спосіб транспортувати матрицю в C ++?


81

У мене є матриця (відносно велика), яку мені потрібно транспонувати. Наприклад, припустимо, що моя матриця є

a b c d e f
g h i j k l
m n o p q r 

Я хочу, щоб результат був таким:

a g m
b h n
c I o
d j p
e k q
f l r

Який найшвидший спосіб це зробити?


2
Це називається "транспонування". Поворот на 90 градусів - це зовсім інше поняття.
Енді Проул,

35
І найшвидший спосіб - це не обертати його, а просто поміняти місцями порядок індексів при доступі до масиву.
Показник високої продуктивності

2
Як би швидко це не було, ви все одно повинні отримати доступ до всіх елементів матриці.
taocp

10
@HighPerformanceMark: Я гадаю, це залежить, якщо ви тоді бажаєте отримувати доступ до матриці повторно в порядку рядків, якщо прапор "транспонований" сильно вас вразить.
Matthieu M.

3
Транспонування матриць сумно відомо проблемами, які вона викликає з кешами пам'яті. Якщо ваш масив достатньо великий, щоб ефективність транспонування була значною, і ви не можете уникнути транспонування, просто надаючи інтерфейс із заміненими індексами, то найкращим варіантом є використання існуючої бібліотечної процедури для транспонування великих матриць. Фахівці вже виконали цю роботу, і вам слід нею скористатися.
Eric Postpischil

Відповіді:


131

Це гарне запитання. Є багато причин, чому ви хотіли б фактично транспонувати матрицю в пам’ять, а не просто обмінюватись координатами, наприклад, при множенні матриці та розмиванні Гауса.

Спочатку дозвольте мені перерахувати одну з функцій, яку я використовую для транспонування ( EDIT: будь ласка, див. Кінець моєї відповіді, де я знайшов набагато швидше рішення )

void transpose(float *src, float *dst, const int N, const int M) {
    #pragma omp parallel for
    for(int n = 0; n<N*M; n++) {
        int i = n/N;
        int j = n%N;
        dst[n] = src[M*j + i];
    }
}

Тепер давайте подивимося, чому транспонування корисно. Розглянемо множення матриць C = A * B. Ми могли б зробити це таким чином.

for(int i=0; i<N; i++) {
    for(int j=0; j<K; j++) {
        float tmp = 0;
        for(int l=0; l<M; l++) {
            tmp += A[M*i+l]*B[K*l+j];
        }
        C[K*i + j] = tmp;
    }
}

Однак таким чином буде багато помилок кешу. Набагато швидше рішення - спочатку взяти транспонування B

transpose(B);
for(int i=0; i<N; i++) {
    for(int j=0; j<K; j++) {
        float tmp = 0;
        for(int l=0; l<M; l++) {
            tmp += A[M*i+l]*B[K*j+l];
        }
        C[K*i + j] = tmp;
    }
}
transpose(B);

Множення матриці дорівнює O (n ^ 3), а транспонування - O (n ^ 2), тому прийняття транспонування має мати незначний вплив на час обчислення (для великого n). У циклі множення матриць плитка навіть ефективніша, ніж перенесення, але це набагато складніше.

Мені б хотілося, щоб я знав швидший спосіб транспонування ( Редагувати: я знайшов швидше рішення, див. Кінець моєї відповіді ). Коли Haswell / AVX2 вийде через кілька тижнів, він матиме функцію збору. Я не знаю, чи це буде корисно в цьому випадку, але я міг би зобразити збір стовпця та виписування рядка. Можливо, це зробить транспонування непотрібним.

Для розмивання Гаусса те, що ви робите - це розмазування по горизонталі, а потім по вертикалі. Але вертикальне розмазування має проблему з кешем, тож те, що ви робите, це

Smear image horizontally
transpose output 
Smear output horizontally
transpose output

Ось стаття від Intel, яка пояснює, що http://software.intel.com/en-us/articles/iir-gaussian-blur-filter-implementation-using-intel-advanced-vector-extensions

Нарешті, те, що я насправді роблю при множенні матриць (і при розмиванні Гауса), - це не точне транспонування, а транспонування у ширину певного розміру вектора (наприклад, 4 або 8 для SSE / AVX). Ось функція, яку я використовую

void reorder_matrix(const float* A, float* B, const int N, const int M, const int vec_size) {
    #pragma omp parallel for
    for(int n=0; n<M*N; n++) {
        int k = vec_size*(n/N/vec_size);
        int i = (n/vec_size)%N;
        int j = n%vec_size;
        B[n] = A[M*i + k + j];
    }
}

РЕДАГУВАТИ:

Я спробував кілька функцій, щоб знайти найшвидший транспонування для великих матриць. Врешті-решт, найшвидшим результатом є використання блокування циклу за допомогою block_size=16( Редагувати: я знайшов швидше рішення за допомогою SSE та блокування циклу - див. Нижче ). Цей код працює для будь-якої матриці NxM (тобто матриця не повинна бути квадратною).

inline void transpose_scalar_block(float *A, float *B, const int lda, const int ldb, const int block_size) {
    #pragma omp parallel for
    for(int i=0; i<block_size; i++) {
        for(int j=0; j<block_size; j++) {
            B[j*ldb + i] = A[i*lda +j];
        }
    }
}

inline void transpose_block(float *A, float *B, const int n, const int m, const int lda, const int ldb, const int block_size) {
    #pragma omp parallel for
    for(int i=0; i<n; i+=block_size) {
        for(int j=0; j<m; j+=block_size) {
            transpose_scalar_block(&A[i*lda +j], &B[j*ldb + i], lda, ldb, block_size);
        }
    }
}

Значення ldaі ldb- це ширина матриці. Вони повинні бути кратними розміру блоку. Щоб знайти значення та виділити пам'ять, наприклад, для матриці 3000x1001, я роблю щось подібне

#define ROUND_UP(x, s) (((x)+((s)-1)) & -(s))
const int n = 3000;
const int m = 1001;
int lda = ROUND_UP(m, 16);
int ldb = ROUND_UP(n, 16);

float *A = (float*)_mm_malloc(sizeof(float)*lda*ldb, 64);
float *B = (float*)_mm_malloc(sizeof(float)*lda*ldb, 64);

Для 3000x1001 це повертає ldb = 3008і lda = 1008

Редагувати:

Я знайшов ще швидше рішення, використовуючи внутрішні характеристики SSE:

inline void transpose4x4_SSE(float *A, float *B, const int lda, const int ldb) {
    __m128 row1 = _mm_load_ps(&A[0*lda]);
    __m128 row2 = _mm_load_ps(&A[1*lda]);
    __m128 row3 = _mm_load_ps(&A[2*lda]);
    __m128 row4 = _mm_load_ps(&A[3*lda]);
     _MM_TRANSPOSE4_PS(row1, row2, row3, row4);
     _mm_store_ps(&B[0*ldb], row1);
     _mm_store_ps(&B[1*ldb], row2);
     _mm_store_ps(&B[2*ldb], row3);
     _mm_store_ps(&B[3*ldb], row4);
}

inline void transpose_block_SSE4x4(float *A, float *B, const int n, const int m, const int lda, const int ldb ,const int block_size) {
    #pragma omp parallel for
    for(int i=0; i<n; i+=block_size) {
        for(int j=0; j<m; j+=block_size) {
            int max_i2 = i+block_size < n ? i + block_size : n;
            int max_j2 = j+block_size < m ? j + block_size : m;
            for(int i2=i; i2<max_i2; i2+=4) {
                for(int j2=j; j2<max_j2; j2+=4) {
                    transpose4x4_SSE(&A[i2*lda +j2], &B[j2*ldb + i2], lda, ldb);
                }
            }
        }
    }
}

1
Гарний знімок, але я не впевнений, що «множення матриці - це O (n ^ 3)», я думаю, що це O (n ^ 2).
ulyssis2

2
@ ulyssis2 Це O (n ^ 3), якщо ви не використовуєте множення матриці Штрассена (O (n ^ 2.8074)). user2088790: Це дуже добре зроблено. Зберігаю це в своїй особистій колекції. :)
saurabheights

10
На випадок, якщо хтось захоче дізнатися, хто написав цю відповідь, це був я. Я одного разу кинув SO, перебрав це і повернувся.
Z бозон

1
@ ulyssis2 Наївне множення матриці - це, безумовно, O (n ^ 3), і, наскільки мені відомо, обчислювальні ядра реалізують наївний алгоритм (я думаю, це тому, що Штрассен закінчує робити більше операцій (доповнень), що погано, якщо ви можете робити швидкі продукти, але я можу помилятися). Це відкрита проблема, чи може множення матриць бути O (n ^ 2) чи ні.
étale-cohomology

Зазвичай кращий варіант покластись на бібліотеку лінійної алгебри, яка виконає роботу за вас. Сучасні бібліотеки, такі як Intel MKL, OpenBLAS тощо, забезпечують динамічну диспетчеризацію процесора, яка вибирає найкращу реалізацію, доступну для вашого обладнання (наприклад, можуть бути доступні ширші векторні регістри, ніж SSE: AVX AVX2, AVX512 ...), тому ви не робите цього Вам не потрібно створювати непереносну програму, щоб отримати швидку програму.
Хорхе Беллон,

39

Це залежатиме від вашої програми, але загалом найшвидшим способом транспонування матриці буде інвертування ваших координат, коли ви шукаєте, тоді вам не потрібно фактично переміщувати будь-які дані.


32
Це чудово, якщо це невелика матриця або ви читаєте з неї лише один раз. Однак, якщо транспонована матриця велика і її потрібно використовувати багато разів, ви все одно можете зберегти швидко транспоновану версію, щоб отримати кращий шаблон доступу до пам'яті. (+1, до речі)
Агентлієн

2
@Agentlien: Чому A [j] [i] буде повільнішим за A [i] [j]?
мензурка

32
@beaker Якщо у вас велика матриця, різні рядки / стовпці можуть займати різні рядки / сторінки кешу. У цьому випадку вам потрібно виконати ітерацію елементів таким чином, щоб ви отримували доступ до сусідніх елементів один за одним. В іншому випадку це може призвести до того, що доступ до кожного елемента стане пропуском кешу, що повністю знищує продуктивність.
Агентлієн

10
@beaker: це пов’язано з кешуванням на рівні центрального процесора (припустимо, що матриця - це одна велика крапка пам’яті), тоді рядки кешу є ефективними рядками матриці, і програма попереднього завантаження може отримати наступні кілька рядків. Якщо ви перемикаєте доступ, кеш-пам’ять процесора / програма попереднього завантаження все ще працює рядок за рядком, тоді як ви отримуєте доступ до стовпця за стовпцем, падіння продуктивності може бути значним.
Matthieu M.

2
@taocp В основному, вам знадобиться якийсь прапор, щоб вказати, що він транспонований, а потім запит на (i,j)відповідь буде зіставлений(j,i)
Шафік Ягмур

5

Деякі подробиці щодо транспонування матриць квадратного плаваючого формату 4х4 (я обговорю 32-бітове ціле число пізніше) з апаратним забезпеченням x86. Корисно почати тут, щоб транспонувати великі квадратні матриці, такі як 8x8 або 16x16.

_MM_TRANSPOSE4_PS(r0, r1, r2, r3)реалізується різними компіляторами. GCC та ICC (я не перевіряв Clang) використовують, unpcklps, unpckhps, unpcklpd, unpckhpdтоді як MSVC використовує лише shufps. Насправді ми можемо поєднати ці два підходи разом.

t0 = _mm_unpacklo_ps(r0, r1);
t1 = _mm_unpackhi_ps(r0, r1);
t2 = _mm_unpacklo_ps(r2, r3);
t3 = _mm_unpackhi_ps(r2, r3);

r0 = _mm_shuffle_ps(t0,t2, 0x44);
r1 = _mm_shuffle_ps(t0,t2, 0xEE);
r2 = _mm_shuffle_ps(t1,t3, 0x44);
r3 = _mm_shuffle_ps(t1,t3, 0xEE);

Одне цікаве спостереження полягає в тому, що два перетасовки можна перетворити на одне перетасовку і дві суміші (SSE4.1), як це.

t0 = _mm_unpacklo_ps(r0, r1);
t1 = _mm_unpackhi_ps(r0, r1);
t2 = _mm_unpacklo_ps(r2, r3);
t3 = _mm_unpackhi_ps(r2, r3);

v  = _mm_shuffle_ps(t0,t2, 0x4E);
r0 = _mm_blend_ps(t0,v, 0xC);
r1 = _mm_blend_ps(t2,v, 0x3);
v  = _mm_shuffle_ps(t1,t3, 0x4E);
r2 = _mm_blend_ps(t1,v, 0xC);
r3 = _mm_blend_ps(t3,v, 0x3);

Це ефективно перетворило 4 перетасовки в 2 перетасовки і 4 суміші. Тут використано на 2 інструкції більше, ніж впровадження GCC, ICC та MSVC. Перевага полягає в тому, що він зменшує тиск у порту, що може мати вигоду за певних обставин. В даний час всі перетасовки та розпакування можуть надходити лише до одного конкретного порту, тоді як суміші можуть надходити до будь-якого з двох різних портів.

Я спробував використовувати 8 перетасовк, як MSVC, і перетворити їх на 4 перетасовки + 8 сумішей, але це не спрацювало. Мені все-таки довелося використовувати 4 розпакування.

Я використовував цю саму техніку для транспонування 8x8 з плаваючою точкою (див. Наприкінці цієї відповіді). https://stackoverflow.com/a/25627536/2542702 . У цій відповіді мені все-таки довелося використати 8 розпаковувань, але я зумів перетворити 8 перетасовк у 4 перетасовки та 8 сумішей.

Для 32-розрядних цілих чисел немає нічого подібного shufps(за винятком 128-розрядних перетасовк з AVX512), тому його можна реалізувати лише з розпакованими пакунками, які, на мою думку, не можуть бути перетворені в суміші (ефективно). З AVX512 vshufi32x4діє ефективно, як shufpsза винятком 128-бітових смуг з 4 цілими числами, а не 32-бітовими плаваючими, тому vshufi32x4в деяких випадках цей метод може бути використаний . З Knights Landing перетасовки в чотири рази повільніші (пропускна здатність), ніж суміші.


1
Ви можете використовувати shufpsдля цілочисельних даних. Якщо ви робите багато перетасовки, можливо, варто зробити все це в домені FP для shufps+ blendps, особливо якщо у вас немає настільки ж ефективного AVX2 vpblendd. Крім того, на апаратному забезпеченні сімейства Intel SnB немає додаткової затримки обходу для використання shufpsміж цілочисельними інструкціями, наприклад paddd. (Однак існує затримка байпасу для змішування blendpsз paddd, проте, згідно з тестуванням SnB Агнера Фога.)
Пітер Кордес,

@PeterCordes, мені потрібно ще раз переглянути зміни домену. Чи існує якась таблиця (можливо, відповідь на SO), яка узагальнює покарання за зміну домену для Core2-Skylake? У будь-якому випадку я більше подумав над цим. Зараз я розумію, чому wim, і ви продовжували згадувати vinsertf64x4у моїй відповіді на транспонування 16х16 замість vinserti64x4. Якщо я читаю, то пишу матрицю, тоді, безумовно, не має значення, чи використовую я домен з плаваючою точкою або цілий домен, оскільки транспонування - це просто переміщення даних.
Z-бозон

1
У таблицях Агнера перераховані домени для кожної інструкції для Core2 та Nehalem (і AMD, я думаю), але не для сімейства SnB. У керівництві по мікроархіві Агнера є лише абзац, в якому сказано, що на SnB він дорівнює 1c і часто 0, з деякими прикладами. В посібнику з оптимізації Intel, на мою думку, є таблиця, але я не намагався її переглядати, тому не пам’ятаю, наскільки детально вона представлена. Я пам’ятаю, що не зовсім очевидно, якою категорією буде дана інструкція.
Пітер Кордес,

Навіть якщо ви не просто пишете назад у пам'ять, це лише 1 додатковий годинник для всього транспонування. Додаткова затримка для кожного операнда може відбуватися паралельно (або поступово), коли споживач транспонування починає читати регістри, записані перетасовками або комбінуваннями. Виконання, що не працює в порядку, дозволяє першим декільком FMA або тому, що завгодно, розпочинатись, поки закінчуються останні кілька перетасовк, але ланцюга затримок dypass немає, лише додаткове максимум одне.
Пітер Кордес,

1
Відповідь Nicw! У посібнику з оптимізації Intel 64-ia-32-архитектур, таблиця 2-3, перелічені затримки обходу для Skylake, можливо, це вас цікавить. Таблиця 2-8 для Хасуела виглядає зовсім інакше.
wim

1

Розглянемо кожен рядок як стовпець, а кожен стовпець як рядок .. використовуйте j, i замість i, j

демонстрація: http://ideone.com/lvsxKZ

#include <iostream> 
using namespace std;

int main ()
{
    char A [3][3] =
    {
        { 'a', 'b', 'c' },
        { 'd', 'e', 'f' },
        { 'g', 'h', 'i' }
    };

    cout << "A = " << endl << endl;

    // print matrix A
    for (int i=0; i<3; i++)
    {
        for (int j=0; j<3; j++) cout << A[i][j];
        cout << endl;
    }

    cout << endl << "A transpose = " << endl << endl;

    // print A transpose
    for (int i=0; i<3; i++)
    {
        for (int j=0; j<3; j++) cout << A[j][i];
        cout << endl;
    }

    return 0;
}

1

транспонування без накладних витрат (клас не завершений):

class Matrix{
   double *data; //suppose this will point to data
   double _get1(int i, int j){return data[i*M+j];} //used to access normally
   double _get2(int i, int j){return data[j*N+i];} //used when transposed

   public:
   int M, N; //dimensions
   double (*get_p)(int, int); //functor to access elements  
   Matrix(int _M,int _N):M(_M), N(_N){
     //allocate data
     get_p=&Matrix::_get1; // initialised with normal access 
     }

   double get(int i, int j){
     //there should be a way to directly use get_p to call. but i think even this
     //doesnt incur overhead because it is inline and the compiler should be intelligent
     //enough to remove the extra call
     return (this->*get_p)(i,j);
    }
   void transpose(){ //twice transpose gives the original
     if(get_p==&Matrix::get1) get_p=&Matrix::_get2;
     else get_p==&Matrix::_get1; 
     swap(M,N);
     }
}

можна використовувати так:

Matrix M(100,200);
double x=M.get(17,45);
M.transpose();
x=M.get(17,45); // = original M(45,17)

звичайно, я не турбувався тут з управлінням пам'яттю, що є вирішальною, але іншою темою.


4
У вас є накладні витрати з вашого покажчика функції, за якими слід стежити за кожним доступом до елемента.
user877329

1

Якщо розмір масивів був відомий раніше, ми могли б використати об'єднання для нашої допомоги. Подобається це-

#include <bits/stdc++.h>
using namespace std;

union ua{
    int arr[2][3];
    int brr[3][2];
};

int main() {
    union ua uav;
    int karr[2][3] = {{1,2,3},{4,5,6}};
    memcpy(uav.arr,karr,sizeof(karr));
    for (int i=0;i<3;i++)
    {
        for (int j=0;j<2;j++)
            cout<<uav.brr[i][j]<<" ";
        cout<<'\n';
    }

    return 0;
}

Я новачок у C / C ++, але це виглядає геніальним. Оскільки об’єднання використовує спільну пам’ять для своїх членів, ви можете читати цю пам’ять по-різному. Таким чином, ви отримуєте транспоновану матрицю, не роблячи нового розподілу масиву. Я правий?
Doğuş

1
template <class T>
void transpose( const std::vector< std::vector<T> > & a,
std::vector< std::vector<T> > & b,
int width, int height)
{
    for (int i = 0; i < width; i++)
    {
        for (int j = 0; j < height; j++)
        {
            b[j][i] = a[i][j];
        }
    }
} 

1
Я б вважав, що це було б швидше, якщо ви обміняєтеся циклами, через менший штраф за пропуск кешу при записі, ніж читання.
феагон

5
Це працює лише для квадратної матриці. Прямокутна матриця - це зовсім інша проблема!
NealB,

2
Питання задає найшвидший спосіб. Це лише спосіб. Що змушує вас думати, що це швидко, не кажучи вже про найшвидший? Для великих матриць це призведе до обробки кеш-пам'яті та матиме жахливі характеристики.
Eric Postpischil

1
@NealB: Як ти це розумієш?
Eric Postpischil

@EricPostpischil ОП запитує про відносно велику матрицю, тому я припускаю, що вони хотіли зробити це "на місці", щоб уникнути подвоєння пам'яті. Коли це зроблено, базова адреса вихідної та кінцевої матриць однакові. Транспонування шляхом перегортання індексів рядків і стовпців працюватиме лише для квадратних матриць. Є способи отримати це право для прямокутних матриць, але вони дещо складніші.
NealB

0

Сучасні бібліотеки лінійної алгебри включають оптимізовані версії найпоширеніших операцій. Багато з них включають динамічну розсилку процесора, яка вибирає найкращу реалізацію для апаратного забезпечення під час виконання програми (без шкоди для портативності).

Це, як правило, краща альтернатива виконанню ручної оптимізації ваших функцій за допомогою власних функцій векторних розширень. Останній прив’яже вашу реалізацію до конкретного постачальника обладнання та моделі: якщо ви вирішите перейти на інший постачальник (наприклад, Power, ARM) або на новіші розширення вектора (наприклад, AVX512), вам доведеться повторно реалізувати його, щоб отримати максимум з них.

Транспонування MKL, наприклад, включає функцію розширення BLAS imatcopy. Ви також можете знайти його в інших реалізаціях, таких як OpenBLAS:

#include <mkl.h>

void transpose( float* a, int n, int m ) {
    const char row_major = 'R';
    const char transpose = 'T';
    const float alpha = 1.0f;
    mkl_simatcopy (row_major, transpose, n, m, alpha, a, n, n);
}

Для проекту C ++ ви можете використовувати Armadillo C ++:

#include <armadillo>

void transpose( arma::mat &matrix ) {
    arma::inplace_trans(matrix);
}

0

intel mkl пропонує матриці транспонування / копіювання на місці та на місці. ось посилання на документацію . Я б порекомендував спробувати внедрення з місця, оскільки швидше десять на місці та в документації останньої версії mkl містить деякі помилки.


-1

Я думаю, що найшвидший спосіб не повинен займати більше O (n ^ 2), також таким чином ви можете використовувати просто O (1) пробіл:
спосіб зробити це - поміняти місцями в парах, тому що коли ви транспонуєте матрицю, то те, що ви do is: M [i] [j] = M [j] [i], тому зберігайте M [i] [j] у температурі, тоді M [i] [j] = M [j] [i], і останній крок: M [j] [i] = темп. це може бути зроблено за один прохід, тому він повинен мати O (n ^ 2)


2
M [i] [j] = M [j] [i] працюватиме лише в тому випадку, якщо це буде квадратна матриця; в іншому випадку це призведе до винятку індексу.
Антоні Томас,

-6

моя відповідь транспонована з матриці 3x3

 #include<iostream.h>

#include<math.h>


main()
{
int a[3][3];
int b[3];
cout<<"You must give us an array 3x3 and then we will give you Transposed it "<<endl;
for(int i=0;i<3;i++)
{
    for(int j=0;j<3;j++)
{
cout<<"Enter a["<<i<<"]["<<j<<"]: ";

cin>>a[i][j];

}

}
cout<<"Matrix you entered is :"<<endl;

 for (int e = 0 ; e < 3 ; e++ )

{
    for ( int f = 0 ; f < 3 ; f++ )

        cout << a[e][f] << "\t";


    cout << endl;

    }

 cout<<"\nTransposed of matrix you entered is :"<<endl;
 for (int c = 0 ; c < 3 ; c++ )
{
    for ( int d = 0 ; d < 3 ; d++ )
        cout << a[d][c] << "\t";

    cout << endl;
    }

return 0;
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.