Чому MATLAB настільки швидкий у матричному множенні?


190

Я роблю деякі орієнтири за допомогою CUDA, C ++, C #, Java та використовую MATLAB для перевірки та генерації матриць. Коли я виконую множення матриць за допомогою MATLAB, 2048x2048а ще більші матриці майже миттєво множуються.

             1024x1024   2048x2048   4096x4096
             ---------   ---------   ---------
CUDA C (ms)      43.11      391.05     3407.99
C++ (ms)       6137.10    64369.29   551390.93
C# (ms)       10509.00   300684.00  2527250.00
Java (ms)      9149.90    92562.28   838357.94
MATLAB (ms)      75.01      423.10     3133.90

Тільки CUDA є конкурентоспроможною, але я подумав, що принаймні C ++ буде дещо близьким і не в 60 разів повільнішим. Я також не знаю, що думати про результати C #. Алгоритм такий же , як C ++ і Java, але гігантський стрибок 2048від 1024.

Як MATLAB так швидко виконує множення матриць?

Код C ++:

float temp = 0;
timer.start();
for(int j = 0; j < rozmer; j++)
{
    for (int k = 0; k < rozmer; k++)
    {
        temp = 0;
        for (int m = 0; m < rozmer; m++)
        {
            temp = temp + matice1[j][m] * matice2[m][k];
        }
        matice3[j][k] = temp;
    }
}
timer.stop();

14
Напевно, це питання, яким алгоритмом ви користуєтесь.
Роберт Дж

24
Переконайтесь, що Matlab не кешує результатом, це хитрий звір. Спочатку переконайтеся, що розрахунок фактично виконується, а потім порівняйте.
rubenvb

27

10
Я насправді вважаю, що ця публікація справді цікава, але мені дуже хотілося б побачити більш відповідні орієнтири. Наприклад, я думаю, що Matlab R2011a автоматично використовує багатопотоковість, а матричні множення реалізуються за допомогою бібліотеки mkl / blas від Intel. Таким чином, я б здогадався, що c ++ є швидшим, якби для множення матриці використовувався виклик mkl. Тоді питання полягатиме в тому, що накладні витрати Матлаба. Я знаю, що це залежить від додаткових деталей множення матриці, але наведені вище цифри зараз досить безглузді.
Лукас

1
ви можете використовувати "алгоритм Страссена" часу роботи O (n ^ 2,81) для великого множення квадратної матриці, яке приблизно в 10 разів швидше, ніж власне множення, яке працює в O (n ^ 3). також SSE / AVX може допомогти вам швидше обійтись на 8-20 разів. всі разом у вас може бути реалізація змінного струму швидше, ніж в системі matlab.
DU Jiaen

Відповіді:


85

Ось мої результати з використанням MATLAB R2011a + Паралельний обчислювальний інструментарій на машині з Tesla C2070:

>> A = rand(1024); gA = gpuArray(A);
% warm up by executing the operations a couple of times, and then:
>> tic, C = A * A; toc
Elapsed time is 0.075396 seconds.
>> tic, gC = gA * gA; toc
Elapsed time is 0.008621 seconds.

MATLAB використовує високооптимізовані бібліотеки для матричного множення, тому просто матричне множення матриць MATLAB настільки швидко. У gpuArrayверсії використовується MAGMA .

Оновлення за допомогою R2014a на машині з Tesla K20c, а також нові timeitта gputimeitфункції:

>> A = rand(1024); gA = gpuArray(A);
>> timeit(@()A*A)
ans =
    0.0324
>> gputimeit(@()gA*gA)
ans =
    0.0022

Оновлення за допомогою R2018b на машині WIN64 з 16 фізичними ядрами і Tesla V100:

>> timeit(@()A*A)
ans =
    0.0229
>> gputimeit(@()gA*gA)
ans =
   4.8019e-04

(Примітка: у якийсь момент (я забуваю, коли саме) gpuArrayперейшов з MAGMA на cuBLAS - MAGMA все ще використовується для деяких gpuArrayоперацій)


