Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Алгоритм Штрассена

.pdf
Скачиваний:
100
Добавлен:
15.04.2015
Размер:
543.74 Кб
Скачать

Алгоритм Штрассена.

Содержание

1

Вступление и постановка задачи

2

2

Алгоритм Штрассена

2

3

Умножение матриц по определению, встроенная функция

 

 

matmul и оптимизация

4

4

Реализация алгоритма

8

1

1Вступление и постановка задачи

Одна из главных задач данной работы - реализовать алгоритм Штрассена. Это рекурсивный алгоритм для умножения матриц, который позволяет значительно ускорить процесс умножения. Надо заметить, что данный алгоритм эффективен только для матриц достаточно большого размера. Определить размер матриц, с которого имеет смысл начинать умножать по алгоритму Штрассена, можно экспериментальным путем. Кроме того, я уже упоминала, что алгоритм является рекурсивным. В нерекурсивной ветви алгоритма используется "простое"умножение матриц, т.е. умножение по определению. Сделать нерекурсивную ветвь наиболее эффективной и организовать выход из рекурсии в нужный момент - еще одна задача моей работы.

Итак, наметим цели работы:

Реализовать алгоритм Штрассена;

Экспериментально найти размер матрицы, для которого следует переходить на умножение по алгоритму;

Организовать выход из рекурсии в нужный момент;

Сделать не рекурсивную ветвь наиболее эффективной.

2Алгоритм Штрассена

Самая затратная по времени операция - умножение. Т.е. чтобы ускорить процесс, нужно сократить их число. Штрассен придумал алгоритм [1, 2, 3], который позволяет избежать одного умножения. Замечательно, что формулы не требуют коммутативности сложения, т.е. они приминимы и для матриц, что позволяет применять алгоритм рекурсивно.

Алгоритм работает с квадратными матрицами размера 2n. Заметим, что для умножения матриц такого размера требуется 23n умножений. Если же матрицы не квадратные, то можно расширить их до нужного размера, заполнив недостающие строки и столбцы нулями. Допустим, есть две матрицы A и B размера 2n, которые требуется перемножить: C = AB. Разобьем каждую матрицу на четыре подматрицы:

A2;1

A2;2

B2;1

B2;2

=

C2;1

C2;2

 

A1;1

A1;2

B1;1

B1;2

 

C1;1

C1;2

 

2

Тогда матрицы Ai;j и Bi;j имеют размер 2n 1. А элементы матрицы можно вычислить следующим образом:

C1;1 = A1;1B1;1 + A1;2B2;1

C1;2 = A1;1B1;2 + A1;2B2;2

C2;1 = A2;1B1;1 + A2;2B2;1

C2;2 = A2;1B1;2 + A2;2B2;2

Но в этом случае мы не уменьшили количество умножений, т.к. каждое уравнение требует двух умножений матриц размера 2n 1. А на каждом шаге рекурсии умножений получится восемь.

Штрассен ввел семь новых элементов, которые вычисляются с помощью одного умножения, а матрица C получается из них сложением или вычитанием. Определим эти семь элементов:

M1 = (A1;1 + A2;2)(B1;1 + B2;2)

M2 = (A2;1 + A2;2)B1;1

M3 = A1;1(B1;2 B2;2)

M4 = A2;2(B2;1 B1;1)

M5 = (A1;1 + A1;2)B2;2

M6 = (A2;1 A1;1)(B1;1 + B1;2)

M7 = (A1;2 A2;2)(B2;1 + B2;2)

Элементы матрицы C вычисляются по формулам:

C1;1 = M1 + M4 M5 + M7

C1;2 = M3 + M5

C2;1 = M2 + M4

C2;2 = M1 M2 + M3 + M6

Теперь на каждом этапе рекурсии требуется выполнять только семь умножений вместо восьми. Предполагается, что итерационный процесс продолжается 2n раз, до тех пор, пока матрицы Ci;j не выродятся в числа. Заметим, что для маленьких матриц алгоритм теряет свою эффективность. Также этот алгоритм не имеет большой устойчивости [3], т.е. для вещественных матриц, в случае, если нужна очень высокая точность, не

3

стоит умножать по Штрассену. Этот существенный минус легко объяснить большим количесвом сложений и вычитаний. Но для целочисленных матриц, конечно, проблемы не возникает.

Рекурсивный алгоритм Штрассена умножает две матрицы за время (n2:81) [1], тогда как простое умножение справляется с этой задачей за (n3). Для матриц большого размера(начиная примерно с 2000) эта разница существенна.

3Умножение матриц по определению, встроенная функция matmul и оптимизация

В этом разделе пойдет речь об умножении матриц без помощи алгоритма Штрассена. Прежде чем приступать непосредственно к алгоритму, нужно выяснить, нельзя ли ускорить процесс умножения с помощью других возможностей фортрана. Также в фортране есть специальная встроенная функция matmul, которая возвращает результат умножения двух матриц. Каким образом эта функция производит умножение, неизвестно. Попробуем написать код для умножения матриц по опредлению. Пусть A и B - умножаемые матрицы, а C - их произведение.

