TRƯỜNG ĐẠI HỌC CÔNG NGHIỆP HÀ NỘI
KHOA CÔNG NGHỆ THÔNG TIN
···🙞🙞🙞🙞🙞···
BÁO CÁO BTL THUỘC HỌC PHẦN:
TÍNH TỐN HIỆU NĂNG CAO
ĐÁNH GIÁ HIỆU NĂNG THUẬT TOÁN FFT (FAST FOURIER
TRANSFORM) SỬ DỤNG MPI
Giáo viên hướng dẫn
:
Ts. Hà Mạnh Đào
Nhóm – Lớp
:
12 - 20231IT6069001
Tên thành viên
:
Nguyễn Quang Vũ
- 2021608053
Lê Đăng Nghĩa
- 2021601694
Nguyễn Trung Hiếu
- 2021604011
LỜI MỞ ĐẦU
Hà Nội - 2023
Biến đổi Fourier, một trong những khái niệm quan trọng và mạnh mẽ
nhất trong toán học và kỹ thuật, đã đóng vai trị khơng thể phủ nhận trong việc
hiểu và phân tích các tín hiệu và hệ thống trong nhiều lĩnh vực khác nhau. Từ
viễn thông đến khoa học dữ liệu, từ xử lý tín hiệu đến hình ảnh y khoa. Biến đổi
Fourier đã trở thành công cụ cơ bản khơng thể thiếu để hiểu sâu hơn về các q
trình dao động và chuỗi thời gian.
Khi nghiên cứu về Biến đổi Fourier, chúng em mở ra cánh cửa tới một thế
giới rộng lớn của phép biến đổi từ miền không gian thời gian sang miền không
gian tần số và ngược lại. Phương pháp này không chỉ giúp chúng em phân tích
các tín hiệu phức tạp thành các thành phần đơn giản hơn mà cịn cung cấp cái
nhìn sâu sắc về cấu trúc và tính chất của các tín hiệu đó.
Trong bài luận này, chúng em sẽ khám phá sâu hơn về biến đổi Fourier,
tìm hiểu về nguồn gốc, cách thức hoạt động, và ứng dụng rộng rãi của nó trong
nhiều lĩnh vực khác nhau. Chúng em sẽ đi từ những khái niệm cơ bản nhất đến
những ứng dụng tiên tiến, nhấn mạnh vào tầm quan trọng và tính ứng dụng của
Biến đổi Fourier trong thế giới hiện đại.
Bài báo cáo này hy vọng sẽ giúp mọi người có cái nhìn tổng quan và chi
tiết hơn về Biến đổi Fourier, từ các khái niệm căn bản đến ứng dụng thực tế,
đồng thời khám phá sự to lớn và đa dạng của nó trong việc giải quyết nhiều vấn
đề phức tạp trong khoa học và kỹ thuật.
Trong nghiên cứu này, chúng em sẽ mơ phỏng sức mạnh của tính tốn song
song thơng qua việc giải quyết bài toán nhân ma trận và đưa ra so sánh về hiệu
suất. Mặc dù đã nỗ lực hết sức để tránh những sai sót, nhưng chúng em nhận
thức rằng có thể vẫn tồn tại những khuyết điểm nhỏ và mong nhận được sự
thông cảm.
Chúng em cũng muốn bày tỏ lòng biết ơn đặc biệt đến TS. Hà Mạnh Đào,
người đã đồng hành và hỗ trợ chúng em suốt quá trình thực hiện đề tài này.
MỤC LỤC
Chương I: TỔNG QUAN ĐỀ TÀI......................................................................4
1.1 Tên đề tài
4
1.2 Giới thiệu đề tài
5
1.3 Kiến thức cần có
5
1.4 Các kỹ năng đã có để thực hiện đề tài nghiên cứu
5
1.5 Thuật toán FFT
5
1.5.1 Định nghĩa..............................................................................................5
1.5.2 Nguyên lý...............................................................................................6
1.5.3 Các vấn đề tính tốn...............................................................................6
1.5.4 Biến đổi Fourier rời rạc..........................................................................7
1.5.5 Biến đổi Fourier nhanh...........................................................................8
1.5.6 FFT nghịch đảo......................................................................................9
1.6 MPI trong tính tốn hiệu năng
11
1.6.1 Định nghĩa............................................................................................11
1.6.2 Mơ hình lập trình..................................................................................11
1.6.3 Cấu trúc chương trình MPI..................................................................13
1.6.4 Hạn chế.................................................................................................13
1.7 Tổng quan về tính tốn hiệu năng cao
14
1.7.1 Hoàn cảnh ra đời..................................................................................14
1.7.2 Định nghĩa............................................................................................14
1.7.3 Các thách thức......................................................................................15
Chương II: KẾT QUẢ NGHIÊN CỨU............................................................16
2.1 Thuật tốn FFT ngơn ngữ tự nhiên (C++)
16
2.2 Thuật toán nhân 2 ma trận tuần tự
18
2.3 Thuật toán nhân 2 ma trận tuần tự nhưng sử dụng thuật tốn FFT
20
2.4 Nhân 2 ma trận có sử dụng FFT và OpenMPI
23
2.5 So sánh kết quả thực tế
27
Chương III: KẾT LUẬN VÀ BÀI HỌC KINH NGHIỆM............................28
3.1 Nội dung đã thực hiện
28
3.2 Kết luận
28
3.2 Hướng phát triển
29
TÀI LIỆU THAM KHẢO.................................................................................30
Chương I: TỔNG QUAN ĐỀ TÀI
1.1 Tên đề tài
Tên đề tài: Đánh giá hiệu năng thuật toán FFT (Fast Fourier Transform) sử
dụng MPI
1.2 Giới thiệu đề tài
Từ những kiến thức đã được dạy bởi thầy và đề tài được thầy giao cho.
Nhóm em đã tìm hiểu và nghiên cứu về thuật toán FFT và việc sử dụng MPI để
đánh giá hiệu năng của thuật toán. Bằng việc áp dụng các kiến thức về MPI đã
học được trên lớp cũng như tài liệu tìm được ở trên mạng. Để hồn thiện sản
phẩm mỗi thành viên trong nhóm cần có kiến thức, sự hiểu biết về thuật toán
FFT và thư viện MPI. Bài báo cáo này là thành quả của tất cả ba thành viên trong
nhóm em sau khi tìm hiểu và nghiên cứu về việc đánh giá hiệu năng thuật toán
FFT khi sử dụng MPI.
1.3 Kiến thức cần có
● Kiến thức về thuật tốn FFT
● Kiến thức về MPI
● Lập trình chương trình bằng ứng dụng Visual Studio
● Khả năng phân tích dữ liệu đầu ra
1.4 Các kỹ năng đã có để thực hiện đề tài nghiên cứu
● Kỹ năng tổng hợp kiến thức: tổng hợp các kiến thức đã học được từ trên
lớp cũng như trên mạng để hoàn thiện bài tập lớn.
● Khả năng đọc hiểu các tài liệu bằng tiếng Anh
● Kỹ năng hoạt động nhóm: phân chia cơng việc phù hợp, thảo luận nhóm
để hồn thành cơng việc
● Kỹ năng viết báo cáo
1.5 Thuật toán FFT
1.5.1 Định nghĩa
- Biến đổi Fourier nhanh (FFT) là một thuật toán rất hiệu quả để tính
tốn Biến đổi Fourier rời rạc (DFT) và Biến đổi ngược. Có nhiều loại thuật tốn
FFT khác nhau sử dụng các kiến thức từ nhiều mảng khác nhau của tốn học,
từ số phức tới lý thuyết nhóm và lý thuyết số.
- Biến đổi Fourier nhanh FFT thực chất là các thuật toán để thực hiện phép
biến đổi Fourier rời rạc DFT nhằm phân tích tín hiệu rời rạc theo thời gian
trong miền tần số hiệu quả nhờ sự cải thiện rất đáng kể về tốc độ và tính toán
đơn giản.
1.5.2 Nguyên lý
- Các thuật toán FFT tập trung vào việc giải quyết độ phức tạp trong tính
tốn của DFT bằng cách làm giảm một cách đáng kể thời gian tính tốn và số
lần thực hiện phép tốn, đặc biệt là đối với các phép toán chiếm nhiều thời
gian thực hiện như phép nhân. Các thuật toán này được sử dụng càng hiệu
quả khi kích thước N của DFT có giá trị càng lớn. Ngồi các thuật tốn nhanh
ban đầu, cho đến nay cũng xuất hiện thêm một số thuật tốn mới hay được sử
dụng.
- Tính DFT của N điểm trực tiếp theo định nghĩa đòi hỏi O(N2) phép tính,
trong khi FFT tính ra cùng kết quả đó trong O(N log N) phép tính.
1.5.3 Các vấn đề tính tốn
- Một bài tốn mở quan trọng về mặt lý thuyết là chứng minh chặn dưới cho
độ phức tạp tính tốn và số phép tính của biến đổi Fourier nhanh. Hiện vẫn
chưa có chứng minh nào cho việc DFT có thực sự địi hỏi Ω(N log N) phép tính,
ngay cả trong trường hợp kích thước N là lũy thừa của hai, mặc dù khơng có
thuật tốn nào có độ phức tạp thấp hơn. Chú ý rằng tuy số phép tính thường là
quan tâm chính về mặt lý thuyết, trên thực tế, tốc độ thực thi phụ thuộc nhiều
yếu tố khác như các tối ưu hóa cho bộ nhớ đệm và ống lệnh CPU (CPU pipes)
1.5.4 Biến đổi Fourier rời rạcn đổi Fourier rời rạci Fourier rời rạci rạcc
Cho đa thức có bậc n - 1 :
Khơng mất tính tổng quát, chúng ta giả sử rằng n - số hệ số - là lũy thừa
của 2 . Nếu n khơng phải là lũy thừa của 2 , thì chúng ta chỉ cần cộng các số hạng
còn thiếu a i x i và đặt các hệ số a i thành 0 .
có n
Lý thuyết về số phức cho chúng ta biết rằng phương trình
n
nghiệm phức (gọi là căn bậc ), và nghiệm có dạng
với
.
Ngồi ra, những số phức này có một số tính chất rất thú vị: ví dụ: gốc n - gốc thứ
n
có thể được sử dụng để mô tả tất cả các gốc thứ khác:
.
Biến đổi Fourier rời rạc (DFT) của đa thức
vectơ của các hệ số
tại điểm
, tức là nó là vectơ:
(hoặc tương đương là
được định nghĩa là các giá trị của đa thức
Tương tự, phép biến đổi Fourier rời rạc ngược được định nghĩa: DFT
nghịch đảo của các giá trị của đa thức
là các hệ số của đa thức
.
Do đó, nếu một DFT trực tiếp tính tốn các giá trị của đa thức tại các
điểm ở nghiệm thứ n , thì DFT nghịch đảo có thể khơi phục các hệ số của đa
thức bằng cách sử dụng các giá trị đó.
1.5.5 Biến đổi Fourier nhanh
Biến đổi Fourier nhanh là một phương pháp cho phép tính tốn DFT
trong
thời gian. Ý tưởng cơ bản của FFT là áp dụng chia để trị. Chúng
ta chia vectơ hệ số của đa thức thành hai vectơ, tính tốn đệ quy DFT cho từng
vectơ và kết hợp các kết quả để tính tốn DFT của đa thức đầy đủ.
Vì vậy, giả sử có một đa thức A(x) với bậc n - 1 , trong đó n là lũy thừa
của 2 , và n > 1 :
Ta chia nó thành hai đa thức nhỏ hơn, một đa thức chỉ chứa các hệ số của
các vị trí chẵn và một đa thức chứa các hệ số của các vị trí lẻ:
Dễ thấy rằng
Các đa thức A0 và A1 chỉ bằng một nửa hệ số của đa thức A . Nếu chúng
ta có thể tính DFT ( A) trong thời gian tuyến tính bằng cách sử dụng DFT ( A0 ) và
DFT ( A1 )
, thì chúng ta sẽ nhận được phép lặp
cho
độ phức tạp về thời gian, kết quả là
Giả sử chúng ta đã tính các vectơ
theo master theorem.
và
Chúng ta hãy tìm một biểu thức cho $
.
.
n
Đối với 2 giá trị đầu tiên, chúng ta chỉ có thể sử dụng phương trình đã lưu ý
trước đó
:
n
Tuy nhiên, đối với 2 giá trị thứ hai, chúng ta cần tìm một biểu thức khác một
chút:
Ở đây chúng ta sử dụng lại
và hai định danh
và
.
Do đó, ý nhận được các cơng thức mong muốn để tính tốn tồn bộ vectơ ( y k ) :
1.5.6 FFT nghịch đảo
Cho vectơ
điểm
- các giá trị của đa thức A bậc n−1 tại các
- được cho trước. Chúng ta muốn khôi phục các hệ số
của đa thức. Vấn đề đã biết này được gọi là phép nội suy và có
các thuật tốn chung để giải quyết vấn đề này. Nhưng trong trường hợp đặc
biệt này (vì chúng ta biết giá trị của các điểm tại gốc của sự thống nhất), chúng
ta có thể thu được một thuật toán đơn giản hơn nhiều (thực tế giống như thuật
tốn FFT trực tiếp).
Chúng ta có thể viết DFT, theo định nghĩa của nó, ở dạng ma trận:
Ma trận này được gọi là ma trận Vandermonde.
Do đó, chúng ta có thể tính vectơ
bằng cách nhân vectơ
từ bên trái với nghịch đảo của ma trận:
Kiểm tra nhanh có thể xác minh rằng ma trận nghịch đảo có dạng sau:
Do đó, ta có được cơng thức:
So sánh điều này với công thức cho y k :
Ta nhận thấy rằng những vấn đề này gần như giống nhau, vì vậy các hệ số a k có
thể được tìm thấy bằng cùng một thuật toán chia để trị, cũng như FFT trực tiếp,
chỉ thay vì w kn chúng ta phải sử dụng w−k
n , và cuối cùng, chúng ta cần chia các hệ
số kết quả cho n.
1.6 MPI trong tính toán hiệu năng
1.6.1 Định nghĩa
- MPI (message passing interace) là một giao diện lập trình ứng dụng
(Application Programming Interface - API) được phát triển từ khá lâu. MPI
cung cấp một thư viện căn bản nhất cho việc tổ chức một chương trình song
song theo kiểu đa tiến trình. Chúng bao gồm các câu lệnh để khởi tạo môi
trường MPI, phân chia các xử lý vào các đoạn mã song song, mỗi phần chạy
trên một tiến trình (process); các câu lệnh để trao đổi dữ liệu giữa các tiến
trình; các câu lệnh đồng bộ hóa các tiến trình...
- Mơ hình xử lý thường được dùng là một tiến trình chính (master) và nhiều
tiến trình phụ (slave). Thơng thường, tiến trình chính tại máy (hoặc CPU)
ngun thủy ban đầu, làm nhiệm vụ khởi tạo chương trình, phân chia cơng
việc và nhận kết quả tính tốn từ các tiến trình phụ. Các tiến trình phụ chạy ở
các máy (hoặc CPU) phụ, mỗi tiến trình nhận dữ liệu ban đầu tiến trình chính,
thực hiện việc tính tốn được giao và gửi kết quả về tiến trình chính.
1.6.2 Mơ hình lập trình
- Ban đầu MPI được thiết kế cho các kiến trúc bộ nhớ phân tán, kiến trúc rất
phổ biến thời kỳ 1980 đến đầu năm 1990.
- Xu hướng công nghệ thay đổi, bộ nhớ chia sẻ kết hợp với mạng máy tính tạo
ra dạng lai của hai hệ thống bộ nhớ chia sẻ và bộ nhớ phân tán.
- Thực thi MPI tương thích với cả hai kiểu kiến trúc trên và cũng tương thích
với các kiểu kết nối và giao thức khác nhau.
- Ngày nay MPI có thể chạy trên hầu hết các nền tảng phần cứng:
● Bộ nhớ chia sẻ
● Bộ nhớ phân tán
● Dạng lai 2 loại trên
1.6.3 Cấu trúc chương trình MPI
1.6.4 Hạn chế
- Tuy có khả năng cung cấp hiệu năng cao và có thể chạy trên cả các kiến
trúc sử dụng bộ nhớ chia sẻ lẫn phân tán, nhưng các chương trình MPI địi hỏi
người lập trình phải tự phân chia chương trình thành các khối cho tiến trình
chính và phụ bằng các câu lệnh 2 tường minh. Các công việc khác như gửi và
nhận dữ liệu, đồng bộ hóa tiến trình… cũng cần được chỉ rõ bằng các câu lệnh
và tham số cụ thể. Điều này gây khó khăn và địi hỏi nhiều cơng sức đối với
người lập trình, thể hiện qua ba khía cạnh lớn. Thứ nhất là MPI đòi hỏi phải tổ
chức ngay từ đầu chương trình thành theo cấu trúc song song trên mơ hình
master-slave, một việc phức tạp hơn nhiều so với việc tổ chức chương trình
tuần tự vốn quen thuộc với tư duy của đa số lập trình viên.
- Thứ hai là nếu MPI được sử dụng để song song hố các chương trình tuần
tự thì vì nó phá vỡ nghiêm trọng cấu trúc ban đầu của chương trình. Điều khó
khăn thứ ba gây nên bởi sự rắc rối của các câu lệnh MPI, với một loạt các tham
số khá khó hiểu cho mỗi câu lệnh. Do ba khó khăn vừa nêu, tuy có hiệu năng
cao và đã cung cấp một cách thức lập trình trừu tượng hóa nhưng MPI vẫn
được xem là assembler của việc lập trình song song.
1.7 Tổng quan về tính tốn hiệu năng cao
1.7.1 Hồn cảnh ra đời
- Các bài tốn phức tạp và địi hỏi sự tính tốn lớn ln được tìm thấy trong
các ngành khoa học như hóa học, vật lý hạt nhân, vũ trụ học, cơ lưu chất, sinh
học, y học... Một số bài tốn có độ phức tạp khá lớn nên được gọi là “các bài
toán thách đố” (Grand Challenge Problems). Các bài toán thách đố này không
thể giải được trong một khoảng thời gian chấp nhận nếu sử dụng các loại máy
tính hiện có ngày nay. Vấn đề càng phức tạp khi bài tốn địi hỏi việc mơ
phỏng và hiển thị mơ hình đồ họa. Mặt khác yêu cầu về thời gian xử lý của bài
toán cũng quan trọng khơng kém. Ví dụ một bài tốn dự báo thời tiết được
giải trong 2 ngày để có thể cho kết quả dự báo thời tiết trong 1 ngày là khơng
có ý nghĩa. Chính u cầu về tính tốn phức tạp này đã thúc đẩy các máy tính
song song có khả năng tính tốn mạnh ra đời.
1.7.2 Định nghĩa
- Tính tốn hiệu năng cao (High Performance Computing - HPC) trên cơ sở
các hệ thống siêu máy tính là một trong những thành phần cốt lõi trong sự
phát triển đó và nó đã đánh dấu cho sự thay đổi trong bước chuyển hóa về
nghiên cứu dựa trên những cơng cụ tính tốn lớn. HPC được coi là một siêu
máy tính tập hợp bởi nhiều máy chủ với sự kết hợp về sức mạnh tính tốn
giúp hiệu năng có sự vượt trội hơn rất nhiều so với một chiếc máy tính truyền
thống.
1.7.3 Các thách thức
- Sự địi hỏi về sức mạnh tính tốn của máy tính khơng ngừng tăng lên nhằm
để giải quyết các bài toán trong lĩnh vực khoa học và công nghệ. Các vấn đề
trong nhiều lĩnh vực đa số đều đưa về mơ hình số và mơ phỏng. Tùy theo độ
lớn của bài tốn cần giải quyết hay độ chính xác của kết quả bài tốn mà khối
lượng tính tốn sẽ lớn đến mức nào. Ví dụ trong bài toán dự báo thời tiết bằng
phương pháp số, để có kết quả dự báo chính xác thì ta phải giải bài tốn trên
một khơng gian rộng hơn và như thế số lượng phép tính cũng nhiều hơn.
- u cầu về thời gian tính tốn cũng là một yếu tố cần sức mạnh tính tốn
của máy tính. Điều này rất thường thấy trong các qui trình sản xuất trong cơng
nghiệp. Trong một qui trình sản xuất sự phối hợp hoạt động của các bộ phận
rất quan trọng. Vì vậy việc tính tốn chậm tại một bộ phận sẽ làm ảnh hưởng
các bộ phận khác. Bài toán dự báo thời tiết trong lĩnh vực khoa học cũng đòi
hỏi về thời gian tính tốn. Để có kết quả dự báo chính xác, ta khơng thể chạy
chương trình trong hai ngày chỉ để dự báo thời tiết cho ngày hôm sau.
- Trong thực tế cịn nhiều bài tốn có độ phức tạp rất lớn mà sức mạnh siêu
máy tính hiện tại cũng chưa giải quyết được. Vì vậy người ta gọi đây là các
thách đố. Hiện nay có rất nhiều lĩnh vực khoa học cần khoa học tính tốn như
hóa học, vật lý hạt nhân, vũ trụ học, cơ lưu chất, sinh học, y học... Khoa học
tính tốn giúp giải quyết nhiều vấn đề trên mơ hình tốn để tiên đốn trước
kết quả thử nghiệm và như vậy giúp rút ngắn q trình thử nghiệm. Nhiều bài
tốn cần giải quyết trên mơi trường tính tốn lưới như trong càng cho thấy sự
cần thiết của sức mạnh tính tốn trong các lĩnh vực khoa học và công nghệ.
Chương II: KẾT QUẢ NGHIÊN CỨU
2.1 Thuật tốn FFT ngơn ngữ tự nhiên (C++)
* Thuật toán:
#include <complex>
#include <iostream>
#include <valarray>
const double PI = 3.141592653589793238460;
typedef std::complex<double> Complex;
typedef std::valarray<Complex> CArray;
// Hàm thực hiện FFT
void fft(CArray& x)
{
const size_t N = x.size();
if (N <= 1) return;
// Chia dãy số thành 2 phần bằng nhau
CArray even = x[std::slice(0, N / 2, 2)];
CArray odd = x[std::slice(1, N / 2, 2)];
// Đệ quy FFT cho cả 2 phần
fft(even);
fft(odd);
// Tính tốn các hệ số biến đổi
for (size_t k = 0; k < N / 2; ++k)
{
Complex t = std::polar(1.0, -2 * PI * k / N) * odd[k];
x[k] = even[k] + t;
x[k + N / 2] = even[k] - t;
}
}
// Hàm thực hiện IFFT (inverse FFT)
void ifft(CArray& x)
{
// Đảo ngược dãy số và áp dụng FFT
x = x.apply(std::conj);
fft(x);
x = x.apply(std::conj);
// Chia tất cả các phần tử cho N
x /= x.size();
}
int main()
{
// Số lượng phần tử trong dãy số
const size_t N = 4;
// Dãy số ban đầu
Complex test[N] = { 1.0, 1.0, 1.0, 1.0};
CArray data(test, N);
// Thực hiện FFT và in kết quả ra màn hình
fft(data);
std::cout << "FFT = " << std::endl;
for (size_t i = 0; i < N; ++i)
{
std::cout << data[i] << std::endl;
}
// Thực hiện IFFT và in kết quả ra màn hình
ifft(data);
std::cout << "IFFT = " << std::endl;
for (size_t i = 0; i < N; ++i)
{
std::cout << data[i] << std::endl;
}
return 0;
}
* Kết quả chạy chương trình:
● IFFT:
IFFT (Inverse Fast Fourier Transform) là thuật tốn tính tốn ngược lại của
FFT. Nó dùng để tính tốn hàm ban đầu từ dãy các hệ số biến đổi đã được
tính bằng FFT. IFFT là thuật tốn rất hữu ích trong các trường hợp cần tính
tốn lại hàm ban đầu từ dãy các hệ số biến đổi.
2.2 Thuật toán nhân 2 ma trận tuần tự
* Thuật toán:
#include <iostream>
#include <time.h>
#include <cstdlib>
using namespace std;
void random_matrix(int& row, int& col, int**& matrix) {
matrix = new int* [row];
for (int i = 0; i < row; ++i)
matrix[i] = new int[col];
for (int i = 0; i < row; ++i)
for (int j = 0; j < col; j++)
matrix[i][j] = rand() % 30 + 1;
}
void display(int row, int col, int** matrix) {
for (int i = 0; i < row; ++i) {
for (int j = 0; j < col; ++j)
cout << matrix[i][j] << " ";
cout << endl;
}
}
int** multiply(int** m1, int** m2, int row_m1, int col_m1, int row_m2, int
col_m2) {
if (col_m1 != row_m2) return 0;
int** c = new int* [row_m1];
for (int i = 0; i < row_m1; ++i)
c[i] = new int[col_m2];
for (int i = 0; i < row_m1; ++i)
for (int j = 0; j < col_m2; ++j) {
int t = 0;
for (int k = 0; k < col_m1; ++k)
t += m1[i][k] * m2[k][j];
c[i][j] = t;
}