Чому це має значення?
Божевільний фізик

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

175

Цей тип запитань повторюється, і на нього слід відповісти чіткіше, ніж "MATLAB використовує високооптимізовані бібліотеки" або "MATLAB використовує MKL" для одного разу на переповнення стека.

Історія:

Матричне множення (разом з матричним вектором, множенням векторів та багатьма матричними розкладами) є (є) найбільш важливими проблемами лінійної алгебри. Ці проблеми з комп’ютерами інженери вирішували з перших днів.

Я не знавець історії, але, мабуть, тоді всі просто переписали свою версію FORTRAN простими петлями. Потім з'явилася деяка стандартизація, з ідентифікацією "ядер" (основних процедур), які потребують більшості лінійних проблем з алгебрами для їх вирішення. Ці основні операції були потім стандартизовані в специфікації, що називається: Основні лінійні алгебри підпрограми (BLAS). Потім інженери могли назвати ці стандартні, добре перевірені процедури BLAS у своєму коді, що значно полегшує їх роботу.

BLAS:

BLAS еволюціонував від рівня 1 (перша версія, яка визначала скалярний вектор і вектор-векторні операції) до рівня 2 (операції вектор-матриця) до рівня 3 (операції з матрицею-матрицею), і забезпечувала все більше і більше "ядер", настільки стандартизованих більше і більше основних операцій лінійної алгебри. Оригінальні реалізації FORTRAN 77 все ще доступні на веб-сайті Netlib .

На шляху до кращих показників:

Таким чином, з роками (особливо між релізами рівня 1 та 2 рівня BLAS: початок 80-х років) апаратне забезпечення змінилося з появою векторних операцій та ієрархій кешу. Ці еволюції дозволили значно підвищити продуктивність підпрограм BLAS. Тоді різні постачальники прийшли разом із впровадженням підпрограм BLAS, які були все більш ефективними.

Я не знаю всіх історичних реалізацій (я тоді не народився і не був дитиною), але два найпомітніші з них з'явилися на початку 2000-х: Intel MKL і GotoBLAS. Ваш Matlab використовує Intel MKL, який є дуже хорошим, оптимізованим BLAS, і це пояснює чудову ефективність, яку ви бачите.

Технічні деталі щодо множення матриці:

Так чому ж Matlab (MKL) настільки швидкий при dgemm(двоточне загальне множення матриці-матриці)? Простіше кажучи: адже він використовує векторизацію та гарне кешування даних. Складніше: див. Статтю Джонатана Мура.

В основному, виконуючи множення в наданому вами коді C ++, ви зовсім не сприймаєте кеш. Оскільки я підозрюю, що ви створили масив покажчиків на рядок масивів, ваш доступ у вашому внутрішньому циклі до k-го стовпця "matice2": matice2[m][k]дуже повільний. Дійсно, коли ви отримуєте доступ matice2[0][k], ви повинні отримати k-й елемент масиву 0 своєї матриці. Потім у наступній ітерації ви повинні отримати доступ matice2[1][k], що є k-м елементом іншого масиву (масив 1). Потім у наступній ітерації ви отримуєте доступ до ще одного масиву і так далі ... Оскільки вся матриця matice2не може вміститись у найвищі кеші (вона є 8*1024*1024великими байтами), програма повинна отримати бажаний елемент з основної пам'яті, втративши багато час.

Якщо ви просто перемістили матрицю, щоб доступ був у суміжних адресах пам'яті, ваш код уже працюватиме набагато швидше, оскільки тепер компілятор може одночасно завантажувати цілі рядки в кеш. Просто спробуйте цю змінену версію:

timer.start();
float temp = 0;
//transpose matice2
for (int p = 0; p < rozmer; p++)
{
    for (int q = 0; q < rozmer; q++)
    {
        tempmat[p][q] = matice2[q][p];
    }
}
for(int j = 0; j < rozmer; j++)
{
    for (int k = 0; k < rozmer; k++)
    {
        temp = 0;
        for (int m = 0; m < rozmer; m++)
        {
            temp = temp + matice1[j][m] * tempmat[k][m];
        }
        matice3[j][k] = temp;
    }
}
timer.stop();

Таким чином, ви можете бачити, як просто локалізація кеша значно підвищила продуктивність коду. Тепер реальні dgemmреалізації використовують це на дуже обширному рівні: вони виконують множення на блоки матриці, визначені розміром TLB (буфер перекладу для перегляду, короткий опис: що можна ефективно кешувати), щоб вони перетікали в процесор саме кількість даних, які вона може обробити. Інший аспект - векторизація, вони використовують векторизовані інструкції процесора для оптимальної пропускної здатності інструкцій, чого ви не можете реально зробити зі свого кросплатформенного коду С ++.

Нарешті, люди, які стверджують, що це з-за алгоритму Страссена або Копперсміта – Винограда, помиляються, обидва ці алгоритми не реалізовані на практиці через згадані вище апаратні міркування.


2
Я щойно переглянув відео Скотта Майєрса про важливість розмірів кешу та розміщення даних у розмірах рядків кеша, а також проблеми, які можуть виникнути з багатопотоковими рішеннями, які не мають спільних даних у джерелі, але в кінцевому підсумку з даними, якими надано обладнання / core-thread level: youtu.be/WDIkqP4JbkE
WillC

40

Ось чому . MATLAB не виконує наївне множення матриць, перебираючи на кожен елемент так, як ви робили у своєму C ++-коді.

Звичайно, я припускаю, що ви просто використовували C=A*Bзамість того, щоб писати функцію множення самостійно.


19

Matlab включив LAPACK деякий час тому, тому я припускаю, що їх множення матриць використовує щось принаймні так швидко. Вихідний код та документація LAPACK легко доступні.

Ви також можете переглянути документ Гото та Ван Де Гейна "Анатомія високоефективного множення матриць" на веб-сайті http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.140.1785&rep=rep1&type=pdf


7
MATLAB використовує бібліотеку Intel MKL, яка забезпечує оптимізовану реалізацію підпрограм BLAS / LAPACK: stackoverflow.com/a/16723946/97160
Amro

11

Відповідь - бібліотеки LAPACK і BLAS роблять MATLAB сліпуче швидким при операціях з матрицею, а не будь-яким патентованим кодом з боку людей у ​​MATLAB.

Використовуйте бібліотеки LAPACK та / або BLAS у вашому коді C ++ для матричних операцій, і ви повинні отримати схожу ефективність, як MATLAB. Ці бібліотеки повинні бути вільно доступними в будь-якій сучасній системі, а частини розроблялися протягом десятиліть у наукових колах. Зауважте, що існує кілька реалізацій, включаючи деякі закриті джерела, такі як Intel MKL .

Дискусія про те, як BLAS отримує високу ефективність , доступна тут.


До речі, в моєму досвіді це серйозний біль викликати бібліотеки LAPACK безпосередньо з c (але того варто). Вам потрібно дуже точно прочитати документацію.


8

Виконуючи множення матриць, ви використовуєте метод наївного множення, який потребує часу O(n^3).

Існує алгоритм множення матриць, який займає O(n^2.4). Це означає, що для n=2000вашого алгоритму потрібно обчислити ~ 100 разів більше, ніж найкращий алгоритм.
Ви дійсно повинні перевірити сторінку wikipedia на предмет множення матриць для отримання додаткової інформації про ефективні способи її реалізації.


і MATLAB, ймовірно, використовує такий алгоритм, оскільки час множення матриці 1024 * 1024 менше, ніж у 8 разів, часу для множення матриці 2048 * 2048! Молодці хлопці MATLAB.
Рено