do i = 1, N

do j = 1, N C (i , j) = 0

do k = 1, N

C(i , j) = C(i , j) + A(i , k )* B(k , j)

end do

end do

end do

Назовем такой вариант def mul. Посмотрим, как быстро умножает матрицы matmul и как медленно происходит умножение по определению. В первой строке таблицы указан размер умножаемых матриц, во второй - время, за которое умножает matmul, а втретьей - время умножения по определению. Время указано в секундах, т.о. матрицы размера 2048 умножаются в первом случае - за 13 секунд, а во втором - за 15 минут!

 

size

128

256

512

1024

2048

 

matmul

4.00000019E-03

2.00009998E-02

0.21601403

1.7041068

13.596855

 

def mul

2.80019976E-02

0.28801799

6.7804241

98.874176

900.42432

Откуда возникает такая разница? В фортране обращение к элементам массива происходит по столбцам [4]. Т.е., к примеру, чтобы обратиться к первому элементу пятого столбца, компилятор сначала просчитает все впереди стоящие элементы по столбцам. Значит, имеет смысл попробовать переписать умножение так, чтобы индекс столбца менялся быстрее,

4

чем индекс строки. Для этого надо поменять внешний и внутренний циклы местами, а средний оставить на месте:

do k = 1, N

do j = 1, N

do i = 1, N

C(i , j) = C(i , j) + A(i , k )* B(k , j)

end do

end do

end do

Теперь к предыдущей таблице добавим результат получившейся программы, назовем ее def mul tr.

 

size

128

256

512

1024

2048

 

matul

4.00000019E-03

2.00009998E-02

0.21601403

1.7041068

13.596855

 

def mul tr

4.00019996E-02

0.26401600

2.1721358

17.385088

138.56866

 

def mul

2.80019976E-02

0.28801799

6.7804241

98.874176

900.42432

Нетрудно догадаться, что транпонировав одну из матриц, можно перемножать только столбцы, что еще сократит время умножения. Для транспонирования можно воспользоваться встроенной функцией transpose. Далее можно избавившись от одного цикла. Это позволяет сделать возможным описание сечений (в данном случае - столбцов) и использование встроенной функции сложения массивов sum. Тогда умножение двух матриц приобретёт вид:

A = transpose (A) do i = 1, N

do j = 1, N

C(j , i) = sum (A(: , j )* B(: , i ))

end do

end do

Посмотрим, на что способен такой вариант(его название - two loops mul):

 

size

128

256

512

1024

2048

 

matmul

4.00000019E-03

2.00009998E-02

0.21601403

1.7041068

13.596855

 

two loops mul

8.00000131E-03

5.60030043E-02

0.49202991

3.6642284

29.393829

 

def mul tr

4.00019996E-02

0.26401600

2.1721358

17.385088

138.56866

 

def mul

2.80019976E-02

0.28801799

6.7804241

98.874176

900.42432

Очевидно, что последний вариант - самый быстрый из рассмотренных, но он уступает matmul. Для наглядности ниже представлен график.

5

Чтобы усовершенствовать наш метод, нужно обратить внимание на компилятор. Что вообще такое компилятор? Это программа, которая переводит текст с языка программирования высокого уровня(в данном случае на Фортране) на язык машинного уровня. Компилятор позволяет осуществить связь между тем, что пишет программист, и процессором компьютера. Я пользуюсь компилятором GF ortran из GNU Compiler Collection, который позволяет оптимизировать компиляцию[5]. Существует несколько уровней оптимизации: O1; O2; O3; Ofast. Каждый имеет свои особенности, которые я не буду подробно описывать. Для умножения матриц и алгоритма Штрассена лучше всего подходит уровень Ofast, т.к. он включает в себя наибольшее количество опций и является самым быстрым. Также можно использовать опцииmarch = native и mtune = native [6], которые генерируют код для указанной архитектуры процессора. На место native компилятор сам ставит значение, соответствующее данному процессору.

До этого момента я использовала стандартную компиляцию:

gfortran -o test_mul test_mul . f90

А теперь при компиляции добавлю вышеописанные опции. Тогда компиляция будет выглядеть так:

gfortran -o test_mul - Ofast - mtune = native - march = native test_mul . f90

6

Посмотрим, как изменятся результаты:

 

size

128

256

512

1024

2048

 

two loops mul

0.0000000

8.00000131E-03

9.20059681E-02

0.69604301

5.7083435

 

def mul tr

0.0000000

1.20010078E-02

0.16401005

1.2800789

9.8126221

 

matmul

4.00000019E-03

2.40010004E-02

0.21201299

1.6761041

13.372837

 

def mul

1.20009994E-02

