الگوریتم‌های ضرب ماتریس

مسئله حل نشده علوم کامپیوتر: سریع‌ترین الگوریتم برای ضرب ماتریس چیست؟
ضرب ماتریس یکی از اعمال پایه در بسیاری از الگوریتم‌های آنالیز عددی محسوب می‌شود به همین دلیل در راستای بهبود زمان آن تلاش‌های بسیاری انجام شده‌است. کاربردهای ضرب ماتریس در بسیاری از زمینه‌های مختلف همچون علم محاسبه و بازشناخت الگو و حتی زمینه‌های به ظاهر بی‌ربط مانند شمردن تعداد گشت‌ها در یک گراف دیده می‌شود. الگوریتم‌های بسیاری برای این‌کار روی سیستم‌های رایانش موازی طراحی شده‌است که در آن چند هسته به صورت همزمان و موازی عملیات را انجام می‌دهند.
اگر از تعریف ضرب ماتریس به صورت مستقیم استفاده کنیم برای ضرب دو ماتریس n × n زمان n³ طول خواهد کشید که به صورت Θ(n³) می‌توانیم نمایش دهیم. الگوریتم‌هایی با زمان اجرای بهتری برای این‌کار ارائه شده‌اند. برای مثال در دههٔ ۱۹۶۰ استراسن در این زمینه الگوریتم‌هایی ارائه داد اما به‌طور کلی هنوز نمی‌دانیم بهترین الگوریتم برای این کار چیست. (در واقع پیچیدگی زمانی آن مشخص نیست)

الگوریتم پیمایشی

طبق تعریف ضرب ماتریس می‌توانیم یک الگوریتم برای این کار ارائه دهیم. به ازای ماتریس

که یک ماتریس

است و ماتریس

که یک ماتریس

است می‌خواهیم ماتریس

که ماتریسی

است را محسابه کنیم. طبق تعریف ضرب ماتریس

بنابراین به ازای هر دوتایی

می‌توانیم با

مقدار

را محاسبه کنیم. یعنی در کل الگوریتمی با زمان اجرای

خواهیم داشت. اگر فرض کنیم هر سه ماتریس

هستند آنگاه الگوریتم

خواهد بود.
1 Input: matrices A and B
2 Let C be a new matrix of the appropriate size
3 For i from 1 to n:
4 For j from 1 to p:
5 Let sum = 0
6 For k from 1 to m:
7 Set sum ← sum + Aᵢₖ × Bₖⱼ
8 Set Cᵢⱼ ← sum
9 Return C

بررسی رفتار حافظه نهان (کش)

در الگوریتم بالا ترتیب حلقه‌ها را می‌توانیم جابه‌جا کنیم. اگر چه این جابه‌جایی در مدت زمان اجرای الگوریتم تأثیری نخواهد داشت اما این ترتیب در بحث‌های مربوط به دسترسی حافظه (access pattern) و مسائل مربوط به استفاده از حافظه نهان پردازنده مهم است. مثلاً اینکه ماتریس‌ها به ترتیب سطری یا ستونی (یا ترکیبی از این دو) ذخیره شوند در زمان حافظهٔ نهان پردازنده تأثیر گذار خواهد بود.
حتی اگر حالت بهینه را در نظر بگیریم که حافظهٔ کش شرکت پذیر کامل با

سطر حافظهٔ

بیتی باشد و ماتریس‌های

و

به صورت سطری ذخیره شده‌باشند، این الگوریتم بهینه نخواهد بود. هنگامی که

از آنجایی که ماتریس‌ها به صورت سطری ذخیره شده‌اند هر پیمایش حلقهٔ داخلی در الگوریتم (یک پیمایش روی سطر ماتریس اول و ستون ماتریس دوم) یک خطای کش به هنگام دسترسی به خانه‌ی‌های ماتریس دوم به همراه خواهد داشت؛ و این به این معناست که الگوریتم در بدترین حالت حاوی

خطای کش خواهد بود. امروزه -یعنی از سال ۲۰۱۰- خطای کش‌ها به نسبت اعمال پردازنده تأثیر بیشتری روی زمان اجرا می‌گذارند بنابراین بهتر است با روشی این خطای کش‌ها را کاهش دهیم.
برای حل این مشکل ماتریس‌ها را به بلوک‌هایی از اردر