4
Я скоріше сумніваюся, що вони використовують "ефективні" алгоритми множення, незважаючи на свої теоретичні переваги. Навіть алгоритм Страссена має труднощі з реалізацією, а алгоритм Coppersmith – Winograd, про який ви, напевно, читали просто просто , не практичний (зараз). Крім того , пов'язані SO нитка: stackoverflow.com/questions/17716565 / ...
Ernir

Цей алгоритм призначений лише для надзвичайно великих матриць.

@Renaud Ось таке визначення відносно постійних накладних витрат
Божевільний фізик

6

Залежно від вашої версії Matlab, я вважаю, що вона вже може використовувати ваш GPU.

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

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


4
Як важкий користувач ML, я можу сказати вам, що вони ще не використовують GPGPU. Нова версія matlab DO використовує SSE1 / 2 (нарешті). Але я зробив тести. MexFunction, що виконує мультиплікаційне множення, працює вдвічі швидше, ніж A.*Bце. Тож ОП майже напевно глумиться на щось.
KitsuneYMG

6
Matlab з інструментом Parallel Computing Toolbox може використовувати графічний процесор CUDA, але це явно - вам потрібно надіслати дані до GPU.
Едрік

Я використовую M1 = одиночний (rand (1024,1024) * 255); M2 = одинарний (rand (1024,1024) * 255); і M3 = M1 * M2; ... потім запишіть у двійковий файл floats, і все це зроблено дуже швидко.
Вовк

3

MATLAB використовує високооптимізовану реалізацію LAPACK від Intel, відому як Intel Math Kernel Library (Intel MKL) - зокрема функцію dgemm . Швидкість Ця бібліотека використовує переваги процесорних функцій, включаючи SIMD інструкції та багатоядерні процесори. Вони не документують, який конкретний алгоритм вони використовують. Якщо ви зателефонували в Intel MKL з C ++, ви побачите подібну продуктивність.

Я не впевнений, що бібліотека MATLAB використовує для множення GPU, але, мабуть, щось на зразок nVidia CUBLAS .


1
Ви маєте рацію, але ви бачили цю відповідь ? Однак IPP не є MKL, і MKL має значно кращі показники лінійної алгебри порівняно з IPP. Крім того, в останніх версіях IPP зняв свій матричний математичний модуль.
chappjc

Вибачте, я мав на увазі MKL не IPP
gregswiss

Ви праві, інша відповідь охоплює це. Це так багатослівно, що я його пропустив.
gregswiss

2

Загальна відповідь "Чому matlab швидше виконувати ххх, ніж інші програми", полягає в тому, що в matlab багато вбудованих, оптимізованих функцій.

Інші програми, які використовуються, часто не мають цих функцій, тому люди застосовують власні творчі рішення, які дивляться повільніше, ніж професійно оптимізований код.

Це можна трактувати двома способами:

1) Загальний / теоретичний шлях: Matlab не є значно швидшим, ви просто робите тест неправильно

2) Реалістичний спосіб: Для цього матеріал Matlab швидший на практиці, оскільки мови як c ++ занадто легко використовуються неефективно.


7
Він порівнює швидкість MATLAB зі швидкістю функції, яку він написав за дві хвилини. Я можу записати більш швидку функцію за 10 хвилин, або набагато швидшу функцію за дві години. Хлопці MATLAB витратили більше двох годин на швидке множення матриць.
gnasher729

2

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

Здається, ви склали матрицю списків списків? Список списків містить покажчики на списки, які потім містять елементи матриці. Місця, що містяться у списках, призначаються довільно. Коли ви перебираєте свій перший індекс (номер рядка?), Час доступу до пам'яті дуже важливий. Для порівняння, чому б вам не спробувати реалізувати матрицю як єдиний список / вектор, використовуючи наступний метод?

#include <vector>

struct matrix {
    matrix(int x, int y) : n_row(x), n_col(y), M(x * y) {}
    int n_row;
    int n_col;
    std::vector<double> M;
    double &operator()(int i, int j);
};