8.80059898E-02

1.9721229

22.437403

220.77379

7

Matmul занял на этот раз третье место, и время его работы почти не изменилось. Это объясняется тем, что эта встроенная функция уже скомпилирована, а вот написанные мной программы - нет, и на них опции компиляции влияют очень сильно. Теперь первое место в таблице занимает умножение с двумя циклами, второе место досталось умножению по столбцам, а умножение по определению занимает почётное последнее место. Последние результаты показывают, что преуспеть можно и без алгоритма Штрассена, а лишь используя знания о том, как устроен язык программирования. Опции компилятора Ofast; mtune = native; march = native сыграли решающую роль в оптимизации, что говорит о том, насколько важно собирать программу под конкретный процессор. В дальнейшем я всегда буду использовать эти три опции. Самое быстрое умножение с двумя циклами лучше всего подойдет для нерекурсивной ветви алгоритма Штрассена, так что часть задачи уже решена.

4Реализация алгоритма

В ходе своей работы я придумала несколько вариантов реализации алгоритма Штрассена. Хотя эти варианты очень похожи, каждый их них имеет свои преимущества и недостатки. Так как алгоритм работает только с квадратными матрицами, размер которых два в натуральной степени, то

8

все остальные матрицы нужно пополнить до нужного размера нулевыми строками и столбцами. Как это будет реализовано непосредственно на языке программирования, не имеет значения. После совершения этой операции мы можем иметь дело с матрицами любого размера. Написать рекурсивную подпрограмму, выполняющую алгоритм Штрассена, не очень сложно: для этого надо просто воспроизвести все то, что описано в соответствующем разделе, на языке программирования. Пусть есть две матрицы A и B, заполненные случайными числами, которые требуется перемножить, и пусть эти матрицы уже имеет размер 2n, а матрица C - результат умножения. В ходе алгоритма потребуется семь новых элементов M1 M7, которые будут вычисляться рекурсивно. Пока оставим вопрос о том, начиная с какого номера лучше выходить из рекурсии, будем умножать по алгоритму, пока матрица не станет размера 8 8.

9

recursive function strassen (A , B ,

N)

result (C)

real C(N , N), AT (N ,

N)

 

 

integer , intent ( in ) :: N

 

 

integer P , i , j ,

k

 

 

 

real , intent ( in )

:: A(N , N), B(N ,

N)

 

real , dimension (: ,:) , allocatable :: A11 , A12

,

A21 ,

A22

 

real ,

dimension (: ,:) ,

allocatable ::

B11 ,

B12

,

B21 ,

B22

 

real ,

dimension (: ,:) ,

allocatable ::

M1 ,

M2 ,

M3 , M4 ,

M5

, M6 , M7

if (N == 8) then

 

AT = transpose (A)

 

do i = 1, N

 

do j

= 1, N

 

C(j , i) = sum ( AT (: , j )* B(: , i ))

end

do

end do

 

return

 

end if

 

P=N /2

allocate ( A11 (P ,P), A12 (P ,P), A21 (P ,P), A22 (P ,P )) allocate ( B11 (P ,P), B12 (P ,P), B21 (P ,P), B22 (P ,P ))

A11

=

A

(1:P , 1: P)

A12

=

A

(1:P , P +1: N)

A21

=

A

(P +1:N , 1: P)

A22

=

A

(P +1:N , P +1: N)

B11

=

B

(1:P , 1: P)

B12

=

B

(1:P , P +1: N)

B21

=

B

(P +1:N , 1: P)

B22

=

B

(P +1:N , P +1: N)

allocate ( M1 (P ,P), M2 (P ,P), M3 (P ,P), M4 (P ,P )) allocate ( M5 (P ,P), M6 (P ,P), M7 (P ,P ))

M1

=

strassen ( A11 + A22 , B11 + B22 , P)

M2

=

strassen ( A21 + A22 , B11 , P)

M3

=

strassen (A11 , B12

- B22 , P)

M4

=

strassen (A22 , B21

- B11 , P)

M5

=

strassen ( A11

+

A12 ,

B22 ,

P)

M6

=

strassen ( A21

-

A11 ,

B11

+ B12 , P)

M7

=

strassen ( A12

-

A22 ,

B21

+ B22 , P)

deallocate (A11 , A12 , A21 , A22 , B11 , B12 , B21 , B22 )

C

(1:P ,

1: P) = M1

+

M4

- M5 + M7

C

(1:P ,

P +1: N)

=

M3

+

M5

C

(P +1:N , 1: P)

=

M2

+

M4

C (P +1:N , P +1: N) = M1 - M2

+ M3 + M6

deallocate (M1 , M2 , M3 , M4 ,

M5 , M6 , M7 )

end function

 

Разберем тут же еще один вариант. Этот вариант не предполагает объявления семи матриц M1 M7, и тогда четыре блока матрицы C можно найти следующим образом:

10