تایی تقسیم می‌کنیم. با اینکار کل یک زیرجدول در حافظهٔ کش قرار می‌گیرد و این بلوک‌ها در هم ضرب می‌شوند. ضرب هر بلوکی هیچ خطای کشی به همراه نخواهد داشت.
01 Input: matrices A and B
02 Let C be a new matrix of the appropriate size
03 Pick a tile size T = Θ(√M)
04 For I from 1 to n in steps of T:
05 For J from 1 to p in steps of T:
06 For K from 1 to m in steps of T:
07 Multiply AI:I₊T, K:K₊T and BK:K₊T, J:J₊T into CI:I₊T, J:J₊T, that is:
08 For i from I to min(I + T, n):
09 For j from J to min(J + T, p):
10 Let sum = 0
11 For k from K to min(K + T, m):
12 Set sum ← sum + Aᵢₖ × Bₖⱼ
13 Set Cᵢⱼ ← Cᵢⱼ + sum
14 Return C
تعداد خطاهای کش در این مدل برابر

خواهدبود. مخرج

باعث می‌شود در پردازنده‌های جدید خطاهای کش در زمان اجرا تأثیر گذار نشوند و صرفاً تحلیل زمانی الگوریتم تأثیر گذار باشد.

ماتریس‌های غیر مربعی

این الگوریتم در عمل برای ماتریس‌های غیر مربعی سریع‌تر عمل می‌کند. کافیست به جای تقسیم کردن هر دو ماتریس به چهار تکه یکی از ماتریس‌ها را به دو تکهٔ برابر (یا تقریباً برابر) با تقسیم کردن سطرها یا ستون‌ها تقسیم کنیم. در زیر می‌توانید الگوریتم چنین کاری را ببینید:
 Inputs: matrices A of size n × m, B of size m × p.
 Base case: if max(n, m, p) is below some threshold, use an unrolled version ofthe iterative algorithm.
 Recursive cases:
 If max(n, m, p) = n, split A horizontally:
       
 Else, If max(n, m, p) = p, split B vertically:
       
 Otherwise, max(n, m, p) = m. Split A vertically and B horizontally:

الگوریتم‌های بهتر از 
  
    
      
        Θ
        (
        
          n
          
            3
          
        
        )
      
    
    {\displaystyle \Theta (n^{3})}

الگوریتم‌هایی وجود دارند که زمان اجرای بهتری از الگوریتم‌های فوق دارند. اولین الگوریتم کشف شده که اینگونه بود الگوریتم استراسن بود که در سال ۱۹۶۹ توسط وولکر استراسن (Volker Strassen) کشف شد. این الگوریتم به «الگوریتم سریع ضرب ماتریس» نیز معروف است. این الگوریتم بر مبنای ضرب دو ماتریس

با ۷ عملیات ضرب است که در عوض تعداد بیشتری جمع و عملیات ریاضی این‌چنینی لازم دارد. با استفاده از این ایده به‌صورت بازگشتی الگوریتمی از

به ما می‌دهد. این الگوریتم پیچیده‌است و ضرایب ثابت آن در تحلیل زمانی به اندازه‌ای زیاد است که تنها برای ماتریس‌های بزرگ کارامدتر از الگوریتم‌های قبلی عمل خواهد کرد.
سریعترین الگوریتم با

الگوریتمی است که از تعمیم الگوریتم کوپراسمیت–وینوگارد به‌دست آمده و از نظر زمانی

می‌باشد. این الگوریتم توسط François Le Gall کشف شد و به قدری ضرایب زیادی دارد و سربار الگوریتم بالاست که تنها برای ماتریس‌های بسیار بزرگی که هم‌اکنون در علوم کامپیوتر کاربردی ندارند، کارامد خواهد بود.
با توجه به این‌که باید حداقل روی همهٔ اعضای دو ماتریس

پیمایش انجام بشود کران‌پایین

برای الگوریتم‌های ضرب ماتریس وجود دارد. راز (Raz) ثابت کرد که کران پایین

نیز برای هر الگوریتم ضرب ماتریس نیز وجود دارد.
همچنین الگوریتم الگوریتم فریوالد یک الگوریتم احتمالی و مونت کارلو است که در

چک می‌کند که آیا ضرب دو ماتریس

برابر

هست یا نه.