І

double &matrix::operator()(int i, int j) {
    return M[n_col * i + j];
}

Слід використовувати той самий алгоритм множення, щоб кількість флопів було однаковим. (n ^ 3 для квадратних матриць розміром n)

Я прошу вас вчасно це зробити так, щоб результат був порівнянний з тим, що ви мали раніше (на тій же машині). За допомогою порівняння ви точно покажете, наскільки важливим може бути час доступу до пам'яті!


2

У C ++ це повільно, оскільки ви не використовуєте багатопотоковість. По суті, якщо A = BC, де всі вони є матрицями, перший рядок A можна обчислити незалежно від 2-го ряду і т. Д. Якщо A, B і C всі n по n матриць, ви можете прискорити множення на коефіцієнт n ^ 2, як

a_ {i, j} = сума_ {k} b_ {i, k} c_ {k, j}

Якщо ви використовуєте, скажімо, Eigen [ http://eigen.tuxfamily.org/dox/GettingStarted.html ], багатопотокове вбудоване і кількість потоків регулюється.


2

Оскільки MATLAB - мова програмування, спочатку розроблена для чисельної лінійної алгебри (матричні маніпуляції), яка має спеціально розроблені бібліотеки для множення матриць. І в даний час MATLAB можна також використовувати графічні процесори (Графічний процесор) для цього додатково.

А якщо ми подивимось на ваші результати обчислень:

             1024x1024   2048x2048   4096x4096
             ---------   ---------   ---------
CUDA C (ms)      43.11      391.05     3407.99
C++ (ms)       6137.10    64369.29   551390.93
C# (ms)       10509.00   300684.00  2527250.00
Java (ms)      9149.90    92562.28   838357.94
MATLAB (ms)      75.01      423.10     3133.90

тоді ми можемо побачити, що не тільки MATLAB настільки швидкий у матричному множенні: CUDA C (мова програмування від NVIDIA) має кращі результати, ніж MATLAB. CUDA C також має спеціально розроблені бібліотеки для множення матриць, і вона використовує графічні процесори.

Коротка історія MATLAB

Клів Молер, голова відділу інформатики університету Нью-Мексико, почав розробляти MATLAB в кінці 1970-х. Він створив це, щоб надати своїм студентам доступ до LINPACK (бібліотека програмного забезпечення для виконання числової лінійної алгебри) та EISPACK(це бібліотека програмного забезпечення для чисельних обчислень лінійної алгебри), без того, щоб вони вивчали Фортран. Незабаром він розповсюдився в інші університети і знайшов сильну аудиторію у спільноті прикладної математики. Джек Літтл, інженер, зазнав цього під час візиту, який Молер здійснив до Стенфордського університету в 1983 році. Визнаючи його комерційний потенціал, він з'єднався з Молером та Стівом Бангертом. Вони переписали MATLAB в C і заснували MathWorks у 1984 році для продовження свого розвитку. Ці переписані бібліотеки були відомі як JACKPAC. У 2000 році MATLAB був переписаний для використання більш нового набору бібліотек для маніпулювання матрицею, LAPACK (це стандартна бібліотека програмного забезпечення для числової лінійної алгебри).

Джерело

Що таке CUDA C

CUDA C використовує також бібліотеки, спеціально розроблені для множення матриць, наприклад OpenGL (Open Graphics Library). Він також використовує GPU та Direct3D (у MS Windows).

Платформа CUDA призначена для роботи з мовами програмування, такими як C, C ++ та Fortran. Ця доступність полегшує спеціалістам паралельного програмування використання ресурсів GPU, на відміну від попередніх API, таких як Direct3D і OpenGL , для яких потрібні передові навички графічного програмування. Також CUDA підтримує рамки програмування, такі як OpenACC та OpenCL .

введіть тут опис зображення

Приклад потоку обробки CUDA:

  1. Скопіюйте дані з основної пам'яті в пам'ять GPU
  2. ЦП ініціює обчислювальне ядро ​​GPU
  3. Ядра CUDA GPU виконують ядро ​​паралельно
  4. Скопіюйте отримані дані з пам'яті GPU в основну пам'ять

Порівняння швидкості виконання процесора та GPU

Ми провели орієнтир, в якому вимірювали кількість часу, необхідне для виконання 50 часових кроків для розмірів сітки 64, 128, 512, 1024 і 2048 на процесорі Intel Xeon X5650, а потім за допомогою GPU NVIDIA Tesla C2050.

введіть тут опис зображення

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

Джерело

З вступу до посібника з програмування CUDA C:

Керований ненаситним попитом на ринок в реальному часі, 3D-графікою високої чіткості, програмований блок графічного процесора або GPU перетворився на високо паралельний багатопотоковий багатокористувальний процесор з величезними обчислювальними кінськими силами та дуже високою пропускною здатністю пам'яті, як це проілюстровано Figure 1та Figure 2.

Рисунок 1. Операції з плаваючою комою в секунду для процесора та GPU

введіть тут опис зображення

Малюнок 2 . Пропускна здатність пам'яті для процесора та GPU

введіть тут опис зображення

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

Малюнок 3 . GPU виділяє більше транзисторів для обробки даних

введіть тут опис зображення

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

Паралельна обробка даних відображає елементи даних у паралельні потоки обробки. Багато прикладних програм, які обробляють великі набори даних, можуть використовувати модель програмування паралельних даних для прискорення обчислень. У 3D-рендерінгу великі набори пікселів і вершин відображаються в паралельні нитки. Аналогічно, додатки для обробки зображень та медіа-файлів, такі як пост-обробка візуалізованих зображень, кодування відео та декодування, масштабування зображення, стерео-бачення та розпізнавання візерунків, можуть відображати блоки зображення та пікселі на потоки паралельної обробки. Насправді багато алгоритмів за межами поля візуалізації та обробки зображень прискорюються паралельною обробкою даних, від загальної обробки сигналів або фізичного моделювання до обчислювального фінансування чи обчислювальної біології.

Джерело

Розширене читання


Кілька цікавих фактів

Я написав множення матриці С ++, яке так само швидко, як і Матлаб, але це потребувало певної обережності. (До того, як Matlab використовував для цього GPU)

Цитування з цієї відповіді .


2
Остання остання цитата - це не факт, це порожнє вихваляння. Ця особа отримала декілька запитів на отримання коду, оскільки опублікувала це. Але жодного коду не видно.
Кріс Луенго

1
Ваш опис того, як швидко ви можете робити обчислення на GPU, взагалі не стосується питання. Всі ми знаємо, що 128 маленьких сердечників можуть виконувати більше однакової, одноманітної роботи, ніж 2 великих ядра. "І тепер MATLAB також може додатково використовувати графічні процесори (блок обробки графіки)." Так, але не за замовчуванням. Нормальне множення матриці все ще використовує BLAS.
Кріс Луенго

@CrisLuengo, гаразд, це не факт! Можливо, ви маєте рацію щодо його «хвалиння» - ми про це не знаємо, а також не знаємо, чому він не відповідає. Для другого коментаря: опис обчислень на GPU відповідає на питання, оскільки для матричного множення в лінійній алгебрі використовується операція з плаваючою комою. Можливо, це не для всіх людей зрозуміло, але я думаю, що вони повинні зрозуміти цю основу. В іншому випадку вони повинні засвоїти цю основу спочатку, перш ніж прочитати статтю про матриці. І якщо хтось інший напише мені про це, то я додам ці деталі. Дякую!
Бхарата

@CrisLuengo, я написав слово "additionally". Це означає: його можна використовувати. Це також означає, що нормальне множення матриць все ще використовує бібліотеки програм. Ви вважаєте, що я повинен змінити свою посаду, щоб бути більш зрозумілою? Дякую за коментарі!
Бхарата
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.